
As 0x project’s whitepaper described “0x is an open protocol for decentralized exchange on the Ethereum blockchain.” In simple terms, 0x defines all the rules which an exchange need’s to perform trade functionality. Anyone can build a decentralized exchange using 0x protocol. Linda Xie wrote an awesome blog about 0x protocol which you can read here.
The main property of a decentralize exchange is giving custodianship to user/traders of their own asset. Unlike a centralized crypto exchange (ex — coinbase, binance) where the exchange stores a user’s private key, in decentralized exchange, private keys always remain with the the users, hence the user is always in control of his own asset. Isn’t it amazing?
A Relayer hosts an off-chain order book. Using relayers, users can find, create, fill or cancel orders. Relayers helps traders discover counter-parties and move cryptographically orders between them. A relayer can talk to other relayers and create a pool of orders to increase liquidity.
I’ve been following the 0x protocol since last year and they have one of the best developer wikis out there!
0x makes it so just about anyone can build a decentralized exchange and earn money. If an entire exchange is too much, you can start with just building a relayer. Today decentralized exchanges are in their infancy stage, but improved tech and UX will attract more people towards DeX in future. Now let’s start building our relayer.
Clone the 0x starter project from GitHub.
git clone https://github.com/0xProject/0x-starter-project.git
Install all the dependencies (we use Yarn: brew install yarn --without-node
):
yarn
Pull the latest Ganache 0x snapshot with all the 0x contracts pre-deployed:
yarn download_snapshot
In a separate terminal, navigate to the project directory and start Ganache:
yarn ganache-cli
In this tutorial, we will perform three operations.
Now we will understand how to create an order, validate it and fill it using 0x-starter-project.
Imports
We will import different utilities which will help us throughout this tutorial. We will also import configuration files. For now, we are using ganache with already 0x smart contracts are deployed privately.
import {
assetDataUtils,
BigNumber,
ContractWrappers,
generatePseudoRandomSalt,
Order,
orderHashUtils,
signatureUtils,
} from '0x.js';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { NETWORK_CONFIGS, TX_DEFAULTS } from '../configs';
import { DECIMALS, NULL_ADDRESS, ZERO } from '../constants';
import { getContractAddressesForNetwork, getContractWrappersConfig } from '../contracts';
import { PrintUtils } from '../print_utils';
import { providerEngine } from '../provider_engine';
import { getRandomFutureDateInSeconds } from '../utils';
You can find all these files the 0x-starter-project. Go check out them, look at configurations and settings.
Provider and constructor
We need to initialize contractWrappers. This provides helper functions around 0x contracts as well as ERC20/ERC721 token contracts on the blockchain. It takes provideEngine
and network configuration of ganache, which will be running by default on http://localhost:8545.
const contractWrappers = new ContractWrappers(providerEngine, getContractWrappersConfig(NETWORK_CONFIGS.networkId));
Web3Provider - Web3 provider allows your application to communicate with an Ethereum Node. A provider can be any module or class instance that implements thesendAsync
method (simply send
in web3 V1.0 Beta). That's it. What this sendAsync
method does is take JSON RPC payload requests and handles them. Web3.js is an example of a javascript module that contains a Web3 HTTP Provider. Using a configured Web3 Provider, the application can request signatures, estimate gas and gas price, and submit transactions to an Ethereum node. The simplest example of a provider is:
const provider = new web3.providers.HttpProvider('http://localhost:8545');
This provider simply takes each JSON RPC payload it receives and sends it on to the Ethereum node running on port 8545.¹
Initialize web3wrapper and get address information
Initialize the Web3Wrapper using provideEngine
, this provides helper functions around fetching account information.
const web3Wrapper = new Web3Wrapper(providerEngine);
const [maker, taker] = await web3Wrapper.getAvailableAddressesAsync();
Initializing address
For this tutorial, we are trading ZRX token with WETH (wrapped eth, erc20 compatible version of eth, eth itself is not ERC20 compatible token, Eth and weth has 1:1 exchange ratio). For that, we need to have the contract address of our 0x protocol to execute orders and ZRX token and WETH token’s ERC20 contract.
const contractAddresses = getContractAddressesForNetwork(NETWORK_CONFIGS.networkId);
const zrxTokenAddress = contractAddresses.zrxToken;
const etherTokenAddress = contractAddresses.etherToken;
Encoding Assets
For this tutorial, maker (seller/order creator) is creating an order of 5 ZRX tokens in exchange of .1 weth. We also need to take care of decimal points as different tokens can use different decimal points. In our case both weth/eth and zrx has 18 decimal points. 0x v2 uses Ethereum ABI encoding scheme to encode asset data into hex strings to encode all the information needed to identify an asset. That’s what we are doing here.
const makerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(5), DECIMALS);
const takerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS);
const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxTokenAddress);
const takerAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddress);
Allowance for transacting assets
We need to allow 0x contract to move assets on the behalf or maker and taker. As we are transacting ETH so first we need to convert ETH into WETH by depositing ETH on WETH contract.
const makerZRXApprovalTxHash = await contractWrappers.erc20Token.setUnlimitedProxyAllowanceAsync(
zrxTokenAddress,
maker,
);
const takerWETHApprovalTxHash = await contractWrappers.erc20Token.setUnlimitedProxyAllowanceAsync(
etherTokenAddress,
taker,
);
const takerWETHDepositTxHash = await contractWrappers.etherToken.depositAsync(
etherTokenAddress,
takerAssetAmount,
taker,
);
Creating an Order
0x protocol has below specific parameters which are needed to create an order. We will set these parameters and will create our order. We will use NULL_ADDRESS
allowing anyone to fill our order.
**exchangeAddress**
: The Exchange address.**makerAddress**
: Ethereum address of our Maker.**takerAddress**
: Ethereum address of our Taker.**senderAddress**
: Ethereum address of a required Sender (none for now).**feeRecipientAddress**
: Ethereum address of our Relayer (none for now).**expirationTimeSeconds**
: When will the order expire (in unix time).**salt**
: Random number to make the order (and therefore its hash) unique.**makerAssetAmount**
: The amount of token the Maker is offering.**takerAssetAmount**
: The amount of token the Maker is requesting from the Taker.**makerAssetData**
: The token address the Maker is offering.**takerAssetData**
: The token address the Maker is requesting from the Taker.**makerFee**
: How many ZRX the Maker will pay as a fee to the Relayer.**takerFee**
: How many ZRX the Taker will pay as a fee to the Relayer.const randomExpiration = getRandomFutureDateInSeconds();const exchangeAddress = contractAddresses.exchange;
const order: Order = {
exchangeAddress,
makerAddress: maker,
takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS,
expirationTimeSeconds: randomExpiration,
salt: generatePseudoRandomSalt(),
makerAssetAmount,
takerAssetAmount,
makerAssetData,
takerAssetData,
makerFee: ZERO,
takerFee: ZERO,
};
Signing Order
Now we need to sign the order with maker’s private key so we can prove that specificed maker address is owned by maker. We will append this signature to our order. To do that we will perform these steps:
const orderHashHex = orderHashUtils.getOrderHashHex(order);
const signature = await signatureUtils.ecSignHashAsync(providerEngine, orderHashHex, maker);
const signedOrder = { ...order, signature };
Validating Order
We need to validate the order is Fillable before calling fillOrder This checks both the maker and taker balances and allowances to ensure it is fillable up to takerAssetAmount. Also if there is any order manipulation by anyone, we will also get to know by validating our order.
await contractWrappers.exchange.validateFillOrderThrowIfInvalidAsync
(signedOrder, takerAssetAmount, taker);
Filling Order
Now if everything worked fine then we can validate our order now using 0x protocol. We will also set the gas limit for executing our order.
txHash = await contractWrappers.exchange.fillOrderAsync(signedOrder, takerAssetAmount, taker, {
gasLimit: TX_DEFAULTS.gas,
});
That’s it. We have filled our order.
yarn scenario:fill_order_erc20
You will see output something like below. If you see the fill_order_erc20.ts
. The above code is available in the same file and it is using printUtils
to print the output below.
yarn run v1.10.1
$ cross-env yarn build && node ./lib/scenarios/fill_order_erc20.js
$ cross-env yarn pre_build && tsc
$ run-s copy_artifacts
$ copyfiles -u 1 './src/artifacts/**/*' ./lib
┌────────────┐
│ Fill Order │
└────────────┘
Accounts
Maker 0x5409ed021d9299bf6814279a6a1411a7e866a631
Taker 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb
Setup
Maker ZRX Approval 0xffa485e507252a439be2677a977e791d9b5ba829251481a3b43479326f55da45
Taker WETH Approval 0x45cdef330d7d0a97cb058a8ba7b25735ee548aabcf00787d178e1fe3fd41200b
Taker WETH Deposit 0xd57ac57521b8040f2a8cf8ee6d1109bd55c283ca4b818d0ca58599ba58625e7d
Order
exchangeAddress 0x48bacb9266a570d521063ef5dd96e61686dbe788
makerAddress 0x5409ed021d9299bf6814279a6a1411a7e866a631
takerAddress 0x0000000000000000000000000000000000000000
senderAddress 0x0000000000000000000000000000000000000000
feeRecipientAddress 0x0000000000000000000000000000000000000000
expirationTimeSeconds 1540250008
salt 1030945504406868252934488830320408688272183043132497204228070027505852506…
makerAssetAmount 5000000000000000000
takerAssetAmount 100000000000000000
makerAssetData 0xf47261b0000000000000000000000000871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c
takerAssetData 0xf47261b00000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082
makerFee 0
takerFee 0
Allowances
Token Maker Taker
WETH 0 MAX_UINT
ZRX MAX_UINT 0
Balances
Token Maker Taker
WETH 0.5 0.1
ZRX 999999980 20
Transaction
┌───────────┬────────────────────────────────────────────────────────────────────┐
│ fillOrder │ 0x3dbe6243a61507aff8f60b88585baa2781bfc959c19334dd953c45a90cbadb28 │
├───────────┼────────────────────────────────────────────────────────────────────┤
│ orderHash │ 0x4fce1fd1a979119fdc4dc09a199a3bc3284bfabde3c455b24d222110dad2425c │
├───────────┼────────────────────────────────────────────────────────────────────┤
│ gasUsed │ 111748 │
├───────────┼────────────────────────────────────────────────────────────────────┤
│ status │ Success │
└───────────┴────────────────────────────────────────────────────────────────────┘
Logs
Fill
contract 0x48bacb9266a570d521063ef5dd96e61686dbe788
makerAddress 0x5409ed021d9299bf6814279a6a1411a7e866a631
feeRecipientAddress 0x0000000000000000000000000000000000000000
takerAddress 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb
senderAddress 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb
makerAssetFilledAmount 5000000000000000000
takerAssetFilledAmount 100000000000000000
makerFeePaid 0
takerFeePaid 0
orderHash 0x4fce1fd1a979119fdc4dc09a199a3bc3284bfabde3c455b24d222110dad2425c
makerAssetData 0xf47261b0000000000000000000000000871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c
takerAssetData 0xf47261b00000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082
Transfer
contract 0x871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c
_from 0x5409ed021d9299bf6814279a6a1411a7e866a631
_to 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb
_value 5000000000000000000
Transfer
contract 0x0b1ba0af832d7c05fd64161e0db78e85978e8082
_from 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb
_to 0x5409ed021d9299bf6814279a6a1411a7e866a631
_value 100000000000000000
Balances
Token Maker Taker
WETH 0.6 0
ZRX 999999975 25
Done in 8.25s.
Conclusion
So, we have learned how we can create and fill orders using 0x protocol.
We are using ganache but we can submit and fill the order to a relayer using 0x/connect (which is a wrapper of 0x protocol’s Standard Relayer API) We will check this in future tutorials.
Next, we’ll discuss different order book strategies. If you have any doubt you can refer to 0x project docs. They have awesome documentation.
Please comment below, if you have more questions about building your own decentralize exchange or relayer.
Thanks!
Originally published:
November 9, 2018