In this article, I will walk you through the code of the Etherdelta smart contract: ERC20 token transfers, orders creation, and trades settlement. The smart contract is written in Solidity, and the repo is publicly accessible on the Github of Etherdelta.
Decentralized exchanges are one of the most fascinating Blockchain technologies. They allow traders to buy or sell ERC20 tokens and other crypto assets without any centralized party. Everything happen in a smart contract, in a decentralized way.
Etherdelta was the first decentralized exchange to launch on the Ethereum network in 2017. At its heyday in late 2017, it used to have a daily trading volume of more than 10,000,000 (10M) dollars. Later, hacks and a growing list of rivals severely weakened its market dominance, until the final blow in 2018 when founder Zach Coburn was sued by the SEC in 2018 for illegally operating a security exchange.
Nonetheless, as a precursor of decentralized exchanges, it remains a special point of interest for developers.
Shameless Plug: If you are interested in learning how to build a decentralized exchange like Etherdelta, register to my course “Ethereum Dapps In Motion” on Manning.com
Table of content
- Functionalities of a crypto exchange
- Architecture of Etherdelta
- Trading process
- Overview of Etherdelta Github repo
- Files of interest
- Contracts of etherdelta.sol
- Token deposit
- Keeping track of tokens
- Tokens withdraw
- Ether deposits & withdrawals
- Creating an order
- Creating a trade: Overview
- Creating a trade: trade() function arguments
- Creating a trade: Checking conditions
- Creating a trade: Update token balances
- Creating a trade: storing filled amounts of orders
- On-chain orders
- Cancelling an order
- Conclusion
Functionalities of a crypto exchange
A crypto exchange has the following functionalities:
- Orderbook management: create, cancel and match orders
- Trade settlement: transfer assets once 2 orders are matched
- Asset custody: keep trader assets safely in custody
- Asset transfers: transfer safely trader assets between exchange and traders
Before decentralized exchanges, most of the crypto exchanges had all the above functionality centralized. The most critical functionality is the asset custody. If the wallet of an exchange is hacked, all trader funds can be lost. We saw this dramatic outcome multiple times: Bitfinex, Montgox, etc…
Decentralized exchanges promise to solve this problem by moving the asset custody to an (un-hackable) smart contract.
Architecture of Etherdelta
Etherdelta has the following components:
- a frontend web application that runs in the browser. Trader wallets are also in the frontend, usually in the form of a browser extension (Metamask)
- an on-chain backend (the Ethereum smart contract): trades settlement and asset custody
- an off-chain backend (server and database): orderbook
Trading process
I am going to explain the trading process assuming we have 2 traders that want to trade in the opposite direction:
- trader 1 wants to buy some tokens B, paying with some tokens A, i.e he already has some tokens A
- trader 2 wants to sell some tokens B, and get some token A in exchange, i.e he already has some tokens B
Step 1: Funding
First, a trader has to fund his account on the Etherdelta contract by sending the token (tokens A) that will be used for purchasing token B:
Step 2: Place order
Then, the trader places an order and sends it to the off-chain orderbook. This order is signed with the private key of the trader, so that anyone can verify later that this trader expressed the intent of:
- selling an amount X of token B
- against token A
- at a certain price
Step 3: Take order
Then, a second trader sees the first (signed) order on the orderbook and decides to trade against it. We say that the trader “take” or “fill” the order. The original signed order is fetched from the backend, and an Ethereum transaction is built with:
- the original signed order
- the intent of the second trader to fill it
This transaction is signed with the private key of the second trader, and sent to the Etherdelta smart contract:
Precision: this second trader has also already completed the Step 1 before (funding of Etherdelta contract with the token that will be sold).
Step 4:
Finally, the Etherdelta contract settles the trade by swapping tokens between the 2 traders. Once this is done, a notification is sent to the off-chain orderbook, which in turn delete the order:
Now, you probably have a better understanding of the trading process. Time to inspect the code!
Shameless Plug: If you are interested in learning how to build a decentralized exchange like Etherdelta, register to my course “Ethereum Dapps In Motion” on Manning.com
Overview of Etherdelta Github repo
The smart contract of Etherdelta is located in this Github repo. The repo contains Javascript files and a single Solidity file.
Note that all the code in this repo is related to the smart contract. The code for the frontend and the backend is not public.
Let’s inspect the dependencies in the package.json
file:
- an old version of web3 (
0.17.0-alpha
, latest version is1.0.0-beta44
in February 2019) - an old version of Solidity (
0.4.2
, latest version is0.5.4
in February in 2019) - an old version of Ganache (still using the old
testrpc
package instead of Ganache) - Some low-level Ethereum npm packages like
ethereumjs-abi
,ethereumjs-tx
On top of it, the last commit was in August 2017: this is some old-code, that doesn’t use any smart contract framework. Instead it uses some low-level Ethereum packages.
Files of interest
There are 4 files that are interesting:
- etherdelta.sol (312 LoC): the smart contracts (yes there are several) of Etherdelta.
- deploy.js (95 LoC): deployment of etherdelta.sol to the Ethereum blockchain
- test.js (818 LoC): Lots of tests for the
etherdelta.sol
smart contract - utility.js (1121 LoC): various wrappers around web3 + fallback on etherscan, and other utilities
We are going to focus on etherdelta.sol, which has all the core logic of the exchange.
Contracts of etherdelta.sol
The etherdelta.sol
file has 7 seven smart contracts (if you are a beginner in Solidity, the same file can have several smart contracts):
- SafeMath
- Token
- StandardToken
- ReserveToken
- AccountLevels
- AccountLevelsTest
- Etherdelta
Let’s go over them one by one.
SafeMath
contains functions to safely add, subtract and multiply integers. It is used by StandardToken
and Etherdelta
contracts. It avoids integer underflow or overflow, which is a potential security bug when dealing with numbers in Solidity. Some of you might be familiar with the implementation of the OpenZeppelin library. The implementation of Etherdelta is different though, and in your own contracts you should make sure to use the implementation of OpenZeppelin instead.
Token
is an interface for the ERC20 standard. It only contains function signatures, but no function bodies. It is a de-facto abstract contract that cannot directly used, but only inherited from. In recent versions of Solidity (i.e 0.5.x), we can use the interface
keyword instead of the contract
keyword to define abstract contracts and better signal our intentions to users.
Standard Token
is an implementation of the ERC20 standard. It inherits from the Token
interface defined just before. Like for SafeMath
, this is not a standard implementation, and in your own contracts you should use the implementation of OpenZeppellin instead.
ReserveToken
is a contract to create a mock token used in tests. It inherit from StandardToken
, which makes it an ERC20 token. It has a privileged user (i.e address) called minter
that can increase or decrease the existing token supply by respectively creating new tokens and destroying existing ones.
AccountLevels
allows to define several type of users: regular users, market maker silver, and market maker gold, with decreasing trading fees. It is an abstract contract that only defines an interface, i.e functions signatures without bodies. It actually has a single accountLevel()
function to return the level of a specific user.
AccountLevelsTest
is the implementation of AccountLevels
. It has a mapping of address
to integers
to store the account level of each user. It also defines an extra function setLevel
to change the account level of any user. it is used in tests only.
Etherdelta
is the main contract of the Etherdelta decentralized exchange, where all the action happens. In the rest of the article, I will focus on this contract. The first step is to understand the token deposit process.
Token deposit
To facilitate trading, the Etherdelta contract requires that users first send their tokens to the contract before being able to trade. Internally, Etherdelta maintains a ledger to keep track of who owns what.
On the frontend, traders will click on a “deposit” button, which will trigger 2 transactions successively:
- Call to the
approve()
function of the token contract, so that Etherdelta can move tokens on behalf of traders - Call to the
depositToken()
function of the Etherdelta contract, to actually transfer the token from the trader address to the Etherdelta contract, using the tokentransferFrom()
function
I don’t really see the point of making 2 transactions instead of just 1 transaction signed by the trader that would directly transfer his tokens to Etherdelta by calling the transfer()
function on the token contract. But that’s how Etherdelta work.
Now, let’s see how the depositToken()
function work inside the Etherdelta contract:
function depositToken(address token, uint amount) {
//remember to call Token(address).approve(this, amount) or this contract will not be able to do the transfer on your behalf.
if (token==0) throw;
if (!Token(token).transferFrom(msg.sender, this, amount)) throw;
tokens[token][msg.sender] = safeAdd(tokens[token][msg.sender], amount);
Deposit(token, msg.sender, amount, tokens[token][msg.sender]);
}
depositToken()
takes 2 arguments:
token
, the address of the ERC20 token to depositamount
, the amount of tokens to deposit
In this line:
if (!Token(token).transferFrom(msg.sender, this, amount)) throw;
Token(token)
instantiates a contract in Solidity, at the correct address (but does not deploy! – it’s just a representation of an existing token in memory). Then transferFrom()
is called on this contract instance to actually transfer the token from the trader to Etherdelta. Note that the keyword this
is casted to the address of Etherdelta.
You would think that the job is done, but we also need to remember that other traders will also send their token to Etherdelta (i.e at the same address!). We need a way to keep track of which tokens belongs to which trader. We will do this with a ledger, like a bank does.
Keeping track of tokens
In the depositToken()
function explained in the previous section, after the actual token transfer, this line is executed:
tokens[token][msg.sender] = safeAdd(tokens[token][msg.sender], amount);
This increments the token balance of the trader by the amount that was sent. You will notice the use of the safeAdd()
function, inherited from the SafeMath
math contract. This avoids underflow/overflow errors.
Let’s see how this tokens
variable is defined at the top of the smart contract:
mapping (address => mapping (address => uint)) public tokens;
This is a mapping of token addresses to another mapping of token balances. Not clear enough? In Javascript, this would be equivalent to an object with this shape:
const tokens = {
'0xA78rw...': { //Address of token A
'0x78Hbn...': 100, //Balance of trader '0x78Hbn...' in token A
'0xYu687...': 150, //Balance of trader '0xYu687...' In token A
...
},
'0xIo8Hb...': { //Address of token B
'0x78Hbn...': 45, //balance of trader '0x78Hbn...' in token B
'0x98UIu...': 9000, //balance of trader '0x98UIu...' in token B
...
},
...
};
We have seen how to deposit tokens. And since we are not studying the contract Etherdelta and not Bitconnect, there must also be a way to withdraw our tokens, right?
Tokens withdraw
The withdraw process is more simple that the deposit process. Only one call to the withdrawToken()
function of Etherdelta is required:
function withdrawToken(address token, uint amount) {
if (token==0) throw;
if (tokens[token][msg.sender] < amount) throw;
tokens[token][msg.sender] = safeSub(tokens[token][msg.sender], amount);
if (!Token(token).transfer(msg.sender, amount)) throw;
Withdraw(token, msg.sender, amount, tokens[token][msg.sender]);
}
There is a check to make sure that the amount to be withdrawn is not above the available token balance of the trader:
if (tokens[token][msg.sender] < amount) throw;
Then the balance is decreased in the internal token ledger of the smart contract:
tokens[token][msg.sender] = safeSub(tokens[token][msg.sender], amount);
Like in depositToken()
, we use a function of SafeMath
to do an arithmetic operation safely (safeSub()
). All the arithmetic operations in the Etherdelta contract do the same thing.
Ether deposits & withdrawals
The previous functions for depositing and withdrawing (depositToken()
and withdrawTokens()
) are for tokens only. For Ether, there are 2 similar functions, deposit()
and widthdraw()
.
deposit()
works similarly as depositToken()
, except that:
- it needs the
payable
keyword on the function signature to be able to receive Ether - it does not take any token argument: The
tokens[0]
in thetokens
mapping is for Ether -
it just needs one transaction, no need for the extra “approve” step.
function deposit() payable { tokens[0][msg.sender] = safeAdd(tokens[0][msg.sender], msg.value); Deposit(0, msg.sender, msg.value, tokens[0][msg.sender]); }
withdraw()
is also very similar to depositToken()
, except that:
- we don’t need to call
transferFrom()
on a token contract, but instead we callmsg.sender.call.value()
. Note that this is a deprecated way of sending money which can lead to reentrancy attack vulnerabilities (not the case in Etherdelta though, because state change is done BEFORE Ether transfer). You should use thetransfer()
function from now on. -
it does not take any token argument: The
tokens[0]
in thetokens
mapping is for Etherfunction withdraw(uint amount) { if (tokens[0][msg.sender] < amount) throw; tokens[0][msg.sender] = safeSub(tokens[0][msg.sender], amount); if (!msg.sender.call.value(amount)()) throw; Withdraw(0, msg.sender, amount, tokens[0][msg.sender]); }
Creating an order
After a trader has sent a token to the Etherdelta contract, he/she can start to create orders.
To create an order, the trader will create a message with the following fields:
- tokenGet: the token to buy
- tokenGive: the token to sell (will be the token that was sent before to the Etherdelta contract)
- amountGet: amount ot tokenGet to buy
- amountGive: amount of tokenGive to sell
- expires: block number at which the order becomes invalid
- nonce: random number to differentiate different orders with the same other parameters (allow to send the same order several times and differentiate between these orders)
- smartContractAddress: address of Etherdelta, allows to create orders specific to an instance of a smart contract
- user: address of trader
This might seem surprising, but there is no price field. The price is implied by amountGet
and amountTake
.
Let’s take an example. Let’s stay that a trader called Bob wants to buy 100 DAI token for 1 ETH. After having sent his 1 ETH to the Etherdelta smart contract, he will create an order containing:
- tokenGet: DAI
- tokenGive: ETH
- amountGet: 100
- amountGive: 1
- expires: (some future block number)
- nonce: (some random number)
- smartContractAddress: address of Etherdelta
- user: address of Bob
Once this message is created, Bob will hash it with sha3
and sign the hash. For signing, Bob, will use the the same private key (~address) that he used to send his 1 ETH to the Etherdelta smart contract, which is also the same as the one in the userAddress
field above.
Finally, Bob sends his order to the orderbook, on the backend server of Etherdelta, with this data:
- the (unsigned) order
- the signature (broken down into 3 magical parameters v, r, s)
What happens next?
Widget not in any sidebars
Creating a trade: Overview
To continue on the example started just before, let’s say that there is another trader called Alice. Alice just queried the orderbook on the backend server of Etherdelta (off-chain), and saw the order of Bob. Alice wants to trade against this order, i.e Alice wants to sell her 100 DAI for the 1 ETH of Bob.
Alice will call the trade()
function of the Etherdelta contract to execute the trade. When she will do so, she is basically saying to the smart contract:
“Hey, this guy called Bob with this address said he with willing to do this trade with anyone. I am willing to be the counterparty for X amount”
Based on this claim, the trade()
function needs to check that:
- Bob is willing to do this trade with these parameters
- Alice is willing to do this trade with these parameters
- Bob and Alice have deposited enough tokens to do this trade
- There is enough left in Bob order to trade with Alice (orders can be partially filled)
A smart contract can easily verify that the sender of a transaction is willing to do something: All transactions, including functions arguments, are signed by the sending address. However, in our case, it’s more complicated. Alice is the sender of the transaction, but we also want to verify Bob version of the story. Bob DID not send this transaction. Bob only signed a message, that is contained in the transaction.
So: 1 will be more challenging. 2, 3 and 4 will be easy.
In the next section, I will walk you through the arguments of the trade()
function.
Creating a trade: trade() function arguments
Let’s see the code of the trade()
function:
function trade(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce, address user, uint8 v, bytes32 r, bytes32 s, uint amount) {
//amount is in amountGet terms
bytes32 hash = sha256(this, tokenGet, amountGet, tokenGive, amountGive, expires, nonce);
if (!(
(orders[user][hash] || ecrecover(sha3("\x19Ethereum Signed Message:\n32", hash),v,r,s) == user) &&
block.number <= expires &&
safeAdd(orderFills[user][hash], amount) <= amountGet
)) throw;
tradeBalances(tokenGet, amountGet, tokenGive, amountGive, user, amount);
orderFills[user][hash] = safeAdd(orderFills[user][hash], amount);
Trade(tokenGet, amount, tokenGive, amountGive * amount / amountGet, user, msg.sender);
}
Wow, that’s a lot of logic!. Let’s breakdown what is happening.
trade()
receives as arguments:
- all of the fields of the order:
tokenGet
,amountGet
,tokenGive
,amountGive
,expires
,nonce,
user. No need to specify the
smartContractAddress` field, because the Etherdelta contract already knows its own address! - v, r, s, the 3 parameters of the order signature (created by Bob)
amount
is the amount oftokenGet
that Alice is willing to trade against Bob. If Alice is willing to take all of Bob order, thenamount
will be 100 (Dai).
Creating a trade: Checking conditions
Next, we compute the hash of the order of Bob with:
bytes32 hash = sha256(this, tokenGet, amountGet, tokenGive, amountGive, expires, nonce);
Remember, Bob signed the HASH of the order, not the order itself, so we need the hash to check against his signature. You will notice that we need to use the SAME EXACT hashing function off-chain when signing the order, otherwise this will not work.
You will also notice that we add the address of Etherdelta with the this
keyword. No need to pass this argument to the function arguments.
Next is the critical part, where we will make sure that Bob signed the order:
if (!(
(orders[user][hash] || ecrecover(sha3("\x19Ethereum Signed Message:\n32", hash),v,r,s) == user) &&
block.number <= expires &&
safeAdd(orderFills[user][hash], amount) <= amountGet
)) throw;
There is actually a lot happening here. Let’s break it down. First, we check if the order was added directly to the smart contract with orders[user][hash]
. This is an emergency mechanism to allow an on-chain orderbook in the case that the off-chain one goes offline. I will explain this in the next section, let’s skip this for now.
Most orders are not on-chain, so we will continue to the next testing condition:
ecrecover(sha3("\x19Ethereum Signed Message:\n32", hash),v,r,s) == user
This is where we test that the order was signed by Bob. Based on the order hash, and the signature of the hash (v, r, s), we are able to recover who is the signer, using the built-in ecrecover()
function. If this is equal to user
, BINGO, Bob did sign the order, and we can continue to the next testing condition.
Next, we make sure that the order has not expired:
block.number <= expires
And finally, we make sure that the unfilled portion of the order is big enough to accomodate the trading amount
desired by Alice:
safeAdd(orderFills[user][hash], amount) <= amountGet
We make use of a variable orderFilles
that I havent explained before. Please skip this for the moment, and I will come back to this variable later in this article.
Next, we need to update the token balances of Alice and Bob…
Creating a trade: Update token balances
The tradebalances()
function is an helper function that is called by the trade()
function. Its role is to update the balances of Bob and Alice to reflect their position AFTER the trade is done:
function tradeBalances(address tokenGet, uint amountGet, address tokenGive, uint amountGive, address user, uint amount) private {
uint feeMakeXfer = safeMul(amount, feeMake) / (1 ether);
uint feeTakeXfer = safeMul(amount, feeTake) / (1 ether);
uint feeRebateXfer = 0;
if (accountLevelsAddr != 0x0) {
uint accountLevel = AccountLevels(accountLevelsAddr).accountLevel(user);
if (accountLevel==1) feeRebateXfer = safeMul(amount, feeRebate) / (1 ether);
if (accountLevel==2) feeRebateXfer = feeTakeXfer;
}
tokens[tokenGet][msg.sender] = safeSub(tokens[tokenGet][msg.sender], safeAdd(amount, feeTakeXfer));
tokens[tokenGet][user] = safeAdd(tokens[tokenGet][user], safeSub(safeAdd(amount, feeRebateXfer), feeMakeXfer));
tokens[tokenGet][feeAccount] = safeAdd(tokens[tokenGet][feeAccount], safeSub(safeAdd(feeMakeXfer, feeTakeXfer), feeRebateXfer));
tokens[tokenGive][user] = safeSub(tokens[tokenGive][user], safeMul(amountGive, amount) / amountGet);
tokens[tokenGive][msg.sender] = safeAdd(tokens[tokenGive][msg.sender], safeMul(amountGive, amount) / amountGet);
}
It takes the same arguments as the trade()
function, except the order signature. At this point, the signature of Bob has been verified already. Note that it’s a private
function, so we cannot call it from outside the smart contract.
Etherdelta makes money by charging fees to traders. This is how these fees are calculated for Bob and Alice:
uint feeMakeXfer = safeMul(amount, feeMake) / (1 ether);
uint feeTakeXfer = safeMul(amount, feeTake) / (1 ether);
uint feeRebateXfer = 0;
if (accountLevelsAddr != 0x0) {
uint accountLevel = AccountLevels(accountLevelsAddr).accountLevel(user);
if (accountLevel==1) feeRebateXfer = safeMul(amount, feeRebate) / (1 ether);
if (accountLevel==2) feeRebateXfer = feeTakeXfer;
}
I am not going to go in the details of fees, but very briefly fees are based on:
- the amount of the trade. The bigger, the larger the fee
- the taker/maker nature of trader: a trader who creates an order (off-chain) is a MAKER, i.e it provides liquidity for other traders. Makers have lower fees to incentivize liquidity providers. Conversely, a trader who fills an order (on-chain) is a TAKER, i.e he/she consumes liquidity and pay higher fees
- The account levels. It enables a tier pricing structure to offer rebates to big traders. Currently not enabled in the deployment of Etherdelta (i.e
accountLevelsAddr
== 0x0 below).
Now that we know the fees for Alice and Bob, we are ready to update their respective token balances.
Remember, we defined a tokens
mapping at the beginning of the Etherdelta contract, to hold token balances for each trader. We will update this mapping. First for the tokenGet
(Dai):
tokens[tokenGet][msg.sender] = safeSub(tokens[tokenGet][msg.sender], safeAdd(amount, feeTakeXfer));
tokens[tokenGet][user] = safeAdd(tokens[tokenGet][user], safeSub(safeAdd(amount, feeRebateXfer), feeMakeXfer));
tokens[tokenGet][feeAccount] = safeAdd(tokens[tokenGet][feeAccount], safeSub(safeAdd(feeMakeXfer, feeTakeXfer), feeRebateXfer));
Remember: Alice is msg.sender
, and Bob is user
. We deduct the DAI from Alice balance and credit Bob. For both of them, we deduct the Etherdelta fee. This fee will be paid to the feeAccount
address that was defined when the Etherdelta was deployed. This address is controlled by the operator of Etherdelta.
Then for the tokenGive
(Ether):
tokens[tokenGive][user] = safeSub(tokens[tokenGive][user], safeMul(amountGive, amount) / amountGet);
tokens[tokenGive][msg.sender] = safeAdd(tokens[tokenGive][msg.sender], safeMul(amountGive, amount) / amountGet);
We do the opposite, i.e debit Bob and credit Alice. You will notice a few differences with tokenGet
:
- calculating the
tokenGive
amount to transfer is more complex. We use a ratio ofamountGive
/amountGet
to infer the trade price, and multiply by theamount
of the trade - There are no fees deducted. Fees are only paid in the
tokenGet
token. It’s an arbitrary choice of Etherdelta to make the contract more simple.
We are almost done with the trade process. However there is one more complication: we need to keep track of which orders have been filled or partially filled.
Widget not in any sidebars
Creating a trade: storing filled amounts of orders
Let’s say that Alice and Bob just traded. Alice took out all of Bob order. The backend server of Etherdelta will pick up on that and remove the order of the orderbook, so that nobody else tries to fill the order as well.
Now, let’s consider a new trader that we will call Fred. Let’s imagine that Fred saw the order of Bob in the orderbook, just after Alice, but just before the execution of the trade. Fred tries to fill the order of Bob and… what happen?
Well, if the Ether balance balance of Bob on the smart contract is not enough, the trade will fail: the resulting underflow error will be caught by safeSub()
used in the tradeTokens()
function and will abort the transaction.
However, if Bob sent for let’s say 2 Ethers to the Etherdelta contract (more that what was necessary for his first trade), and it hasn’t expired yet, the trade will be executed a second time and Bob will be very pissed! How could this happen? Well, the signed order presented by Alice is still the same signed order presented by Fred, and the smart contract will still accept it as a proof of Bob intention to trade.
In order to avoid this, we need to keep track of:
- the orders that were already traded on Etherdelta
- as well as the amount already traded
This line of code in the trade()
function will do just this, after the execution of tradeTokens()
:
orderFills[user][hash] = safeAdd(orderFills[user][hash], amount); //notice how we increment the amount already traded
Remember, user
is the address of Bob, hash
is the hash of his order, and amount
is the amount filled by Alice. As for the orderFills
variable, it is defined at the top of the contract:
mapping (address => mapping (bytes32 => uint)) public orderFills;
Re-using the Javascript object analogy, it’s equivalent to:
const orderFills = {
'0xA78rw...': { //Address of trader A - user who CREATED and SIGNED the order 'AHJrwjk...': 100, //Order AHJrwjk...' Has been filled for 100 units of `tokenGet`
'opIO9eq...': 10, //Order opIO9eq...' Has been filled for 10 units of `tokenGet`
...
},
...
};
That’s it for the main part of the logic. There are still a couple of quirks to cover, so let’s continue!
On-chain orders
There is always a slim chance that the off-chain orderbook goes offline. If that happens, traders need an emergency solution to still be able to place their orders. This can be especially critical in case of major market events that force them to react quickly. This being said, it is only an emergency solution, because it is much more costly and slow than the off-chain orderbook: a transaction fee needs to be paid for every order, instead of just paying for filling trades.
The function that allows to place on-chain order is the order()
function:
function order(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce) {
bytes32 hash = sha256(this, tokenGet, amountGet, tokenGive, amountGive, expires, nonce);
orders[msg.sender][hash] = true;
Order(tokenGet, amountGet, tokenGive, amountGive, expires, nonce, msg.sender);
}
This time, no need to have any complex logic to verify the authenticity of the order. The creator of the order himself/herself send the order. In this case, security is already guaranteed by the signing mechanism of Ethereum transactions.
We add the order to the orders
mapping, which is used only for on-chain orders:
orders[msg.sender][hash] = true;
It’s defined as:
mapping (address => mapping (bytes32 => bool)) public orders;
Or in other words, if that were a Javascript object:
const orderFills = {
'0xA78rw...': { //Address of trader A - user who CREATED and SIGNED the order 'AHJrwjk...': true, //Order AHJrwjk...' Has been placed
'opIO9eq...': true, //Order opIO9eq...' Has been placed
...
},
...
};
We have covered most of the features of Etherdelta. One more feature to cover (and it’s an important one: cancelling orders). Courage, we are almost out of the wood!
Cancelling an order
Let’s say that a trader sent an order to the backend server of Etherdelta. The order is not matched right away, some times passes, and in the meantime the market moves a lot. The level at which the order was made is not a good one anymore, and the trader wants to cancel his/her order.
We could have a mechanism where the backend server has an API for cancelling orders. But that’s not safe enough for the trader, because if unfortunately the backend server is hacked, his/her orders can still be matched against another order, and the smart contract will happily settle the trade.
Fortunately, the Etherdelta has a mechanism to solve this. It’s possible to call a function directly on the Etherdelta contract to cancel a trade:
function cancelOrder(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce, uint8 v, bytes32 r, bytes32 s) {
bytes32 hash = sha256(this, tokenGet, amountGet, tokenGive, amountGive, expires, nonce);
if (!(orders[msg.sender][hash] || ecrecover(sha3("\x19Ethereum Signed Message:\n32", hash),v,r,s) == msg.sender)) throw;
orderFills[msg.sender][hash] = amountGet;
Cancel(tokenGet, amountGet, tokenGive, amountGive, expires, nonce, msg.sender, v, r, s);
}
The arguments are similar to previous functions that we saw before in this article, so I will not repeat myself, but in a nutshell we need to provide all the info of an order, plus a signature of the order hash.
In the body function, we then hash the order, and we make sure that the signature provided is the one of the sender of the transaction.
Then, we fill the order at 100% with:
orderFills[msg.sender][hash] = amountGet;
It’s important to understand that there is no direct mechanism to cancel an order. We just pretend it has been filled entirely. If any trader tries to trade against this order later, the Etherdelta smart contract will reject the trade, as the unfilled portion of the order is 0.
Finally, we emit a Cancel
event, so that the (centralized) backend of Etherdelta remove the order from its orderbook:
Cancel(tokenGet, amountGet, tokenGive, amountGive, expires, nonce, msg.sender, v, r, s);
Conclusion
We just went through the whole process of trading a token in the Etherdelta smart contract, going through all the steps in the code!
We learned:
- how the tokens are transferred between traders
- how orders are created
- how orders are cancelled
- how trades are created
- how Etherdelta manage to keep its orderbook off-chain but still settle trades on-chain, using signed orders and cryptography to maintain the security of the system
Shameless Plug: If you are interested in learning how to build a decentralized exchange like Etherdelta, register to my course “Ethereum Dapps In Motion” on Manning.com
"I don’t really see the point of making 2 transactions instead of just 1 transaction signed by the trader that would directly transfer his tokens to Etherdelta by calling the transfer() function on the token contract. But that’s how Etherdelta work."
Because Etherdelta needs to know from whom the tokens came from so that it can increment the balance for that specific user. Etherdelta SC would not even be aware that someone sent tokens to it if somebody just uses transfer().