Project - Building a DEX

Hi @dan-i @thecil ,

I tried to make a deposit to of “LINK” to my Wallet (as described in the video).
This is my Wallet.sol code

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../node_modules/@openzeppelin/contracts/utils/math/SafeMath.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";

contract Wallet is Ownable {
    using SafeMath for uint256;

    struct Token {
        bytes32 ticker;
        address tokenAddress;

    mapping(bytes32 => Token) public tokenMapping;

    bytes32[] public tokenList;

    mapping(address => mapping(bytes32 => uint256)) public balances;

    modifier tokenExist(bytes32 ticker) {
        require(tokenMapping[ticker].tokenAddress != address(0), "Token does not exist");

    function addToken(bytes32 ticker, address tokenAddress) external onlyOwner {
        tokenMapping[ticker] = Token(ticker, tokenAddress);

    function deposit(uint amount, bytes32 ticker) external tokenExist(ticker) {
        IERC20(tokenMapping[ticker].tokenAddress).transferFrom(tokenMapping[ticker].tokenAddress, address(this), amount);
        balances[msg.sender][ticker] = balances[msg.sender][ticker].add(amount);

    function withdraw(uint amount, bytes32 ticker) external tokenExist(ticker) {
        require(balances[msg.sender][ticker] >= amount, "Balance not sufficient");
        balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);
        IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount);


And this is my “LINK” Token.sol code

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Link is ERC20 {

    constructor() ERC20("ChainLink", "LINK") public {
        _mint(msg.sender, 1000);


All previous steps were successful but when I tried to call deposit function, it throws some error:

truffle(develop)> wallet.deposit(100, web3.utils.fromUtf8("LINK"))
Error: Returned error: VM Exception while processing transaction: revert ERC20: transfer amount exceeds balance -- Reason given: ERC20: transfer amount exceeds balance.
    at evalmachine.<anonymous>:0:8
    at sigintHandlersWrap (vm.js:273:12)
    at Script.runInContext (vm.js:140:14)
    at runScript (/home/kresna/.nvm/versions/node/v14.15.5/lib/node_modules/truffle/build/webpack:/packages/core/lib/console.js:270:1)
    at Console.interpret (/home/kresna/.nvm/versions/node/v14.15.5/lib/node_modules/truffle/build/webpack:/packages/core/lib/console.js:285:1)
    at bound (domain.js:413:15)
    at REPLServer.runBound [as eval] (domain.js:424:12)
    at REPLServer.onLine (repl.js:817:10)
    at REPLServer.emit (events.js:315:20)
    at REPLServer.EventEmitter.emit (domain.js:467:12)
    at REPLServer.Interface._onLine (readline.js:337:10)
    at REPLServer.Interface._line (readline.js:666:8)
    at REPLServer.Interface._ttyWrite (readline.js:1010:14)
    at REPLServer.self._ttyWrite (repl.js:907:9)
    at ReadStream.onkeypress (readline.js:213:10)
    at ReadStream.emit (events.js:315:20)
    at ReadStream.EventEmitter.emit (domain.js:467:12)
    at emitKeys (internal/readline/utils.js:345:14)
    at (<anonymous>)
    at ReadStream.onData (readline.js:1144:36) {
  data: {
    '0x129c458a669afe0c56350c6daadb34bea0b097c44a5e82f582e6612425b7cb73': {
      error: 'revert',
      program_counter: 2340,
      return: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002645524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e63650000000000000000000000000000000000000000000000000000',
      reason: 'ERC20: transfer amount exceeds balance'
    stack: 'RuntimeError: VM Exception while processing transaction: revert ERC20: transfer amount exceeds balance\n' +
      '    at Function.RuntimeError.fromResults (/home/kresna/.nvm/versions/node/v14.15.5/lib/node_modules/truffle/build/webpack:/node_modules/ganache-core/lib/utils/runtimeerror.js:94:1)\n' +
      '    at BlockchainDouble.processBlock (/home/kresna/.nvm/versions/node/v14.15.5/lib/node_modules/truffle/build/webpack:/node_modules/ganache-core/lib/blockchain_double.js:627:1)\n' +
      '    at runMicrotasks (<anonymous>)\n' +
      '    at processTicksAndRejections (internal/process/task_queues.js:93:5)',
    name: 'RuntimeError'
  reason: 'ERC20: transfer amount exceeds balance',
  hijackedStack: 'Error: Returned error: VM Exception while processing transaction: revert ERC20: transfer amount exceeds balance -- Reason given: ERC20: transfer amount exceeds balance.\n' 

PS: I’ve checked Link balances and it was 1000. I also call approval function from Link to make allowance spending 500 token.

What might goes wrong?

Thank you for the help

Thanks Danni, I had tried to subtract everything instead of adding so I guess the double declaration was not letting it execute correctly…thanks again for the detailed explenation

You are missing an await.

Carlos Z

1 Like


Hey @dan-i @thecil Ive justed watched the first 2 videos of the DEX section. Im taing my time changing things and trying do figure out other approaches and im having a blast. But i would like a little clarity on the IERC20 interface contract.

So i know that that in a nutshell IERC20 defines function signatures if you will, without specifying any behavouir. only the function statment and what it returns. Also that ERC20 inerits from this interface. So we can think of it like a template i suppose you could say. However what i want to know is this

I will use the deposit function as the example to explain my question. So in the deposit function we call this line

IERC20(tokenMapping[ticker].tokenAddress).transferFrom(msg.sender, address(this), amount);

Which calls the transferFrom function in the IERC20 contract. However what i need clarity on is how does this call to transferFrom implement the allowance requirement so that when we migrate an test that we cannot deposit tokens without first granting an allowance to the tokens address for depositing. Because if we take a look at the IERC20 contract as i said there is no body to any of the functions in the IERC20 contract.


So how is their a link made between the transferFrom and having to have an allowance to call it. (Stupidly enough) My first thoughts were that perhaps somehow there was a link to ERC20 and that is where the link between the allowance requirment and our wallet deposit function is made for depositing tokens but that cant be correct beacuse theres no link between IERRC20 and ERC20.

Could it be perhaps that since the deposit function is external that the link is made. So we are not calling the deposit function from within the contract in which it is defined so im thinking maybe that its something to do with our token address or token contract address that calls the deposit function and since that inherits from ERC20 that thats how the link is made? I could be completley wrong and thats why im posting there i just wanted some clarity on this.

Because obvioulsy calling the func without the IERC20…

IERC20(tokenMapping[ticker].tokenAddress).transferFrom(msg.sender, address(this), amount);

like so


allows a straight up deposit without the need for an spending allowance as i expected.

1 Like

Hey @mcgrane5, hope you are well.

The IERC20, is just an interface on the functions that you can invoke over another ERC20 contract.

Although does not contain any logic, just the function header, when the contract is call it through the IERC20 along with the contract address, it will be triggered on the ERC20 contract.

So the allowance is need it in order to the parent contract (Dex in this example) being able to spent your tokens in your behalf.

Remember that your address own some LINKs, but those are in the balance mapping of the LINK contract, so when you want to transfer LINK to the DEX to be deposited, it will trigger the transferFrom function from the LINK contract.

Now if we take a look on the transferFrom function from the LINK contract, it triggers 2 internal functions:

     * @dev See {IERC20-transferFrom}.
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     * Requirements:
     * - `sender` and `recipient` cannot be the zero address.
     * - `sender` must have a balance of at least `amount`.
     * - the caller must have allowance for ``sender``'s tokens of at least
     * `amount`.
    function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
        _transfer(sender, recipient, amount);
        _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance"));
        return true;

So basically, if the _allowances[sender][_msgSender()] is not enough to substract the amount to transfer, it will trigger "ERC20: transfer amount exceeds allowance" error message.

To avoid that, you should approve the DEX contract to spent your LINK before you transfer them from the LINK contract to the DEX.

Hope I explain my self, if not, let me know :nerd_face:

Carlos Z

1 Like

Hi @Keith2

Here a detailed explanation for you: FAQ - How do change truffle version

Keep me posted,

Hey @kresna_sucandra

Seems like you have not called the ‘approve’ function necessary before calling ‘transferFrom’ (I cannot see this call in your code).
Can you please share the whole project so that I can double check?


Hi @dan-i

The approve() function is called inside Truffle develop console as Filip video shows. I think I’ve followed all Filip instruction steps.

Thank you

1 Like

Hey @thecil.

My mann. Thank you so much that a great explanation. Yeah it makes so much senses now with the fact that the token address and balances are a map and that way when we want to transfer it will call transferFrom from our token contract and thus the allowance is required since it inerits from ERC20 and using IERC20 in our wallet contract actually allows us to call .transferFrom which otherwise would be unavailalbe (lol there sure is a lot of levels of inheritance going on under the hood). To hit home on the fact i stopped inheriting ERC20 in my Token contract and just wrote my own mint/transfer functions directly in the contract and without the approvved allowance requirment while keeping the

 IERC20(tokenMapping[ticker].tokenAddress).transferFrom(msg.sender, address(this), amount);

line of code in my wallet contract and as i thought, after migration the allowance was not required (just to proove the point to myself as confirmation). So thank you Carlos i now see exactly where the connection between the IERC20 interface, our wallet transferFrom call and the token contract are being made. Cheers for clearing that up. Not a big issue i just wanted some clarity.

I think after this DEX project i may go back and try to write it myself from scratch using as little of openzeppelin as i can (Not that this is a good idea) but just to get used to what goes into standard contacts like these and being able to write them on the fly. I also feel that over reliance on open zeppelin could be a double edged sword and hinder your learning if you try to be as minimal as you can by overusing it. Whats your thoughts on this?


1 Like

Hello everyone. Just a simple bit of advice here that may save someone some time. When making the folder where you have your test files. Make sure its called ‘test’ and not anything else. I had mine called ‘tests’ and for awhile i was just getting an output like this


With no error message or anything. It finally hit me that im writing truffle test and my folder is called tests so i changed it from ‘tests’ to ‘test’ and everything is fine. So if anyone had the same problem be sure to only call the folder ‘test’.


Thanks @dan-i -

I’ll have a look, much appreciated

1 Like

Hi All,

I have just started with the project and have some very basic questions, regarding the way Filip has coded the withdraw function.

function withdraw(uint amount, bytes32 ticker){

        require(balances[msg.sender][ticker] >= amount, "Balance is not sufficient");
        require(tokenMapping[ticker].tokenaddress != address(0));
        balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);
        IERC20(tokenMapping[ticker].tokenaddress).transfer(msg.sender, amount);


Question 1: Why the inheritance statement is not given at the start, which says contract Wallet is IERC20. Why the 'is IERC20* is missing ?

Question 2: IERC20 is an interface and hence no constructor is defined. So, why IERC20 constructor is being initiated within the Withdraw function.

Sorry to ask these two questions, but I have not seen such an implementation in the previous lessons. Can somebody help on this ?

From my personal opinion, is just good practice to take what you need from openzepellin, using the ERC20 for example saves you a lot of time, but if you need extra functionalities, you have to add it yourself to your ERC20 contract.

I do remember that Filip explain it on a video, where you should take the functionalities of the ERC20pausable into your contract, its a good example on how to use different components from openzepellin.

Openzepellin is one of the best ways to program secure smart contracts with the same standards from others (all erc20 should be the same for example, no matter if you code it yourself or use openzepellin, you must fulfill the same requirements for ERC20 standard)

Carlos Z

1 Like

hey @thecil yeah i see it suppose the most important thing is that your contracts are secure and conform to industry standards. thank you for the great advice.


Hey @kresna_sucandra

The issue might be strictly related to the way you are calling the approve function :slight_smile:
Can you please share that part too?


1 Like

hey @kresna_sucandra when i was doing testing i also ran into a similar error for a while. Check out your link.approve( ) line. It should take the wallet address as the argument and not the token address so should be

link.approve(**wallet.addess**, 100000)

and not

link.approve(**link.addess,** 100000)

for a while i had the former and i kept getting that same error as you did. Your situation could be completley different but check it out anyway since your error is very similar to the one i had.

1 Like

Hi @rays

Importing using is:
is means that your contract inherits from another contract.
contract Test is ERC20 means that your Test contract inherits from the the ERC20 one.
Some docs:

Question 1: Why the inheritance statement is not given at the start, which says contract Wallet is IERC20. Why the 'is IERC20* is missing ?

In this case, you don’t need the full implementation of a contract (as in the previous example above where you were inheriting all functions and variables of erc20).
You just need your contract to know that somewhere there is a contract that has x , y , z methods.
In this case you have to import but not inherit.

Question 2: IERC20 is an interface and hence no constructor is defined. So, why IERC20 constructor is being initiated within the Withdraw function.

That is not a constructor and is strictly related with my statement above.

You just need your contract to know that somewhere there is a contract that has x , y , z methods.
In this case you have to import but not inherit.

You can read this:

 IERC20(tokenMapping[ticker].tokenaddress).transfer(msg.sender, amount);


Somewhere in the blockchain there is a contract that has x,y,z functions (the functions specified in the interface).
The contract address is tokenMapping[ticker].tokenaddress and the function I want to call in that contract is .transfer()

Let me know if you have any question,

Hi Dani,

Thanks a lot for the detailed explanation. Just one thing still bothers me. IERC20 is not a contract, but an interface. Can we still treat it as a Contract, operating at some address ?


Hi @rays

Can we still treat it as a Contract, operating at some address ?

This is the definition of interface :slight_smile:
You declare interfaces to say that ‘somewhere in the blockchain there is a contract that have the functions declared in the interface’.

In this way you will be able to call the functions stated in your interface by just providing the contract address.


Hi Dani,

Understood. Thanks a lot !


1 Like