Token Vendor | Web3.0 dApp Dev 0x08

Authors: ken, msfew, Snowyaya

Token Vendor is a vending machine that's combined of scaffold-eth and BuidlGuidl. This tutorial will walk you through how the project is implemented. To do that, we split the entire project to five smaller parts and it's verrifiable whether each part functions successfully after it's implemented.

0x01 Installation and Local Configuration

Step1. Open the terminal window and clone scaffold-eth base code

git clone https://github.com/scaffold-eth/scaffold-eth-typescript-challenges.git challenge-2-token-vendor

cd challenge-2-token-vendor

git checkout challenge-2-token-vendor

Step2. Install dependencies

yarn install

Step3. Prepare the enviroment
We need three terminal windows for this step, and each will execute their own command consecutively.
The three commands will be used to:

yarn chain (start the `hardhat` backend and local nodes)

yarn deploy (compile, deploy the smart contract and push to the front-ended project reference)

yarn start (React front-ended App)
Terminal
Terminal

After commands executed

Command Completed
Command Completed
App UI
App UI

0x02 Prepare MetaMask Account

Generate Account
Generate Account
View Account
View Account

Change the DEBUG statement in packages/hardhat-ts/hardhat.config.ts to const DEBUG = true;;
When viewing the account information, the private key of the wallet will aso be displayed, which can be imported to MetaMask.

Configure MetaMask:
If you do not have MetaMask local network, configure by doing so:

  • network name: Localhost 8545
  • new RPC URL: http://localhost:8545
  • chain ID: 31337
  • currency symbol: ETH

Remember to check the value of chain ID. It should be 1337 normally, but for hardhat, it is 31337. If it's not changed, you might end up having this issue, which couldn't send transactions.

0x03 Launch your own token

3.1 Token smart contract

The Token in Ethereum is actually smart contract. The location of the contract file we need to customize is: packages/hardhat-ts/contracts/YourToken.sol.

pragma solidity >=0.8.0 <0.9.0;
// SPDX-License-Identifier: MIT

// inherited from OpenZeppelin's ERC20 Token standard
import '@openzeppelin/contracts/token/ERC20/ERC20.sol';

// learn more: https://docs.openzeppelin.com/contracts/3.x/erc20

contract YourToken is ERC20 {
  // ToDo: add constructor and mint tokens for deployer,
  //       you can use the above import for ERC20.sol. Read the docs ^^^

  // constructor
  constructor() public ERC20('Gold', 'GLD') {
    // _mint() 1000 * 10 ** 18 to msg.sender
    _mint(msg.sender, 1000 * 10**18);
  }
}

By inheriting the ERC20 standard (is ERC20), this Token has basic functions such as basic transfer, querying the balance of the Token held by the account. We only need to name the Token and specify its initial total amount to use.

  • token symbol: GLD
  • token name: Gold
  • initial amount: 1000 * 10**18 (1000 Token)

10**18 means 10 to the 18th degree, where there are 18 zeroes. Why use such a large number, 1000000000000000000, to represent a Token? The reason is the same as we use penny in calculating dollar. The language EVM and Solidity can only handle integers, so in order to facilitate the Token to be cut into small units for circulation, we need a smaller unit, and the inherited ERC20 defines the length of this decimal place to be 18.

3.2 Deploy and transfer Token

After the contract is written, change the statement in packages/hardhat-ts/deploy/00_deploy_your_token.ts, fill in the prepared account, and test whether the transfer can be done successfully:

  // Todo: transfer tokens to frontend address
  const result = await yourToken.transfer(
    "0xC0802222dB0F31d63dc85d9C8CAa00485715A13c", ethers.utils.parseEther("1000"));

Note that if we want to transfer 1000 Token, we do not directly pass 1000 to the transfer function, but need to go through ethers.utils.parseEther to convert it into the number that is handleable by the contract.

After modification, deploy by command yarn deploy --reset.

Deploy Token
Deploy Token

If deployed successfully, call the balanceOf function through the Debug page in the browser to check whether the address after the transfer has the corresponding number of Tokens.

Account Balance
Account Balance

You can also try to transfer tokens from the current account to another account. However, before that, the wallet of the current account needs to have few ETH.

Press the button Grab funds from the faucet on the current page to get some.

Grab ETH
Grab ETH

Another way to get more ETH is through the faucet.paradigm.xyz page to authorize Twitter to log in and fill in the address to request.

Through the transfer function on the Debug page, we can transfer Token. The amount to be filled in needs to be converted. In the meantime, you can also click the Send button and modify it again in the confirmation box that pops up in MetaMask.

Note:
Due to the contract changes or the time constraints, you may need to deploy multiple times and cannot complete the test at one time. Changes to the local network will cause the number of transactions in the account to be different from the number of transactions on MetaMask. When initiating a transaction, MetaMask is likely to alert you to the errors like Nonce too high. Expected nonce to be 0 but got x.. If so, you need to prepare a new account, or delete the account from MetaMask and try importing it again.

Transfer Another
Transfer Another
Confirm Transfer
Confirm Transfer

0x04 Build a vending machine Vendor

Now we start to implement the smart contract of the vending machine Vendor. It's framework is in the file packages/hardhat-ts/contracts/Vendor.sol.

4.1 Define the Token price

Token trading requires first determining the exchange rate between Token and ETH.

In the contract, we can define how many tokens an ETH can buy through the constant tokensPerEth. Remember that 1 Token means 1 * 10**18. Since the free ETH that the test account can get may be very few, this number may be set larger.

uint256 public constant tokensPerEth = 10000;

4.2 buyTokens function

The logic of buying Token is very simple. It is to calculate how many Tokens the sender of the transaction can get from the vending machine according to the amount of ETH in the transaction. The complete contract after implementing the buyTokens function is as follows:

pragma solidity >=0.8.0 <0.9.0;
// SPDX-License-Identifier: MIT

import '@openzeppelin/contracts/access/Ownable.sol';
import './YourToken.sol';

contract Vendor is Ownable {
  YourToken yourToken;

  uint256 public constant tokensPerEth = 10000;

  event BuyTokens(address buyer, uint256 amountOfEth, uint256 amountOfTokens);

  constructor(address tokenAddress) public {
    yourToken = YourToken(tokenAddress);
  }

  // ToDo: create a payable buyTokens() function:
  function buyTokens() public payable returns (uint256 tokenAmount) {
    require(msg.value > 0, 'ETH used to buy token must be greater than 0');

    uint256 tokenToBuy = msg.value * tokensPerEth;

    // check if the Vendor Contract has enough amount of tokens for the transaction
    uint256 vendorBalance = yourToken.balanceOf(address(this));
    require(vendorBalance >= tokenToBuy, 'Vendor has not enough tokens to sell');

    // Transfer token to the msg.sender
    bool success = yourToken.transfer(msg.sender, tokenToBuy);
    require(success, 'Failed to transfer token to user');

    emit BuyTokens(msg.sender, msg.value, tokenToBuy);

    return tokenToBuy;
  }
}

Key parts of the contract:

  1. payable modifier:

    indicates that this function can receive ETH. It is necessary to mark this function as payable because buying Token requires transferring ETH to Vendor.

  2. the statement require(msg.value > 0, 'ETH used to buy token must be greater than 0');

    constraint checking. Obviously, to buy Token, the amount of incoming ETH must be greater than 0. The value of msg.value is the amount of ETH.

  3. address(this) function call:

    get the address of this contract

  4. emit BuyTokens(msg.sender, msg.value, tokenToBuy)

    trigger the BuyTokens event to record the address, cost, and purchase quantity of the purchased Token. Evens are available as EVM logging.

4.3 withdraw function

After the vending machine Vendor sells the Token, the buyer's ETH slowly accumulates into the Vendor account. So how do you get the ETH out of the contract? At this point we need to implement the withdraw function:

// ToDo: create a withdraw() function that lets the owner withdraw ETH
function withdraw() public onlyOwner {
  uint256 ethToWithdraw = address(this).balance;
  require(ethToWithdraw > 0, 'No ETH to withdraw');

  payable(msg.sender).transfer(ethToWithdraw);
}

Key parts of the contract:

  1. onlyOwner midifier:

    Indicates that this function can only be called by the contract owner. This modifier is inherited from the Ownable.sol contract.

  2. statement payable(msg.sender).transfer(ethToWithdraw):

    When calling the transfer function, the payable address must be used, not the ordinary address. So, you need to use payable() to convert instead of address() as in the previous sentence.

4.4 Deploy Vendor contract

withdraw function is ready. Who are allowed to withdraw funds from the contract? Of course the owner of the vendor, which is the owner of the vendor contract. The initial owner is the address that deploys the contract. If the address you connected with MetaMask is not the address for deployment, then you need to transfer the ownership to the address you use to log in the App.

Uncomment packages/hardhat-ts/deploy/01_deploy_vendor.ts's commented lines, then modify the Token amount transferring from YourToken to Vendor. If needed, assign new owner to Vendor contract.

// Todo: deploy the vendor

await deploy('Vendor', {
  // Learn more about args here: https://www.npmjs.com/package/hardhat-deploy#deploymentsdeploy
  from: deployer,
  args: [yourToken.address],
  log: true,
});

const vendor = await ethers.getContract("Vendor", deployer);

// Todo: transfer the tokens to the vendor
console.log("\n 🏵  Sending all 1000 tokens to the vendor...\n");

await yourToken.transfer(
  vendor.address,
  ethers.utils.parseEther("500") // Specify the amount of Token transferred to the Vendor.
);

// Assign new owner of Vendor contract
await vendor.transferOwnership("0xC0802222dB0F31d63dc85d9C8CAa00485715A13c");

Note: You also need to uncomment the lines that transfers Token to your address in packages/hardhat-ts/deploy/00_deploy_your_token.ts or make the value smaller, like modifying it into 500.

After the change, run yarn deploy --reset to deploy the contract again.

Below is the output for successful deployment:

Vendor will initially have 500 tokens, and I am going to buy 10 tokens.

Vendor Balance
Vendor Balance

It will need about 0.001 ETH.

Buy Token
Buy Token

After the purchase, Vendor will have 490 tokens left, and ETH balanced is increased by 0.001. We can also see the event for Buy Token:

New Balance
New Balance

Withdraw ETH (Now the account has 0.0089 ETH)

Withdraw
Withdraw

Withdraw successfully (Now the account has 0.0098 ETH)

Withdraw Success
Withdraw Success

0x05 Vendor buy back

Sometimes, we need to sell our Token and get ETH back. It will be great if Vendor can have this kind of operation.

Suppose here is the buy back steps:

  1. Token owner takes out some of the Token, call YourToken's approve function with Vendor's contract address and Token amount, means user allows Vendor to sell this amount of Token.
  2. Then, user can call Vendor contract's sellTokens function based on the approved amount, and get back ETH to user's address.

5.1 YourToken's approve function

This funciton does not need to be implemented in YourToken's contract since it is inherited from ERC20.sol. (Codes below do not need to be copied to YourToken contract)

  • _approve is the main part of the logic. When user calls the method, spender will be the Vendor contract's address.

  • 核心部分 _allowances[owner][spender] = amount; 负责在 YourToken 合约地址里面记录下用户允许 Vendor 获取的 Token 数量。

  • The core is _allowances[owner][spender] = amount; that records in the YourToken's address the amount of users' Token amount that allows Vendor to get.

     function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }
    
    function _approve(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");
    
        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }
    

5.2 Vendor's sellTokens function:

Copy the SoldTokens event and sellTokens function into Vendor.sol:

```solidity
// ToDo: create a sellTokens() function:
event SoldTokens(uint256 amountOfEth, uint256 amountOfTokens);

function sellTokens(uint256 tokenToSell) public {
  require(tokenToSell > 0, 'You need to sell at least some tokens');

  // Calculate the needed ETH amount
  uint256 ethSold = tokenToSell / tokensPerEth;
  require(address(this).balance > ethSold, 'Not enough ETH to buy from Vendor');

  // Transfer Token from user to Vendor contract
  yourToken.transferFrom(msg.sender, address(this), tokenToSell);

  payable(msg.sender).transfer(ethSold);

  emit SoldTokens(ethSold, tokenToSell);
}

With the previous experience about `buyTokens`, we can easily understand `sellToken`. But the function of `yourToken.transferFrom` called within it needs to be looked at. `transferFrom`'s implementation in `ERC20.sol` is like that (you do not need to copy them into YourToken.sol or Vendor.sol):

function transferFrom(
    address from,
    address to,
    uint256 amount
) public virtual override returns (bool) {
    address spender = _msgSender();
    _spendAllowance(from, spender, amount);
    _transfer(from, to, amount);
    return true;
}

// check whether owner (Vendor contract) can get `msg.sender`'s needed amount of Token.
function _spendAllowance(
    address owner,
    address spender,
    uint256 amount
) internal virtual {
    uint256 currentAllowance = allowance(owner, spender);
    if (currentAllowance != type(uint256).max) {
        require(currentAllowance >= amount, "ERC20: insufficient allowance");
        unchecked {
            _approve(owner, spender, currentAllowance - amount);
        }
    }
}

function _transfer(
    address from,
    address to,
    uint256 amount
) internal virtual {
    require(from != address(0), "ERC20: transfer from the zero address");
    require(to != address(0), "ERC20: transfer to the zero address");

    _beforeTokenTransfer(from, to, amount);

    uint256 fromBalance = _balances[from];
    require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
    unchecked {
        _balances[from] = fromBalance - amount;
    }
    _balances[to] += amount;

    emit Transfer(from, to, amount);

    _afterTokenTransfer(from, to, amount);
}

Some key parts:

1. `internal` functions can only be called inside the contract or the child.
2. `virtual` functions means the method can be overwritten by the child.
3. ERC20 contract is to use a `_balances` mapping variable to store the amount of Token a address has. Transfer Token from one address to another, is to modify the value of the field in `_balances`.

   Let's see what sellTokens page works.

   Approve Vendor can sell this amount of Token:

   Sell Token to Vendor:

   Successfully with ETH (Now we have 0.0098 ETH)

## 0x06 Deploy contract to test networks

Now, `YourToken` and `Vendor`'s contracts are all done, and tested in the local test network. Now we can deploy the contract to the public testnet or mainnet.

File `packages/hardhat-ts/hardhat.config.ts`

```typescript
// const defaultNetwork = 'localhost';
const defaultNetwork = 'ropsten';

File packages/vite-app-ts/src/config/providersConfig.ts

// export const targetNetworkInfo: TNetworkInfo = NETWORKS.local;
export const targetNetworkInfo: TNetworkInfo = NETWORKS.ropsten;

After modifying the previous files, you can deploy the contract to Ropsten with yarn deploy (you need to have ETH on Ropsten network). The following error may be threwed when deploying:

deploying "Vendor"replacement fee too low (error={"name":"ProviderError","code":-32000,"_isProviderError":true}

This means the deployment operation is too fast, so that the transaction is sent too early and blocked. To solve this, you can seperate two deploy config files under packages/hardhat-ts/deploy into different folders.

Finally, run yarn build and yarn surge to build the front-end page, and deploy to Surge static file. Now we are all set!

thinkingincrowd
thinkingincrowd
msfew
msfew
Subscribe to Web3dAppDevCamp
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.