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.
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)
After commands executed
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:
Localhost 8545
http://localhost:8545
31337
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.
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.
GLD
Gold
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.
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
.
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.
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.
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.
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
.
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;
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:
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.
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.
address(this)
function call:
get the address of this contract
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.
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:
onlyOwner
midifier:
Indicates that this function can only be called by the contract owner. This modifier is inherited from the
Ownable.sol
contract.
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 usepayable()
to convert instead ofaddress()
as in the previous sentence.
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.
It will need about 0.001 ETH.
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:
Withdraw ETH (Now the account has 0.0089 ETH)
Withdraw successfully (Now the account has 0.0098 ETH)
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:
approve
function with Vendor's contract address and Token amount, means user allows Vendor to sell this amount of Token.sellTokens
function based on the approved amount, and get back ETH to user's address.approve
functionThis 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);
}
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!