Build And Deploy A Multi Token wallet On The Celo Blockchain

Build And Deploy A Multi Token wallet On The Celo Blockchain https://celo.academy/uploads/default/optimized/2X/5/5477175e1069fd76b8ea277d63a3270ec8687c28_2_1024x576.jpeg
none 4.0 1

Introduction

In this tutorial, we will be creating a Decentralized Multitoken wallet smart contract that allows users to deposit and withdraw multiple ERC20 standard tokens in a single wallet. The contract is designed to support any ERC20 token that implements the IERC20 interface. The contract will also have functionality to set the maximum idle time for an account, as well as the ability to add or remove supported tokens.

Project Repository

Prerequisites

To follow this tutorial, you will need the following:

  • Basic knowledge of Solidity programming language.
  • A Development Environment Like Remix.
  • The celo Extension Wallet.

SmartContract

Let’s begin writing our smart contract in Remix IDE

The completed code Should look like this.

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

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);

    function transfer(address to, uint256 value) external returns (bool);

    function transferFrom(
        address from,
        address to,
        uint256 value
    ) external returns (bool);
}

contract MultiTokenWallet {
    uint256 private _maxIdleTime = 30 minutes;

    address private contractOwner;

    constructor() {
        contractOwner = msg.sender;
    }

    mapping(address => mapping(address => uint256)) private _balances;
    mapping(address => bool) private _supportedTokens;
    mapping(address => uint256) private _lastSeen;

    event Deposit(
        address indexed account,
        address indexed token,
        uint256 amount
    );
    event Withdrawal(
        address indexed account,
        address indexed token,
        uint256 amount
    );

    function deposit(address token, uint256 amount) external {
        require(_supportedTokens[token], "Unsupported token");
        require(
            IERC20(token).transferFrom(msg.sender, address(this), amount),
            "Token transfer failed"
        );
        _balances[msg.sender][token] += amount;
        _lastSeen[msg.sender] = block.timestamp;
        emit Deposit(msg.sender, token, amount);
    }

    function withdraw(address token, uint256 amount) external {
        require(_balances[msg.sender][token] >= amount, "Insufficient balance");
        require(
            IERC20(token).transfer(msg.sender, amount),
            "Token transfer failed"
        );
        _balances[msg.sender][token] -= amount;
        _lastSeen[msg.sender] = block.timestamp;
        emit Withdrawal(msg.sender, token, amount);
    }

    function balanceOf(
        address account,
        address token
    ) external view returns (uint256) {
        return _balances[account][token];
    }

    function setSupportedToken(address token, bool isSupported) external {
        require(
            msg.sender == owner(),
            "Only owner can modify supported tokens"
        );
        _supportedTokens[token] = isSupported;
    }

    function setMaxIdleTime(uint256 idleTime) external {
        require(msg.sender == owner(), "Only owner can modify max idle time");
        _maxIdleTime = idleTime;
    }

    function owner() public view returns (address) {
        return (contractOwner);
    }
}

Setting up

First, we declared our license and the solidity version.

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

The next part of the code defines an interface called IERC20, which is a standard interface for ERC20 tokens. It defines three functions: balanceOf(), transfer(), and transferFrom().

And then we defined our smart contract MultiTokenWallet.

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

contract MultiTokenWallet {
    // Contract code goes here
}

Variables and mappings

We will now define some variables and mappings that will be used by our contract.

    uint256 private _maxIdleTime = 30 minutes;

    address private contractOwner;

    constructor(){
        contractOwner = msg.sender;
    }

    mapping(address => mapping(address => uint256)) private _balances;
    mapping(address => bool) private _supportedTokens;
    mapping(address => uint256) private _lastSeen;

The _maxIdleTime variable determines the amount of time that an account can remain idle before its funds can be forfeited. The contractOwner variable is used to store the address of the owner of the contract, which is set to the address of the account that deployed the contract.

The _balances mapping stores the balances of each account for each supported token. The _supportedTokens mapping is used to keep track of which tokens are supported by the wallet. And the _lastSeen mapping keeps track of the last time that an account interacted with the wallet.

Events

We will now define two events that will be emitted by our contract whenever a deposit or withdrawal is made.

    event Deposit(address indexed account, address indexed token, uint256 amount);
    event Withdrawal(address indexed account, address indexed token, uint256 amount);

The Deposit event is emitted whenever a deposit is made to the wallet, and includes the account that made the deposit, the token that was deposited, and the amount that was deposited. The Withdrawal event is emitted whenever a withdrawal is made from the wallet, and includes the account that made the withdrawal, the token that was withdrawn, and the amount that was withdrawn.

Deposit function

We will now define the deposit() function, which will allow users to deposit tokens into the wallet.

     function deposit(address token, uint256 amount) external {
        require(_supportedTokens[token], "Unsupported token");
        require(IERC20(token).transferFrom(msg.sender, address(this), amount), "Token transfer failed");
        _balances[msg.sender][token] += amount;
        _lastSeen[msg.sender] = block.timestamp;
        emit Deposit(msg.sender, token, amount);
    }

The function takes two arguments: token, which is the address of the token being deposited, and amount, which is the amount of the token being deposited. The function first checks that the token being deposited is supported and then it transfers the token from the sender to the smart contract. It then update the account balance and the last seen of the sender address.

And finally it emits a Deposit event with the defined outputs

Withdraw function

Now let’s take a look at the withdraw() function. This function allows a user to withdraw a certain amount of tokens from their balance in the contract. Similar to the deposit function, the user must specify which token they wish to withdraw, and the amount they wish to withdraw.

function withdraw(address token, uint256 amount) external {
    require(_balances[msg.sender][token] >= amount, "Insufficient balance");
    require(IERC20(token).transfer(msg.sender, amount), "Token transfer failed");
    _balances[msg.sender][token] -= amount;
    _lastSeen[msg.sender] = block.timestamp;
    emit Withdrawal(msg.sender, token, amount);
}

The function first checks that the user has a sufficient balance of the specified token in the contract. If the user does not have enough balance, the function will revert and the transaction will not be executed. If the user has enough balance, the function will transfer the specified amount of tokens from the contract to the user’s address using the transfer function from the IERC20 interface. The user’s balance in the contract is then updated accordingly and the _lastSeen mapping is updated with the current timestamp to track the last time the user interacted with the contract. Finally, the function emits a Withdrawal event to notify external parties of the transaction.

BalanceOf function

Next, let’s look at the balanceOf function:

function balanceOf(address account, address token) external view returns (uint256) {
    return _balances[account][token];
}

This function simply returns the balance of the specified token for the specified account. It is a view function, meaning that it does not modify the state of the contract and does not require a transaction to be executed.

setSupportedToken function

Moving on, we have the setSupportedToken function:

  function setSupportedToken(address token, bool isSupported) external {
    require(msg.sender == owner(), "Only owner can modify supported tokens");
    _supportedTokens[token] = isSupported;
}

This function allows the contract owner to add or remove support for a particular token. The function takes in the token address and a boolean flag indicating whether the token should be supported or not. The function first checks that the caller of the function is the contract owner using the owner function, and reverts the transaction if this condition is not met. If the caller is the contract owner, the function updates the _supportedTokens mapping to reflect the new support status for the specified token.

The executeProposal() is a function that allows the proposer of a proposal to execute it. The function first checks that the proposer is the one calling the function, that the proposal has not been executed yet, and that the number of “yes” votes is greater than the number of “no” votes. If all of these conditions are met, the function sets the executed flag to true, indicating that the proposal has been executed. Finally, any actions described in the proposal can be performed.

setMaxIdleTime function

Lastly, we have the setMaxIdleTime() function:

 function setMaxIdleTime(uint256 idleTime) external {
    require(msg.sender == owner(), "Only owner can modify max idletime");
    _maxIdleTime = idleTime;
}

This function allows the contract owner to set the maximum amount of time that can elapse without a user interacting with the contract before their tokens are considered “idle”. The function takes in the idle time in seconds as an argument. The function first checks that the caller of the function is the contract owner using the owner function, and reverts the transaction if this condition is not met. If the caller is the contract owner, the function updates the _maxIdleTime variable to reflect the new maximum idle time.

Deployment

To deploy our smart contract successfully, we need the celo extention wallet which can be downloaded from here

Next, we need to fund our newly created wallet which can done using the celo alfojares faucet Here

You can now fund your wallet and deploy your contract using the celo plugin in remix.

Conclusion

That concludes our tutorial on the MultiTokenWallet contract! In summary, we’ve covered how to implement a simple multi-token wallet contract that allows users to deposit and withdraw various ERC20 tokens. The contract owner can add or remove support for different tokens, as well as set a maximum idle time.

Next Steps

I hope you learned a lot from this tutorial. Here are some relevant links that would aid your learning further.

About the author

I’m Jonathan Iheme, A full stack block-chain Developer from Nigeria.

Thank You!!

1 Like

Good explanation of the solidity code :clap:

2 Likes

One of the best tutorial that explore solidity here on this platform

2 Likes

Trying to also deploy it from end , a great tutorial tho

Insightful tutorial.

3 Likes

Wow, you’ve been a great contributor to the great community. Thanks for this thoroughly explain content (all the extra link you provided worked)!

2 Likes