GridPlus / cryptobridge-contracts

Smart contracts for trustless bridges
MIT License
75 stars 31 forks source link

Cryptobridge Contracts

This module was experimental and has been deprecated in favor of superior Plasma designs within the Ethereum community

WARNING: This package is functional, but is unaudited and in still in development. It should not be used in production systems with large amounts of value.

This repo implements the trustless EVM bridge (now termed "cryptobridge") contract. For more background on the concept, see this article. The bridges are maintained by networks of participants running the cryptobridge client.

Bridge

Bridge Basics

A bridge exists as two contracts (Bridge.sol) on two separate EVM-based blockchains. A set of participants may stake (Bridge.stake()) a specified token (Bridge.stakeToken()) to enter the pool of proposer candidates. Every time a piece of data is submitted to a bridge, a new proposer is chosen pseudorandomly (Bridge.getProposer()) with probability proportional to the participant's stake.

This proposer listens to the bridged blockchain and collects block headers until he/she is ready to submit data to the bridge contract on his/her origin blockchain (i.e. where he/she is currently the proposer). At such a time, the proposer packages the block headers into a Merkle root (note: for now, the number of headers being packaged must be a power of two) and includes the block number of the last packaged header (the starting block number is assumed to be 1 greater than the last checkpointed header in the previous header root saved to the Bridge).

With data in hand, the proposer passes root, chainAddr, startBlock, endBlock to the other staking participants, who are currently validators. Note that chainAddr corresponds to the address of the Bridge contract on the blockchain being bridged. If this root is consistent with the one the validators compute, they will sign the following hash: keccak256(root, chainAddr, startBlock, endBlock), where arguments are tightly packed as they would be in Solidity (i.e. with numbers being left-padded to 32 bytes).

Once enough validators sign off (at least Bridge.validatorThreshold()), the proposer may submit the data to the bridge via Bridge.proposeRoot(). Assuming the signatures are correct, the proposer will be rewarded based on the current reward (Bridge.reward()). This is parameterized by Bridge.updateReward() and is a function of the number of blocks elapsed since the last root was checkpointed. This allows the proposer to wait until it is profitable to checkpoint the data (e.g. to wait out periods of high gas prices). Note that there is also a cutoff number of blocks, after which anyone may proposer a header with signatures and receive the reward. In future versions, this cutoff can be made into a random range to avoid proposers from waiting too long.

APIs

The following is a set of APIs for the end user, stakers/proposer, and admin. If you would like to get started installing and testing this package, please skip to Installation and Setup

User API

Users may deposit tokens on the bridge contract in their blockchain and withdraw them from the corresponding bridge contract in the destination blockchain. Since the proposer only relays a single Merkle root hash, the user has to prove a few things from several pieces of data.

deposit (token, toChain, amount)

Function: deposit
Purpose: Deposit tokens so that they can be withdrawn on another chain.
Arguments:
  * token (address: the address of the token being deposited)
  * toChain (address: the address of the corresponding bridged blockchain, i.e where the coins will be withdrawn)
  * amount (uint256: amount to deposit)

Notes:

prepWithdraw ( v, [r, s, txRoot], addrs, amount, path, parentNodes, netVersion, rlpDepositTxData, rlpWithdrawTxData)

Function: prepWithdraw
Purpose: Step 1 of withdrawal. Initialize a withdrawal and prove a transaction. Save the transaction root and other data.
Arguments:
  * v (bytes: value of v received from transaction receipt in origin chain, see note below)
  * [r, s, txRoot] (1)
  * addrs (address[3]: [fromChain, depositToken, withdrawToken]. fromChain = address of origin chain bridge contract, depositToken = address of token deposited in the origin chain, withdrawToken = address of mapped token in this chain)
  * amount (bytes: amount deposited in origin chain, hex integer, atomic units)
  * path (bytes: path of deposit transaction in the transactions Merkle-Patricia tree)
  * parentNodes (bytes: concatenated list of parent nodes in the transaction Merkle-Patricia tree)
  * netVersion (bytes: version of the origin chain, only needed if v is EIP155 form, can be called from web3.version.network)   
  * rlpDepositTxData (rlp binary encoded Deposit transaction data)
  * rlpWithdrawTxData (rlp binary encoded Withdraw transaction data)

(1) [r, s, txRoot]

JavaScript code Example

      // Make the transaction
      const prepWithdraw = await BridgeA.prepWithdraw(
        deposit.v, 
        [deposit.r, deposit.s, depositBlock.transactionsRoot],
        [BridgeB.options.address, tokenB.options.address, tokenA.address], 
        5,
        path, 
        parentNodes, 
        version,
        rlpDepositTxData.toString('binary'),
        rlpWithdrawTxData.toString('binary'),
        { from: wallets[2][0], gas: 500000 }
      );

For more details on how to setup the transaction, see test/bridge.js.

Notes:

proveReceipt (logs, cumulativeGas, logsBloom, receiptsRoot, path, parentNodes)

Function: proveReceipt
Purpose: Step 2 of withdrawal. Prove a receipt and save the receipts root to an existing pending withdrawal.
Arguments:
  * logs (bytes: encoded logs, see below)
  * cumulativeGas (bytes: amount of gas used after this transaction completed in the block, hex integer)
  * logsBloom (bytes: raw data from deposit transaction receipt)
  * receiptsRoot (bytes32: root of the receipts in the deposit's block)
  * path (bytes: path of the receipt in the receipt Merkle-Patricia tree)
  * parentNodes (bytes: concatenated list of parent nodes in the receipt Merkle-Patricia tree)

Notes:

withdraw (blockNum, timestamp, prevHeader, rootN, proof)

Function: withdraw
Purpose: Step 3 of withdrawal. Prove block header and receive tokens.
Arguments:
  * blockNum (uint256: block number the block containing the deposit on the bridged blockchain)
  * timestamp (uint256: timestamp on the block containing the deposit on the bridged blockchain)
  * prevHeader (bytes32: the previous modified block header (NOT Ethereum block header), see note below for formatting)
  * rootN (uint256: index of the header root corresponding to the origin chain)
  * proof (bytes: concatenated Merkle proof, see note below for formatting)

Notes:

getTokenMapping (chain, token) constant

Function: getTokenMapping
Purpose: Get the token associated with your token from another chain. This will be your withdrawal token if you deposit the other one.
Arguments:
  * chain (address: the bridge contract on the origin chain where you would deposit your tokens)
  * token (address: the token you would deposit)
Returns:
  * address: the token you will receive as a withdrawal on this chain if you deposit your token on your chain

getLastBlockNum (fromChain) constant

Function: getLastBlockNum
Purpose: Find the most recent block on the given chain that has been included in a proposed header root. If you have a deposit in a block less than or equal to this one on the provided chain, you may begin the withdrawal process.
Arguments:
  * fromChain (address: the bridge contract on the origin chain)
Returns:
  * uint256: last block on the origin chain that was relayed to this chain

Staker API

Any participant may join a staking pool, but future versions may give the option to whitelist a set of participants.

stake (amount)

Function: stake
Purpose: Join a staking pool or add to your stake in the pre-determined stakeToken.
Arguments:
 * amount (uint256: atomic units of staking token to add to the pool. This will credit your account with more stake.

Notes:

destake (amount)

Function: destake
Purpose: Remove stake from a poo in the pre-determined stakeToken.
Arguments:
 * amount (uint256: atomic units of staking token to remove from the pool)

Notes:

proposeRoot (headerRoot, chainId, end, sigs)

Function: proposeRoot
Purpose: May only be called by elected proposer, submit a headerRoot and validator signatures and receive a reward in return.
Arguments:
 * headerRoot (bytes32: the Merkle root of the modified block headers since the last block checkpointed. See withdraw() notes on block header formatting. Ordering in Merkle tree is based on block number)
 * chainId (address: location of bridge contract on connected chain)
 * end (uint256: last block number in the header Merkle tree corresponding to the root being submited)
 * sigs (bytes: concatenated list of signatures of form 'r,s,v'. See notes on `prepWithdraw()` for instructions on formatting `v`)

Notes:

getProposer () constant

Function: getProposer
Purpose: Get the current proposer for all chains.

Notes:

Admin API

Admin functionality is key to running a clean bridge. In v0.1, there is only one admin - the user who deploys the contract. In the future, this role can be delegated to the stakers or an elected representative.

Bridge (token)

Function: default function
Purpose: Set admin and staking token
Arguments:
 * token (address: the staking token. Once set, this cannot be changed!)

addToken (newToken, origToken, fromChain) onlyAdmin

Function: addToken
Purpose: Create a token and move all units to this bridge contract, then associate to a token on an existing bridge.
Arguments:
 * newToken (address: token on this blockchain to map)
 * origToken (address: token on fromChain to map)
 * fromChain (address: bridge contract on the bridged blockchain)

Notes:

associateToken (newToken, origToken, toChain) onlyAdmin

Function: associateToken
Purpose: Associate an existing token to a replicated token.
Arguments:
 * newToken (address: newly replicated token on bridged blockchain)
 * origtoken (address: token on this blockchain to map)
 * fromChain (address: bridge contract on blockchain housing the replicated token)

Notes:

updateValidatorThreshold (newThreshold) onlyAdmin

Function: updateValidatorThreshold
Purpose: Change the number of validators required to propose a root
Arguments:
 * newThreshold (uint256: new number of validators needed to propose a root)

Notes:

updateReward (base, a, max)

Function: updateReward
Purpose: Change the reward issued to the proposer
Arguments:
 * base (uint256: minimum number of tokens rewarded for proposing a root)
 * a (uint256: number of tokens per additional block in the range of the root tree)
 * max (uint256: maximum number of tokens rewarded for proposing a root)

Notes:

Installation and Setup

EthPM

This package is not yet installable via EthPM.

Setup and Testing

In order to run tests against the contract, execute the following commands, which should be self-explaining

git clone https://github.com/GridPlus/cryptobridge-contracts.git
cd cry*ts
npm install
cp secretsTEMPLATE.json secrets.json
npm install -g truffle
truffle install tokens

Important: If you are a network participant, then you have to replace the seed-phrase within secrets.json with your own unique seed-phrase. For a simple isolated test-run, secrets.json can remain as it is.

Starting Test Networks

The convenience script parity/boot.js boots multiple parity instances with one command. All instances will have instant sealing. Unfortunately, this will be a lot slower than using TestRPC/Ganache (1).

npm run parity 7545 8545

Testing

Start the tests via truffle (which launches truffle.js first, then test/bridge.js)

truffle compile
truffle test

Further testing runs (with no contract changes) only require truffle test.

For the case you ran into problems, cleanup the build directory with rm -rf build (or rmdir /S /Q build) before running truffle compile && truffle test.

Sending Tokens

A convenience script is included to allow you to send tokens to a recipient once your secrets.json file is set up. If you'd like to see which options you may use, run:

node scripts/sendTokens.js --help

Here is an example using the default network (localhost:7545):

node scripts/sendTokens.js --token 0x87a464eb78986993a16bbeff76e1e3f0cd181060 --to 0xd1aa9d98da70774190b6a80fbead38b1e4e07928 --number 100

(1) TestRPC/Ganache

Unfortunately TestRPC/Ganache are incompatible with these tests because they do not provide v, r, s signature parameters for transactions (see issue).