Develop a Marketplace Contract with Token Payment
Introduction
In my previous article (link) I have shown the use of dapps.tools in Ethereum contract development, and see how to use dapp and seth to perform unit testing of contract code with a testset, and deploy my contract code in a local testnet.
In this article I first create another contract simulating a marketplace. And deployment I will add a token as the payment for items in the marketplace. The token I am using is from ds-token package, which is a mintable ERC20 token. Finally we will demonstrate the execution of contract functions and see how my contract interacts with the token contract.
Overview of the Application
The application is a smart contract simulating a marketplace operation. It keeps items listed by someone (seller) at a price tag, and another (buyer) can buy the item by paying that price to the seller. It also comes with some checking functions to see if that item exists or is already sold. Meanwhile the buyer can remove the item from the marketplace provided that the item is not sold to anyone yet.
In phase 1 we will complete these functions. In phase 2, we will implement an ERC20 token as the (only) payment method in this marketplace.
This application itself by no means a complete one for production, and there are still many to do for tuning and optimization before it is a full function. Nevertheless, this fulfils my purpose to show how to use dapp.tools to build something interesting and add a token using ds-token package.
Phase 1: Marketplace without Token Payment
Create a Workspace for this contract. We name the workspace as emarket (directory name). With dapp.tools we will have a predefined contract name Emarket.sol and the associated testing contract Emarket.t.sol.
We will look into the two contract files. Then begin to test and deploy the contract on testnet. Finally we will simulate some activities over this deployed contract.
Contract: Emarket.sol
Here is the contract code I am using in this demonstration.
The data structure Item keeps the detail of each item posted in the emarket. It contains a description, the seller and the buyer, price, and a boolean whether this item is sold or not. Note that the price is just a number at this stage, and later it will be an amount of token after we implement the token in phase 2.
The items is the mapping from an index to the data structure Item. We simply maintain a counter itemCount for counting index.
No constructor is needed in this contract. Six functions are defined in this contract.
addItem(description, price) is called when a seller lists an item in the emarket. What we need are just a description and the price of that item. This will be recorded as a new item indexed with itemCount+1.
getItem(index) is called when anyone wishes to show detail about an item. We will get back the description and price.
checkedItemExisting(index) is called when anyone wishes to know whether an item exists or not.
checkedItemSold(index) is called when anyone wishes to know whether an item has already been sold.
removedItem(index) can only be called by the seller who lists the item before.
buyItem(index) is called by a buyer. Buyer can only buy item if it is not sold yet. Currently the logic simply updates the buyer address and marks the sold flag.
Test Contract: Emarket.t.sol
We have defined four test cases for my emarket contract. They are quite self-explanatory so here we do not provide the detail.
Setup Workspace
We will have setup our workspace emarket as shown the previous article.
mkdir emarket
cd emarketdapp init
After we paste our contract Emarket.sol and the test contract Emarket.t.sol, we can perform the unit test using dapp test
.
And from the result we know all test cases are passed.
Run Testnet and Deploy the Contract
As before, we open another terminal to run a testnet, using dapp testnet
. To get two more addresses for demonstration, we can can specify option -- account
.
We will create the environment variables for demonstration purpose. The first one we use ETH_FROM such that it is the coinbase account. If we do not specify anything it is the default account.
export ETH_KEYSTORE=~/.dapp/testnet/8545/keystore
export ETH_GAS=2000000
export ETH_FROM=0x60c5d2e21151275b752627d000428431dde3db07
export ALICE=0x56c01aa43dbf31100b7335d9c247adb55d2eeceb
export BOB=0x31ba7173a223db36b9d51220ea046a99ea8ae8d4
And we deploy the contract using dapp create
.
Now we have the contract address 0x7e2d…d237. As good practice we use an environment variable to keep this address.
export EMARKET=0x7e2daacd18aeda6f8220e542137bff36a40ed237
Interacting with the Contract
In this demo we simulate a real life example. This involves the following steps.
- Check currently no items are in the marketplace yet.
- Alice lists an item “a pen” and marks the price 100. Check that the item is in marketplace.
- Bob buys this item. Check that this item is sold and Bob’s address is correctly recorded.
- Alice tries to remove this item but fails, as this item is already sold.
Step 1: First let’s see the current itemCount.
seth call $EMARKET “itemCount()”
Step 2: Alice lists an item.
seth send $EMARKET “addItem(string memory, uint)” $(seth —-from-ascii “a pen”) $(seth —-to-uint256 100) --from=$ALICE
Then take a look on the itemCount and item #1 detail.
seth call $EMARKET “itemCount()”
seth call $EMARKET “items(uint)(string memory,address,address,uint,bool)” $(seth —-to-uint256 1)
We can see there is one item, and we see the description (“a pen” in ascii), seller Alice, and this item not sold yet.
Step 3: Now Bob buys this item (item #1).
seth send $EMARKET “buyItem(uint)” $(seth —-to-uint256 1) — from=$BOB
And after that we check item #1 again.
seth call $EMARKET “items(uint)(string memory,address,address,uint,bool)” $(seth —-to-uint256 1)
Now we see the buyer address (Bob’s) recorded, and this item is marked as true (sold).
Step 4: Alice tries to remove this item. She cannot as this item was sold already.
seth send $EMARKET “removeItem(uint)” $(seth —-to-uint256 1) —-from=$ALICE
Phase 2: Marketplace with Token as Payment Methods
Create a New Workspace
With the basic functions of marketplace ready, we will now include an ERC20 token as the pricing and payment method for this marketplace.
To keep things neat, here we create another workspace, namely emarketwithcoin
mkdir emarketwithcoin
cd emarketwithcoindapp init
We are using ds-token
package provided in dapp.tools
. ds-token
is a package of standard ERC20 implementation, plus some additional functions like mint() and burn(). There are some advanced features working with other packages but the basic ERC20 and the mint/burn are good enough for our demonstration.
We will first install the package.
dapp install ds-token
All the required packages required are installed. The package is installed under lib/ds-token.
If we inspect the token contracts in lib/ds-token/src/base.sol and lib/ds-token/src/token.sol, we probably see an implementation of ERC20 tokens, and some additional functions. Meanwhile each comes with a unit test contract, and the test are running when the contract is deployed.
Modify Our Contract Code
For simplicity, we only focus on the contract code, and put aside the testing contract code (we see how it works in early sessions). It is not the best practice as we should always use testing contract for unit testing. Nevertheless, we assume our code are good for demonstration.
Here is the contract code for Emarketwithcoin.
Here I just highlight the additional portion on top of the previous example.
1. Import the contract from the ds-token
package. Here we import token.sol.
import “ds-token/token.sol”;
If we take a look on the code, token.sol further imports base.sol. And token.sol provides the class DSToken that we will use later. Note the default DSToken comes without a fixed supply and with decimal precision 18. For demonstration purpose we will keep this without considering the actual precision.
2. Specify where the token contract is.
ERC20 public emark;constructor (address _emark) public {
emark = ERC20(_emark);
}
Here we define an object emark, which holds the deployed token contract. As a result, in our deployment we first deploy a token contract, and the contract ID of the deployed contract is passed to this contract through constructor. We will see how it works later.
3. Transfer tokens when buy an item.
function buyItem(uint _index) public {
Item storage i = items[_index];
require(i.seller != address(0), “no such item”); // not exists
require(!i.sold, “item sold already”);
require(i.price <= emark.balanceOf(msg.sender), “not enough tokens”);
i.buyer = msg.sender;
i.sold = true;
emark.transferFrom(msg.sender, i.seller, i.price);
}
We add two logics here. First we will check buyer’s balance when he buys a listed item. If balance is not enough the transaction fails. Also we will use transferFrom to transfer tokens from someone who buys this item to the seller of this item. Since it is the Emarketwithcoin contract performs the transferFrom, the buyer needs to approve the Emarketwithcoin contract for the transfer. We will see how it works later.
4. Update the test contract (inside Emarketwithcoin.t.sol).
function setUp() public {
emarketwithcoin = new Emarketwithcoin(address(0x123));
}
As we have included constructor and an address is required, we need to modify the Emarketwithcoin.t.sol with a pseudo address for getting through the test. Again we are not defining any test cases and therefore the test address is not meaningful in this case.
Deploy Contracts in Testnet
As before, we open another terminal to run a testnet, using dapp testnet
. We also needs two more addresses for demonstration.
We will create the environment variables for demonstration purpose. The first one we use ETH_FROM such that it is the coinbase account. If we do not specify anything it is the default account.
export ETH_KEYSTORE=~/.dapp/testnet/8545/keystore
export ETH_GAS=3000000
export ETH_FROM=0x631911a584ae91af67efce2adb3d20b5e29b3be5
export ALICE=0x2963cb08f690a072b718addbc6478641305f5e69
export BOB=0x486e85148d38402bd72a30fea6dcbc7a5cb5f3d8
Now we deploy the token contract first. The symbol we pass to the new contract is emark.
dapp create DSToken $(seth —-to-bytes32 $(seth —-from-ascii emark))
Now we have the token contract ID (or contract address). We will use an environment variable EMK to hold this address.
export EMK=0xce13f923964091acb55c4c8fcf5b6f2a3261dda6
With this, we can deploy the Emarketwithcoin contract. We specify the token contract ID as required in constructor.
dapp create eMarketwithcoin $EMK
Now we have another contract ID. We will hold this in environment variable MARKET.
export MARKET=0xda99912a9c7a370cd24621f0147f4b13e1d0f830
Now we have both contracts deployed.
Interacting with the Contracts
Our demonstration flow on these deployed contract is simple.
- Check emark balance of both Alice and Bob, and see no balance at the beginning.
- Mint Bob 100 emarks. It will be used for buying an item listed by Alice.
- Alice lists an item with price set to 80 emarks.
- Bob buys this item.
- Check emark balance of both Alice and Bob, and we see 80 emarks transferred to Alice.
Always remember we have two contracts: EMK for token, and MARKET for marketplace. They are two independent contracts, and the only linkage is that MARKET contract will call EMK when someone buys items.
Step 1: We begin with token contract. Check balance of total EMK supply, and balances of Alice and Bob.
seth call $EMK “totalSupply()”
seth call $EMK “balanceOf(address)” $ALICE
seth call $EMK “balanceOf(address)” $BOB
Initially all are zero. No emark exists yet.
Step 2: We mint 100 emarks to Bob.
seth send $EMK “mint(address,uint)” $BOB $(seth —-to-uint256 100)
And check the balance. Bob has 100 emarks (0x64) and total emark supply is also 100.
Before MARKET can transfer tokens from Bob’s account, Bob needs to approve MARKET that amount. Assuming Bob approves 80 emarks.
seth send $EMK “approve(address,uint)” $MARKET $(seth —-to-uint256 80) —-from=$BOB
Step 3: We move to the marketplace. Alice lists an item “a pen” and the price is 80 emarks.
seth send $MARKET “addItem(string memory, uint)” $(seth —-from-ascii “a pen”) $(seth —-to-uint256 80) —-from=$ALICE
And we will see the item #1 recorded.
seth call $MARKET “items(uint)(string memory,address,address,uint,bool)” $(seth —-to-uint256 1)
Item is listed by Alice and now sold yet.
Step 4: Bob is buying this item.
seth send $MARKET “buyItem(uint)” $(seth —-to-uint256 1) —-from=$BOB
And we check the item #1 again.
seth call $MARKET “items(uint)(string memory,address,address,uint,bool)” $(seth —-to-uint256 1)
The item is sold to Bob.
Step 5: Finally we check the total EMK supply and balance of both Alice and Bob again.
We see 80 emarks is transferred from Bob to Alice as Bob has bought Alice’s item with 80 emarks. And the total supply remains 100 emarks.
Closing
We have shown a sample marketplace contract (again, not optimized one and not for production). We saw how to use test contract to implement some unit tests on the functions. After deploying on the testnet, we demonstrate how to execute the functions defined in the marketplace. Then we use the same marketplace contract and add token as the pricing and payment method. Using ds-token package and with a few modifications, the marketplace contract can perform the token transfer when a buying is made.
Hope you can see the use of dapp.tools and the convenience when developing contract on Ethereum platform. There are some other packages from dapp.tools and you will find more fun from them.