Create Vault Smart Contract

Learn how to create, deploy, and interact with Vault Smart Contract on the Celo Ecosystem
Celo
AdvancedReactNodeJSSmart contractsTruffle1.5 hours
Written by Team Celo Helpi

Introduction

In this tutorial, we will create, deploy, and interact with our first Vault Smart Contract on the Celo Ecosystem.
What's a Vault Smart Contract, and the deployment, and interaction thing?
A Vault as defined in this tutorial proofs that your contributed funds are locked through inviolable smart contracts.
This tutorial has 3 parts as mentioned below -
  • First, we'll create a new account and configure essential things to connect to Alfajores.
  • Second, we will be deploying a Vault Smart Contract to the Alfajores Testnet using DataHub and Truffle.
  • Third, we will interface with the deployed Vault Smart Contract and demonstrate its functionalities using a frontend with React and Web3.

Prerequisites

Creating Project (Central Base)

Since Celo runs the Ethereum Virtual Machine, we can use Truffle to compile our smart contract. But just to do things right, let's first initialize a node project (silently...).
$ mkdir vault-dapp $ cd vault dapp $ npm init -y --silent
Adding our dependencies (for now):
$ npm install --save @openzeppelin/contracts truffle @celo/contractkit dotenv web3
Initialize a bare-bones truffle project, by running the following command.
$ npx truffle init
Set (datahub url + api-key, see Prerequisites for the api-key) to an environment variable. For this create a file called ./.env
DATAHUB_NODE_URL=https://celo-alfajores--rpc.datahub.figment.io/apikey/<YOUR API KEY>/
Make sure our ./.env does not get pushed by mistake (our key is inside). For this create a new file called ./.gitignore
node_modules .env
Lastly, initialize git.
$ git init
OK, I'd love to congratulate you, we just initialized a node and a truffle project. We also added git and secured our key like a pro. Your directory tree should look like this:
04/17/2021 05:03 AM <DIR> . 04/17/2021 05:03 AM <DIR> .. 04/17/2021 05:03 AM <DIR> contracts 04/17/2021 05:03 AM <DIR> migrations 04/17/2021 05:00 AM <DIR> node_modules 04/17/2021 05:00 AM 678,735 package-lock.json 04/17/2021 05:00 AM 395 package.json 04/17/2021 05:03 AM <DIR> test 10/26/1985 04:15 AM 4,598 truffle-config. 04/17/2021 05:00 AM 395 .env 3 File(s) 683,728 bytes 6 Dir(s) 231,304,970,240 bytes free

Get Account and Funds

Our next step is to get an Account (Address / PrivateKey) from the Celo Alfajores Network. We are going to use Truffle Console to get one real quick.
Copy and paste the following into the ./truffle-config.js that was generated by Truffle:
// LOAD ENV VAR require("dotenv").config(); // INIT PROVIDER USING CONTRACT KIT const Kit = require("@celo/contractkit"); const kit = Kit.newKit(process.env.DATAHUB_NODE_URL); // AWAIT WRAPPER FOR ASYNC FUNC async function awaitWrapper() { let account = kit.connection.addAccount(process.env.PRIVATE_KEY); // ADDING ACCOUNT HERE } awaitWrapper(); // TRUFFLE CONFIG OBJECT module.exports = { networks: { alfajores: { provider: kit.connection.web3.currentProvider, // CeloProvider network_id: 44787, // latest Alfajores network id }, }, // Configure your compilers compilers: { solc: { version: "0.8.3", // Fetch exact version from solc-bin (default: truffle's version) }, }, db: { enabled: false, }, };
Here we initialized a provider using our environment variable DATAHUB_NODE_URL.

Connect to Alfajores with the console

Now we can connect to Alfajores using truffle console . Run the following on your console.
$ npx truffle console --network alfajores
Once connected, initialize and print an account as follows:
let account = web3.eth.accounts.create(); console.log(account);
This is my output:
$ truffle console --network alfajores web3-shh package will be deprecated in version 1.3.5 and will no longer be supported. web3-bzz package will be deprecated in version 1.3.5 and will no longer be supported. truffle(alfajores)> let account = web3.eth.accounts.create() undefined truffle(alfajores)> console.log(account) { address: '0x7cdf6c19E5491EA23aB14132f8a76Ff1C74ccAFC', privateKey: '0x167ed276fb95a17de53c6b0fa4737fc2f590f3e6c5b9de0793d9bcdf63140650', signTransaction: [Function: signTransaction], sign: [Function: sign], encrypt: [Function: encrypt] }
you can exit from console with ctrl+C or ctrl+D.
From here we need the address, and privateKey as I mentioned.
Success! We got and account, before we forget let's save it into our .env as we will use it later. Your .env should look like this.
DATAHUB_NODE_URL=https://celo-alfajores--rpc.datahub.figment.io/apikey/<YOUR API KEY>/ ADDRESS=0x7cdf6c19E5491EA23aB14132f8a76Ff1C74ccAFC # your address PRIVATE_KEY=0x167ed276fb95a17de53c6b0fa4737fc2f590f3e6c5b9de0793d9bcdf63140650 # your private key

Funding the account

Let's add funds to our account using the Alfajores Faucet, go there with your address. (Do you also wish to have one but for the Mainnet): Go here -> Alfajores Faucet and you will get:
cGLD => 5 cUSD => 10 cEUR => 10
you can verify your balance in Alfajores Blockscout searching you address.
We are finally done with this section. We are getting closer by the minute.

Deploy the Vault smart contract

Now that we have an account and funds, let's add a smart contract to our Truffle project. From the console, run the following:
npx truffle create contract Vault
The above command will create a new Smart Contract in the following location:
ls -la vault-dapp/contracts # Listing directory: vault-dapp/contracts: Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 4/17/2021 6:12 AM 114 Vault.sol
The code for the Vault Smart Contract its been cooked for us already. Copy, paste, read:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract Vault is Ownable { using SafeMath for uint256; using SafeERC20 for IERC20; struct Items { IERC20 token; address withdrawer; uint256 amount; uint256 unlockTimestamp; bool withdrawn; bool deposited; } uint256 public depositsCount; mapping (address => uint256[]) public depositsByTokenAddress; mapping (address => uint256[]) public depositsByWithdrawers; mapping (uint256 => Items) public lockedToken; mapping (address => mapping(address => uint256)) public walletTokenBalance; address public helpiMarketingAddress; event Withdraw(address withdrawer, uint256 amount); constructor() { } function lockTokens(IERC20 _token, address _withdrawer, uint256 _amount, uint256 _unlockTimestamp) external returns (uint256 _id) { require(_amount > 500, 'Token amount too low!'); require(_unlockTimestamp < 10000000000, 'Unlock timestamp is not in seconds!'); require(_unlockTimestamp > block.timestamp, 'Unlock timestamp is not in the future!'); require(_token.allowance(msg.sender, address(this)) >= _amount, 'Approve tokens first!'); _token.safeTransferFrom(msg.sender, address(this), _amount); walletTokenBalance[address(_token)][msg.sender] = walletTokenBalance[address(_token)][msg.sender].add(_amount); _id = ++depositsCount; lockedToken[_id].token = _token; lockedToken[_id].withdrawer = _withdrawer; lockedToken[_id].amount = _amount; lockedToken[_id].unlockTimestamp = _unlockTimestamp; lockedToken[_id].withdrawn = false; lockedToken[_id].deposited = true; depositsByTokenAddress[address(_token)].push(_id); depositsByWithdrawers[_withdrawer].push(_id); return _id; } function withdrawTokens(uint256 _id) external { require(block.timestamp >= lockedToken[_id].unlockTimestamp, 'Tokens are still locked!'); require(msg.sender == lockedToken[_id].withdrawer, 'You are not the withdrawer!'); require(lockedToken[_id].deposited, 'Tokens are not yet deposited!'); require(!lockedToken[_id].withdrawn, 'Tokens are already withdrawn!'); lockedToken[_id].withdrawn = true; walletTokenBalance[address(lockedToken[_id].token)][msg.sender] = walletTokenBalance[address(lockedToken[_id].token)][msg.sender].sub(lockedToken[_id].amount); emit Withdraw(msg.sender, lockedToken[_id].amount); lockedToken[_id].token.safeTransfer(msg.sender, lockedToken[_id].amount); } function getDepositsByTokenAddress(address _id) view external returns (uint256[] memory) { return depositsByTokenAddress[_id]; } function getDepositsByWithdrawer(address _token, address _withdrawer) view external returns (uint256) { return walletTokenBalance[_token][_withdrawer]; } function getVaultsByWithdrawer(address _withdrawer) view external returns (uint256[] memory) { return depositsByWithdrawers[_withdrawer]; } function getVaultById(uint256 _id) view external returns (Items memory) { return lockedToken[_id]; } function getTokenTotalLockedBalance(address _token) view external returns (uint256) { return IERC20(_token).balanceOf(address(this)); } }
Let's go through the main sections of the our Vault Smart Contract:
Structure to store our deposits:
struct Items { IERC20 token; address withdrawer; uint256 amount; uint256 unlockTimestamp; bool withdrawn; bool deposited; }
lockTokens function accepts the following parameters when being invoked:
lockTokens(IERC20, address, amount, time)
and it will lock an amount of ERC20 tokens in the contract for an arbitrary time.
IERC20 _token, => An ERC20 Token. address _withdrawer => The Address which can withdraw (usually same as deposing address). uint256 _amount => Amount the ERC20 Token. uint256 _unlockTimestamp => When to unlock deposit.
The withdrawTokens function accepts an address and checks it's existence as a _withdrawer in our Structure. The function also checks if funds are past the _unlockTimestamp.
withdrawTokens(address)
address _withdrawer => The address which was registered in our contract when the deposit was made _withdrawer

Compiling the contract

We are now ready to compile our solidity code using Truffle. From your terminal run the following:
npx truffle compile
This is my output, all good if yours matches:
web3-shh package will be deprecated in version 1.3.5 and will no longer be supported. web3-bzz package will be deprecated in version 1.3.5 and will no longer be supported. Compiling your contracts... =========================== > Compiling @openzeppelin/contracts/access/Ownable.sol > Compiling @openzeppelin/contracts/token/ERC20/IERC20.sol > Compiling @openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol > Compiling @openzeppelin/contracts/utils/math/SafeMath.sol > Compiling @openzeppelin/contracts/token/ERC20/IERC20.sol > Compiling @openzeppelin/contracts/utils/Address.sol > Compiling @openzeppelin/contracts/utils/Context.sol > Compiling ./contracts/Migrations.sol > Compiling ./contracts/Vault.sol > Artifacts written to vault-dapp/build/contracts > Compiled successfully using: - solc: 0.8.3+commit.8d00100c.Emscripten.clang
Truffle automatically puts our Vault smart contract bytecode and ABI inside the following json file. Make sure you can see it!
$ ls -la vault-dapp/build/contracts # Directory Listing: vault-dapp/build/contracts Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 4/17/2021 7:12 AM 870362 Vault.json
I hope you are ready for deployment, because that's next!

Deploying contract

One last step, is necessary to make a new migration to deploy the Vault Contract, create the file migrations/2_vault_deployment.js to achieve this
const Vault = artifacts.require("Vault"); module.exports = function (deployer) { deployer.deploy(Vault); };
With this we are ready to run npx truffle migrate --network alfajores, this will hopefully deploy our Vault Smart Contract to the Celo Alfajores Network. In your console run.
npx truffle migrate --network alfajores
A successful deployment looks like this:
Starting migrations... ====================== > Network name: 'alfajores' > Network id: 44787 > Block gas limit: 0 (0x0) 1_initial_migration.js ====================== Deploying 'Migrations' ---------------------- > transaction hash: 0x9223481ec81ab8efe26c325a61bf87369fa451210f2be6a08237df769952af45 > Blocks: 0 Seconds: 0 > contract address: 0xC58c6144761DBBE7dd7633edA98a981cb73169Df > block number: 4661754 > block timestamp: 1618659322 > account: 0x7cdf6c19E5491EA23aB14132f8a76Ff1C74ccAFC > balance: 4.93890836 > gas used: 246292 (0x3c214) > gas price: 20 gwei > value sent: 0 ETH > total cost: 0.00492584 ETH > Saving migration to chain. > Saving artifacts ------------------------------------- > Total cost: 0.00492584 ETH 2_vault_deployment.js ===================== Deploying 'Vault' ----------------- > transaction hash: 0x8b112defb0ed43eee6009445f452269e18718094cbd949e6ff7f51ef078abd84 > Blocks: 0 Seconds: 0 > contract address: 0xB017aD96e31B43AFB670dAB020561dA8E2154C5B > block number: 4661756 > block timestamp: 1618659332 > account: 0x7cdf6c19E5491EA23aB14132f8a76Ff1C74ccAFC # save this address to next steps > balance: 4.89973486 > gas used: 1916762 (0x1d3f5a) > gas price: 20 gwei > value sent: 0 ETH > total cost: 0.03833524 ETH > Saving migration to chain. > Saving artifacts ------------------------------------- > Total cost: 0.03833524 ETH Summary ======= > Total deployments: 2 > Final cost: 0.04326108 ETH
Awesome!, our Vault Smart Contract is now in Alfajores, and we can deposit funds into it, lock them, and withdraw them. Let's build an interface in React for our Vault Smart Contract next.

Take a break!

In this section, we’ve learned how to create our first Celo Vault contract, compile and deploy it to Alfajores, also some of the language constructors of Solidity. Congratulations for making it this far! Have a break, stand up and stretch, get some water and then come back for the React finale!

Create the React dApp

Now that we have our vault contract, let's connect the methods with an interface.

Interface with the Vault Smart Contract

First, let's initialize our react app, we can do this within our node/truffle project directory we have been working on:
One important thing, ensure that you have the latest version of create-react-app installed. If you have it installed already and encounter any errors, see this article for help.
npx create-react-app my-vault-interface cd my-vault-interface
We need to add the following dependencies to our new react project:
npm install @celo/contractkit web3 dotenv
Let's also transfer our .env file over to this new project, with a quick fix:
REACT_APP_DATAHUB_NODE_URL=https://alfajores-forno.celo-testnet.org REACT_APP_ADDRESS=0x7cdf6c19E5491EA23aB14132f8a76Ff1C74ccAFC # your recently created account address REACT_APP_PRIVATE_KEY=0x167ed276fb95a17de53c6b0fa4737fc2f590f3e6c5b9de0793d9bcdf63140650 # your recently created account private key REACT_APP_VAULT_ADDRESS=0xB017aD96e31B43AFB670dAB020561dA8E2154C5B # the recently created address of the vault contract (see 2_vault_deployment.js deploy output above)
This is how mine looks like, as you can see we still have the same variables, juts added the REACT_APP prefix.
Next, let's transfer the json file where our contract bytecode and ABI resides (VAULT.json file), and paste inside a new contract folder into the root of our new React Project, see below:
Original Location (Truffle Compile) Directory: vault-dapp/build/contracts/Vault.json Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 4/17/2021 7:35 AM 870803 Vault.json React App Location Directory: vault-dapp/my-vault-interface/src/contract/Vault.json Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 4/17/2021 7:35 AM 870803 Vault.json
With this we complete the requirements of our frontend, to recap, this is how the React Project directory tree looks like:
Directory: vault-dapp/my-vault-interface Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 4/17/2021 9:03 AM node_modules d---- 4/17/2021 7:51 AM public d---- 4/17/2021 9:46 AM src -a--- 4/17/2021 9:59 AM 287 .env -a--- 4/17/2021 7:51 AM 310 .gitignore -a--- 4/17/2021 9:03 AM 766889 package-lock.json -a--- 4/17/2021 9:03 AM 903 package.json -a--- 4/17/2021 7:51 AM 3362 README.md -a--- 4/17/2021 7:51 AM 507434 yarn.lock
Directory: vault-dapp/my-vault-interface/src Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 4/17/2021 9:46 AM contract -a--- 4/17/2021 7:51 AM 564 App.css -a--- 4/17/2021 10:01 AM 1520 App.js -a--- 4/17/2021 7:51 AM 246 App.test.js -a--- 4/17/2021 7:51 AM 366 index.css -a--- 4/17/2021 7:51 AM 500 index.js -a--- 4/17/2021 7:51 AM 2632 logo.svg -a--- 4/17/2021 7:51 AM 362 reportWebVitals.js -a--- 4/17/2021 7:51 AM 241 setupTests.js
Directory: vault-dapp/my-vault-interface/src/contract Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 4/17/2021 7:35 AM 870803 Vault.json

Populate our main application

Our next step is to copy and paste some code to the App React Component App.js.
import React, { useState, useEffect } from "react"; import { newKit } from "@celo/contractkit"; import dotenv from "dotenv"; import Vault from "./contract/Vault.json"; // LOAD ENV VAR dotenv.config(); const kit = newKit(process.env.REACT_APP_DATAHUB_NODE_URL); const connectAccount = kit.addAccount(process.env.REACT_APP_PRIVATE_KEY); // CONTRACT INSTANCE const VaultO = new kit.web3.eth.Contract( Vault.abi, process.env.REACT_APP_VAULT_ADDRESS ); function App() { const [balances, setBalances] = useState({ CELO: 0, cUSD: 0, Vault: 0 }); const [info, setInfo] = useState(""); const [lockAmount, setLockAmount] = useState("0.3"); const [idVault, setIdVault] = useState("0"); const [listOfVaults, setListOfVaults] = useState([]); const update = () => { getBalanceHandle(); getLockerIdsInfo(); }; const getBalanceHandle = async () => { const goldtoken = await kit._web3Contracts.getGoldToken(); const totalLockedBalance = await VaultO.methods .getTokenTotalLockedBalance(goldtoken._address) .call(); const totalBalance = await kit.getTotalBalance( process.env.REACT_APP_ADDRESS ); const { CELO, cUSD } = totalBalance; setBalances({ CELO: kit.web3.utils.fromWei(CELO.toString()), cUSD: kit.web3.utils.fromWei(cUSD.toString()), Vault: kit.web3.utils.fromWei(totalLockedBalance.toString()), }); }; const approve = async () => { setInfo(""); // MAX ALLOWANCE const allowance = kit.web3.utils.toWei("1000000", "ether"); // GAS ESTIMATOR const gasEstimate = kit.gasEstimate; // ASSET TO ALLOW const goldtoken = await kit._web3Contracts.getGoldToken(); // TX OBJECT AND SEND try { const approveTxo = await goldtoken.methods.approve( process.env.REACT_APP_VAULT_ADDRESS, allowance ); const approveTx = await kit.sendTransactionObject(approveTxo, { from: process.env.REACT_APP_ADDRESS, gasPrice: gasEstimate, }); const receipt = await approveTx.waitReceipt(); // PRINT TX RESULT console.log(receipt); setInfo("Approved!!"); } catch (err) { console.log(err); setInfo(err.toString()); } }; const lock = async () => { setInfo(""); try { // TIMESTAMP const lastBlock = await kit.web3.eth.getBlockNumber(); let { timestamp } = await kit.web3.eth.getBlock(lastBlock); var timestampObj = new Date(timestamp * 1000); // TIME TO LOCK + 10 MINS var unlockTime = timestampObj.setMinutes(timestampObj.getMinutes() + 10) / 1000; // 10 minutes by default // AMOUNT TO LOCK const amount = kit.web3.utils.toWei(lockAmount + "", "ether"); // ERC20 TO LOCK const goldtoken = await kit._web3Contracts.getGoldToken(); // TX OBJECT AND SEND const txo = await VaultO.methods.lockTokens( goldtoken._address, process.env.REACT_APP_ADDRESS, amount, unlockTime ); const tx = await kit.sendTransactionObject(txo, { from: process.env.REACT_APP_ADDRESS, }); // PRINT TX RESULT const receipt = await tx.waitReceipt(); update(); setInfo("Celo locked!"); console.log(receipt); } catch (err) { console.log(err); setInfo(err.toString()); } }; const withdraw = async () => { setInfo(""); try { const txo = await VaultO.methods.withdrawTokens(idVault); const tx = await kit.sendTransactionObject(txo, { from: process.env.REACT_APP_ADDRESS, }); const receipt = await tx.waitReceipt(); update(); console.log(receipt); setInfo("Celo unlocked!"); } catch (err) { console.log(err); setInfo(err.toString()); } }; const getLockerIdsInfo = async () => { setInfo(""); try { const ids = await VaultO.methods .getVaultsByWithdrawer(process.env.REACT_APP_ADDRESS) .call(); let vaults = []; for (let id of ids) vaults.push([id, ...(await VaultO.methods.getVaultById(id).call())]); console.log("IDS:", vaults); setListOfVaults(vaults); } catch (err) { console.log(err); setInfo(err.toString()); } }; useEffect(update, []); return ( <div> <h1>ACTIONS:</h1> <button onClick={approve}>APPROVE</button> <button onClick={getBalanceHandle}>GET BALANCE</button> <button onClick={getLockerIdsInfo}>GET LOCKER IDS</button> <div style={{ display: "flex" }}> <div style={{ margin: "0.5rem" }}> <h1>Lock Celo Token:</h1> <input type="number" value={lockAmount} min="0" onChange={(e) => setLockAmount(e.target.value)} /> <button onClick={lock}>LOCK</button> </div> <div style={{ margin: "0.5rem" }}> <h1>Withdraw Celo Token:</h1> <input type="number" value={idVault} min="0" onChange={(e) => setIdVault(e.target.value)} /> <button onClick={withdraw}>WITHDRAW</button> </div> </div> <h1>DATA WALLET</h1> <ul> <li>CELO BALANCE IN ACCOUNT: {balances.CELO}</li> <li>cUSD BALANCE IN ACCOUNT: {balances.cUSD}</li> <li>TOTAL VALUE LOCKED IN CONTRACT: {balances.Vault}</li> </ul> <h1>INFO:</h1> <h2 style={{ color: "red" }}>{info}</h2> <h2>Your Vaults:</h2> <table> <thead> <th>ID</th> <th>Value</th> <th>Withdraw until</th> <th>Withdrawn</th> <th>deposited</th> </thead> <tbody> {listOfVaults.map((item) => ( <tr> <td>{item[0]}</td> <td>{kit.web3.utils.fromWei(item[3].toString())}</td> <td>{new Date(item[4] * 1000).toLocaleTimeString()}</td> <td>{item[5] ? "yes" : "no"}</td> <td>{item[6] ? "yes" : "no"}</td> </tr> ))} </tbody> </table> </div> ); } export default App;
Let's go over our App Component, much fun here I promise...
contractKit will help us interact with the Celo Blockchain effectively and efficiently using web3 under the hood. Environment variables are present to use constant info, see the use of dotenv. Finally we import our json representing the contract (recently I learned this json with bytecode, ABI its called contract/truffle artifact)
import React, { useState } from "react"; import { newKit } from "@celo/contractkit"; import dotenv from "dotenv"; import Vault from "./contract/Vault.json"; // LOAD ENV VAR dotenv.config();
Next, we initialize our instance of contractKit, using our DataHub Node URL, we also add to the kit our test account using its private key. Finally the Contract Object is instantiated for later use.
const kit = newKit(process.env.REACT_APP_DATAHUB_NODE_URL); const connectAccount = kit.addAccount(process.env.REACT_APP_PRIVATE_KEY); // CONTRACT INSTANCE const VaultO = new kit.web3.eth.Contract( Vault.abi, process.env.REACT_APP_VAULT_ADDRESS );
We will be using useState to save, modify and display our balances (wallet and vault contracts), the deposits, the ID of the vault for withdrawing and the list of contracts,
const [balances, setBalances] = useState({ CELO: 0, cUSD: 0, Vault: 0 }); const [info, setInfo] = useState(""); const [lockAmount, setLockAmount] = useState("0.3"); const [idVault, setIdVault] = useState("0"); const [listOfVaults, setListOfVaults] = useState([]);
Before we can interact with our new Vault Smart Contract, we need to approve the use of this smart contract by our wallet, setting a default allowance. The approve function creates and sends a Transaction Object indicating we are approving, though also setting a max allowance for this smart contract to use. After we console.log the receipt
const approve = async () => { setInfo(""); // MAX ALLOWANCE const allowance = kit.web3.utils.toWei("1000000", "ether"); // GAS ESTIMATOR const gasEstimate = kit.gasEstimate; // ASSET TO ALLOW const goldtoken = await kit._web3Contracts.getGoldToken(); // TX OBJECT AND SEND try { const approveTxo = await goldtoken.methods.approve( process.env.REACT_APP_VAULT_ADDRESS, allowance ); const approveTx = await kit.sendTransactionObject(approveTxo, { from: process.env.REACT_APP_ADDRESS, gasPrice: gasEstimate, }); const receipt = await approveTx.waitReceipt(); // PRINT TX RESULT console.log(receipt); setInfo("Approved!!"); } catch (err) { console.log(err); setInfo(err.toString()); } };
Let's take a look at the lock function which follows. Here, we get our unlock timestamp (10 minutes after transaction is sent), we estimate gas, specify we will lock 1 celo, instantiate our contract object using it's abi and address. Our transaction object (txo) will use the lockTokens method available in our contract object, and pass our collected / required parameters (Token address, Account address and of course the amount to lock and the timestamp that represents how many time it will be locked). Finally the transaction object will be included in a new transaction (tx).
We await our receipt after, and console.log it.
const lock = async () => { setInfo(""); try { // TIMESTAMP const lastBlock = await kit.web3.eth.getBlockNumber(); let { timestamp } = await kit.web3.eth.getBlock(lastBlock); var timestampObj = new Date(timestamp * 1000); // TIME TO LOCK + 10 MINS var unlockTime = timestampObj.setMinutes(timestampObj.getMinutes() + 10) / 1000; // 10 minutes by default // AMOUNT TO LOCK const amount = kit.web3.utils.toWei(lockAmount + "", "ether"); // ERC20 TO LOCK const goldtoken = await kit._web3Contracts.getGoldToken(); // TX OBJECT AND SEND const txo = await VaultO.methods.lockTokens( goldtoken._address, process.env.REACT_APP_ADDRESS, amount, unlockTime ); const tx = await kit.sendTransactionObject(txo, { from: process.env.REACT_APP_ADDRESS, }); // PRINT TX RESULT const receipt = await tx.waitReceipt(); update(); setInfo("Celo locked!"); console.log(receipt); } catch (err) { console.log(err); setInfo(err.toString()); } };
Our next stop is the withdraw function, it uses the withdrawTokens method in the contract, and needs the ID of the vault in where you want to withdraw, you can see those ids in the generated table
const withdraw = async () => { setInfo(""); try { const txo = await VaultO.methods.withdrawTokens(idVault); const tx = await kit.sendTransactionObject(txo, { from: process.env.REACT_APP_ADDRESS, }); const receipt = await tx.waitReceipt(); update(); console.log(receipt); setInfo("Celo unlocked!"); } catch (err) { console.log(err); setInfo(err.toString()); } };
getLockerIdsInfo gets the list of vaults/lockers that the current account has in the contract. It uses the getVaultsByWithdrawermethod of the contract returning an array of userfull info:
const getLockerIdsInfo = async () => { setInfo(""); try { const ids = await VaultO.methods .getVaultsByWithdrawer(process.env.REACT_APP_ADDRESS) .call(); let vaults = []; for (let id of ids) vaults.push([id, ...(await VaultO.methods.getVaultById(id).call())]); console.log("IDS:", vaults); setListOfVaults(vaults); } catch (err) { console.log(err); setInfo(err.toString()); } };
And last the markup that defines the buttons and labels, defines 3 buttons for approving the use of the contract, obtain the current balance in the account and get the IDs of the vault in the contract; Also two inputs for deposit and withdrawal some amount of Celos. Some labels show CELO, cUSD in the current account and TVL (Total Value Locked) in the contract. Last is the table with the vaults in the contract of the current user.
You can interact with the interface and see the contract behavior:
You can verify that the constraints of the contract are preserved, like the min time-locked, not existent IDs, already withdrawn lockers, etc.
It's important to see that always we use try/catch clauses to verify the reversed conditions and other errors that might occur.
The code repository is here: Link

Conclusion

This tutorial was aimed to provide a bare-bones implementation of a dApp in the Celo Ecosystem. We covered the Vault Smart Contract development and deployment, together with the bootstrapping of a React Application to interact with its basic functions (approve, lock, withdraw). We hope to continue extending this documentation.
The tutorial was a team effort by Celo Helpi.
If you had any difficulties following this tutorial or simply want to discuss Celo and DataHub tech with us you can join our community today!
Table of Contents