Writing the contract

Let's begin writing the orderbook smart contract with the functions that we discussed in the previous section:

  1. Start by creating a smart contract file called Orderbook.sol.
  2. We begin by defining first the Solidity compiler version:
pragma solidity ^0.5.2;
  1. Next, we import the open-zeppelin contract template (ERC20.sol):
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";

Importing the ERC20 contract allows our smart contract to transfer the Gold and USD ERC20 tokens to and from the trader's Ethereum accounts, using the Transfer method in the ERC20 contract. 

  1. Next, we define the contract name as Orderbook:
contract Orderbook {
using SafeMath for uint;

We also use the using keyword to declare that we'll be using the SafeMath library for all integer arithmetic calculations.

  1. Start by declaring a data structure for the orders or the offers that will be managed by our orderbook. To do so we define the Order struct:
struct Order
{
uint Amount;
uint Price;
uint TimeStamp;
address Trader;
bytes2 Status;
}

The Order struct has five members, namely Amount, Price, TimeStamp, Trader, and Status. Amount indicates the amount of gold in grams that is being bought or sold in the order.

Price indicates the price in US dollars per gram of gold that the buyer is willing to pay, or that the seller is expecting as payment.

TimeStamp stores the time at which the order was placed in Universal Time Coordinated (UTC).

Trader stores the Ethereum address of the trader who placed the order.

Status shows the current status of the order ('A'—Available, 'T'—Traded).

  1. We define two struct arrays using our Order structs called Buys and Sells. As the name suggests, the Buy[] array will capture buys and Sells[] will capture sells:
Order[] Buys;
Order[] Sells;
ERC20 public ERC20Base;
ERC20 public ERC20Counter;
address owner;

We also define two contract instances using the ERC20 contract. The ERC20Base instance will point to our USD token and the ERC20Counter instance will point to our Gold token.

  1. Lastly, we define an address variable to capture the contract owner's address:
modifier onlyOwner {
if (msg.sender!=owner) revert();
_;
}

A modifier onlyOwner is also created. It checks if the contract invoker is the contract owner.

  1. When the contract is first loaded, our constructor is fired. The constructor takes the address of the Base and Counter tokens as input parameters. For our project, these will be the USD token address and the Gold token address, respectively. We also set the owner address variable to the address that deploys the contract (msg.sender):
constructor (address Base,address Counter) public
{
ERC20Base = ER20(Base);
ERC20Counter = ERC20(Counter);
owner = msg.sender;
}
  1. There are three types of events that will be emitted by our contract. The events are namely BuyAdded, SellAdded, and TradeAdd:
event BuyAdded(
uint indexed Order_No,
uint Amt,
uint Price,
address trader
);

event SellAdded(
uint indexed Order_No,
uint Amt,
uint Price,
address trader
);

event TradeAdd(
uint indexed Order_No,
uint Amt,
uint Price,
address maker,
address taker
);

All three emit the order and trade details, including the OrderNo, Amount, Price, and the address of the trader who placed the order. In the case of TradeAdd, the maker is the trader who placed the order that is being traded, and the taker is the trader who matches against the order.

  1. Now, we come to the interesting part. The addBuy method accepts an order from the user, and adds it to the buys[] array that we declared earlier:
function addBuy(uint Amt, uint BuyPrice) public returns (uint) 
{
ERC20Base.transferFrom(msg.sender, address(this),Amt.mul(BuyPrice));
Buys.push(Order(Amt,BuyPrice,now,msg.sender,'A'));
emit BuyAdded(Buys.length,Amt,BuyPrice,msg.sender);
return Buys.length;
}
  • The addBuy function takes the order amount (Amt) and price (BuyPrice) as input parameters.
  • On invoking the addBuy method, it first transfers the equivalent USD tokens for the order from the trader's Ethereum address to the smart contract's address. So, let's say if you wanted to buy 10 grams of gold for $50 per gram, addBuy would transfer -> 10 grams * $50 = $500 from your Ethereum account to the smart contract's address.
  • The smart contract's address here is represented by address(this).
  • addBuy invokes the transferFrom method in the Base asset ERC20 interface to transfer the USD tokens from the trader's account to the smart contract address. This method will only work successfully if the trader has approved the contract to move funds from its address.
  • After a successful transfer, it pushes a new buy order with the details to the buys array (Buys[]). Lastly, it emits the BuyAdded event, and returns the new length of the Buys array to the contract invoker.
  1.  The addSell method also works similar to the addBuy method:
function addSell(uint Amt, uint SellPrice) public returns (uint) 
{
ERC20Counter.transferFrom(msg.sender, address(this),Amt);
Sells.push(Order(Amt,SellPrice,now,msg.sender,'A'));
emit SellAdded(Sells.length,Amt,SellPrice,msg.sender);
return Sells.length;
}
  • It takes the order amount (Amt) and price (SellPrice) as input parameters. It uses the ERC20Counter contract instance that points to the Gold token to transfer gold tokens from the trader's address to the contract address.
  • After the successful transfer, a new sell order is added to the orderbook sell array (Sells[]).
  • Lastly, it emits the SellAdded event with the order details. It then returns the new length of the Sells array to the contract invoker.
  1. The viewLengthBuy() and viewLengthSell() methods return the current length of the Buys and Sells arrays, respectively, to the contract invoker:
function viewLengthBuy() public view returns (uint) 
{
return Buys.length;
}

function viewLengthSell() public view returns (uint)
{
return Sells.length;
}
  1. The viewBuy method returns an already recorded buy order to the contract invoker:
function viewBuy(uint OrderNo) public view returns (uint,uint,uint, address) 
{
return (
Buys[OrderNo-1].Amount,
Buys[OrderNo-1].Price,
Buys[OrderNo-1].TimeStamp,
Buys[OrderNo-1].Trader
);
}

It takes OrderNo as an input parameter. It returns the order Amount, Price, Timestamp, and the address of the Trader.

  1. The viewSell method is similar to the viewBuy method:
function viewSell(uint OrderNo) public view returns (uint,uint,uint,address) 
{
return (
Sells[OrderNo-1].Amount,
Sells[OrderNo-1].Price,
Sells[OrderNo-1].TimeStamp,
Sells[OrderNo-1].Trader
);
}

It takes OrderNo as an input parameter and returns the sell order details.

  1. Next, we come to the trade function. This method is invoked whenever you need to trade against an existing order:
function trade(uint OrderNo, uint Amt, uint TradePrice, uint trade_type) public returns ( uint, uint , address) 
{
// 1 is Buy trade , 2 is Sell Trade

Let's look at this method in detail:

  • The trade function takes OrderNo to be traded against, and trading amount (Amount), trading price (TradePrice), and trade_type as input parameters. trade_type can have a value of 1 for buy trades, and 2 for sell trades.
  • When an order is sent to the trade method, it checks whether the order consumes the matching order fully or partially. It also checks if the incoming request is for a buy order or a sell order. Accordingly, it handles the trading request.
  1. Thus, if the incoming request is for a Buy trade and the trade amount is equal to the order amount, the snippet of code does the following:
if (trade_type == 1 && Sells[OrderNo-1].Amount == Amt)
{
require(TradePrice >= Sells[OrderNo-1].Price, "Invalid Price");
ERC20Base.transferFrom(msg.sender, Sells[OrderNo-1].Trader,Amt.mul(Sells[OrderNo-1].Price));
Sells[OrderNo-1].Amount = 0;
Sells[OrderNo-1].Status = 'T';
ERC20Counter.transfer(msg.sender, Amt);
emit TradeAdd(OrderNo, Amt, Sells[OrderNo-1].Price,Sells[OrderNo-1].Trader,msg.sender);
return (
OrderNo,
Amt,
msg.sender
); }
  1. The method starts by first checking that the trading price (buy price) is greater than or equal to the sell order that it is matching against. This is enforced using a require statement, and in the case of a mismatch, a revert() statement is thrown with the message "Invalid Price":
require(TradePrice >= Sells[OrderNo-1].Price, "Invalid Price");
  1. Next, it uses the ERC20Base contract instance to transfer the equivalent US dollars from the taker's Ethereum address to the maker's Ethereum address. The taker here is the trader who submits the trading offer to the trade method. The maker is the trader who had placed the order that the method is matching against. The trade method assumes that the traders have already approved the smart contract address to move USD tokens from their Ethereum address:
ERC20Base.transferFrom(msg.sender, Sells[OrderNo-1].Trader,Amt.mul(Sells[OrderNo-1].Price));
  1. Next, the trade method updates the Sell order. The amount is set to 0, and the status to 'T' for traded:
Sells[OrderNo-1].Amount = 0;
Sells[OrderNo-1].Status = 'T';
  1. After updating the matching order, the counter asset, which is gold in our case, is transferred to the trader who invoked the trade method. This completes the exchange of the gold and US dollar assets between the two buyers:
 ERC20Counter.transfer(msg.sender, Amt);
emit TradeAdd(OrderNo, Amt, Sells[OrderNo-1].Price,Sells[OrderNo-1].Trader,msg.sender);

Lastly, an event is emitted with the trade details.

  1. Finally, the method returns the order number that is traded against (OrderNo), the trading amount (Amt), and the trader's address to the contract invoker:
return ( 
OrderNo,
Amt,
msg.sender
);
  1. The alternative buy case when the trading amount is less than the order amount is similar to this case. The difference is that the status of the order of the trade is still kept as A for available, and the order amount is updated to show the amount available in the order after a successful trade:
else if (trade_type == 1 && Sells[OrderNo-1].Amount > Amt)
{
ERC20Base.transferFrom(msg.sender, Sells[OrderNo-1].Trader,Amt.mul(Sells[OrderNo-1].Price));
require(TradePrice >= Sells[OrderNo-1].Price, "Invalid Price");
Sells[OrderNo-1].Amount = Sells[OrderNo-1].Amount - Amt;
Sells[OrderNo-1].Status = 'A';
ERC20Counter.transfer(msg.sender, Amt);
emit TradeAdd(OrderNo, Amt, Sells[OrderNo-1].Price,Sells[OrderNo-1].Trader,msg.sender);
return (
OrderNo,
Amt,
msg.sender
);
}

Both the sell cases are similar to buy. The only difference is that the trade_type input parameter should be set to 2, and the trading price in the sell case should be less than or equal to the buy order that it is matching against. Putting it all together, this how the trading method looks.

  1. In the case where none of the conditions are met, a revert statement is thrown by the trade method with the message, "Invalid trade parameters"
  2. One last method that we are left with in our orderbook contract is the decommission method. The decommission method is invoked by the contract owner to decommission the orderbook. It returns the assets held against the orders that are waiting in the orderbook to the traders:
function decommission() public onlyOwner
{
uint i = 0;
while ( i <= Buys.length || i <= Sells.length)
{
if( i <= Buys.length)
{
uint Amt = Buys[i].Amount;
Amt = Amt.mul(Buys[i].Price);
ERC20Base.transfer(Buys[i].Trader,Amt);
delete Buys[i];
}

if( i <= Sells.length)
{
ERC20Counter.transfer(Sells[i].Trader,Sells[i].Amount);
delete Sells[i];
}
i++;
}

This method uses the onlyOwner modifier to ensure that only the contract owner can invoke it. It iterates against the buys[] and sells[] arrays, and transfers the equivalent base or counter amount back to the trader who placed the order. After a successful transfer, it deletes the buy or sell request from the orderbook.

Great, so that was our Orderbook smart contract. Let's compile and deploy this contract, along with the contracts that we compiled previously.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.68.14