Transfers and approval of ERC-20 tokens from a solidity smart contract
In the previous tutorial we studied the anatomy of an ERC-20 token in Solidity on the Ethereum blockchain. In this article we’ll see how we can use a smart contract to interact with a token using the Solidity language.
For this smart contract, we’ll create a really dummy decentralized exchange where a user can trade ether for our newly deployed ERC-20 token.
For this tutorial we’ll use the code we wrote in the previous tutorial as a base. Our DEX will instantiate an instance of the contract in its constructor and perform the operations of:
- exchanging tokens to ether
- exchanging ether to tokens
We’ll start our Decentralized exchange code by adding our simple ERC20 codebase:
1pragma solidity ^0.8.0;23interface IERC20 {45 function totalSupply() external view returns (uint256);6 function balanceOf(address account) external view returns (uint256);7 function allowance(address owner, address spender) external view returns (uint256);89 function transfer(address recipient, uint256 amount) external returns (bool);10 function approve(address spender, uint256 amount) external returns (bool);11 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);121314 event Transfer(address indexed from, address indexed to, uint256 value);15 event Approval(address indexed owner, address indexed spender, uint256 value);16}171819contract ERC20Basic is IERC20 {2021 string public constant name = "ERC20Basic";22 string public constant symbol = "ERC";23 uint8 public constant decimals = 18;242526 mapping(address => uint256) balances;2728 mapping(address => mapping (address => uint256)) allowed;2930 uint256 totalSupply_ = 10 ether;313233 constructor() {34 balances[msg.sender] = totalSupply_;35 }3637 function totalSupply() public override view returns (uint256) {38 return totalSupply_;39 }4041 function balanceOf(address tokenOwner) public override view returns (uint256) {42 return balances[tokenOwner];43 }4445 function transfer(address receiver, uint256 numTokens) public override returns (bool) {46 require(numTokens <= balances[msg.sender]);47 balances[msg.sender] = balances[msg.sender]-numTokens;48 balances[receiver] = balances[receiver]+numTokens;49 emit Transfer(msg.sender, receiver, numTokens);50 return true;51 }5253 function approve(address delegate, uint256 numTokens) public override returns (bool) {54 allowed[msg.sender][delegate] = numTokens;55 emit Approval(msg.sender, delegate, numTokens);56 return true;57 }5859 function allowance(address owner, address delegate) public override view returns (uint) {60 return allowed[owner][delegate];61 }6263 function transferFrom(address owner, address buyer, uint256 numTokens) public override returns (bool) {64 require(numTokens <= balances[owner]);65 require(numTokens <= allowed[owner][msg.sender]);6667 balances[owner] = balances[owner]-numTokens;68 allowed[owner][msg.sender] = allowed[owner][msg.sender]-numTokens;69 balances[buyer] = balances[buyer]+numTokens;70 emit Transfer(owner, buyer, numTokens);71 return true;72 }73}7475Show allCopy
Our new DEX smart contract will deploy the ERC-20 and get all the supplied:
1contract DEX {23 IERC20 public token;45 event Bought(uint256 amount);6 event Sold(uint256 amount);78 constructor() {9 token = new ERC20Basic();10 }1112 function buy() payable public {13 // TODO14 }1516 function sell(uint256 amount) public {17 // TODO18 }1920}Show allCopy
So we now have our DEX and it has all the token reserve available. The contract has two functions:
buy
: The user can send ether and get tokens in exchangesell
: The user can decide to send tokens to get ether back
The buy function
Let’s code the buy function. We’ll first need to check the amount of ether the message contains and verify that the contracts own enough tokens and that the message has some ether in it. If the contract owns enough tokens it’ll send the number of tokens to the user and emit the Bought
event.
Note that if we call the require function in the case of an error the ether sent will directly be reverted and given back to the user.
To keep things simple, we just exchange 1 token for 1 Wei.
1function buy() payable public {2 uint256 amountTobuy = msg.value;3 uint256 dexBalance = token.balanceOf(address(this));4 require(amountTobuy > 0, "You need to send some ether");5 require(amountTobuy <= dexBalance, "Not enough tokens in the reserve");6 token.transfer(msg.sender, amountTobuy);7 emit Bought(amountTobuy);8}Copy
In the case where the buy is successful we should see two events in the transaction: The token Transfer
and the Bought
event.
The sell function
The function responsible for the sell will first require the user to have approved the amount by calling the approve function beforehand. Approving the transfer requires the ERC20Basic token instantiated by the DEX to be called by the user. This can be achieved by first calling the DEX contract's token()
function to retrieve the address where DEX deployed the ERC20Basic contract called token
. Then we create an instance of that contract in our session and call its approve
function. Then we are able to call the DEX's sell
function and swap our tokens back for ether. For example, this is how this looks in an interactive brownie session:
1#### Python in interactive brownie console...23# deploy the DEX4dex = DEX.deploy({'from':account1})56# call the buy function to swap ether for token7# 1e18 is 1 ether denominated in wei8dex.buy({'from': account2, 1e18})910# get the deployment address for the ERC20 token11# that was deployed during DEX contract creation12# dex.token() returns the deployed address for token13token = ERC20Basic.at(dex.token())1415# call the token's approve function16# approve the dex address as spender17# and how many of your tokens it is allowed to spend18token.approve(dex.address, 3e18, {'from':account2})19Show allCopy
Then when the sell function is called, we’ll check if the transfer from the caller address to the contract address was successful and then send the Ethers back to the caller address.
1function sell(uint256 amount) public {2 require(amount > 0, "You need to sell at least some tokens");3 uint256 allowance = token.allowance(msg.sender, address(this));4 require(allowance >= amount, "Check the token allowance");5 token.transferFrom(msg.sender, address(this), amount);6 payable(msg.sender).transfer(amount);7 emit Sold(amount);8}Copy
If everything works you should see 2 events (a Transfer
and Sold
) in the transaction and your token balance and ether balance updated.
From this tutorial we saw how to check the balance and allowance of an ERC-20 token and also how to call Transfer
and TransferFrom
of an ERC20 smart contract using the interface.
Once you make a transaction we have a JavaScript tutorial to wait and get details about the transactions(opens in a new tab) that were made to your contract and a tutorial to decode events generated by token transfers or any other events(opens in a new tab) as long as you have the ABI.
Here is the complete code for the tutorial:
1pragma solidity ^0.8.0;23interface IERC20 {45 function totalSupply() external view returns (uint256);6 function balanceOf(address account) external view returns (uint256);7 function allowance(address owner, address spender) external view returns (uint256);89 function transfer(address recipient, uint256 amount) external returns (bool);10 function approve(address spender, uint256 amount) external returns (bool);11 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);121314 event Transfer(address indexed from, address indexed to, uint256 value);15 event Approval(address indexed owner, address indexed spender, uint256 value);16}171819contract ERC20Basic is IERC20 {2021 string public constant name = "ERC20Basic";22 string public constant symbol = "ERC";23 uint8 public constant decimals = 18;242526 mapping(address => uint256) balances;2728 mapping(address => mapping (address => uint256)) allowed;2930 uint256 totalSupply_ = 10 ether;313233 constructor() {34 balances[msg.sender] = totalSupply_;35 }3637 function totalSupply() public override view returns (uint256) {38 return totalSupply_;39 }4041 function balanceOf(address tokenOwner) public override view returns (uint256) {42 return balances[tokenOwner];43 }4445 function transfer(address receiver, uint256 numTokens) public override returns (bool) {46 require(numTokens <= balances[msg.sender]);47 balances[msg.sender] = balances[msg.sender]-numTokens;48 balances[receiver] = balances[receiver]+numTokens;49 emit Transfer(msg.sender, receiver, numTokens);50 return true;51 }5253 function approve(address delegate, uint256 numTokens) public override returns (bool) {54 allowed[msg.sender][delegate] = numTokens;55 emit Approval(msg.sender, delegate, numTokens);56 return true;57 }5859 function allowance(address owner, address delegate) public override view returns (uint) {60 return allowed[owner][delegate];61 }6263 function transferFrom(address owner, address buyer, uint256 numTokens) public override returns (bool) {64 require(numTokens <= balances[owner]);65 require(numTokens <= allowed[owner][msg.sender]);6667 balances[owner] = balances[owner]-numTokens;68 allowed[owner][msg.sender] = allowed[owner][msg.sender]-numTokens;69 balances[buyer] = balances[buyer]+numTokens;70 emit Transfer(owner, buyer, numTokens);71 return true;72 }73}747576contract DEX {7778 event Bought(uint256 amount);79 event Sold(uint256 amount);808182 IERC20 public token;8384 constructor() {85 token = new ERC20Basic();86 }8788 function buy() payable public {89 uint256 amountTobuy = msg.value;90 uint256 dexBalance = token.balanceOf(address(this));91 require(amountTobuy > 0, "You need to send some ether");92 require(amountTobuy <= dexBalance, "Not enough tokens in the reserve");93 token.transfer(msg.sender, amountTobuy);94 emit Bought(amountTobuy);95 }9697 function sell(uint256 amount) public {98 require(amount > 0, "You need to sell at least some tokens");99 uint256 allowance = token.allowance(msg.sender, address(this));100 require(allowance >= amount, "Check the token allowance");101 token.transferFrom(msg.sender, address(this), amount);102 payable(msg.sender).transfer(amount);103 emit Sold(amount);104 }105106}Show allCopy
Last edit: @nhsz(opens in a new tab), August 15, 2023