Loading...
Hardhat is an Ethereum development environment just like Truffle, which we'll use to develop our contract and deploy it on our local network and EtherJs is a library for interacting with the Ethereum blockchain that we'll use in our test suits to interact with the deployed contract.
Installation and setup
Let’s go over the necessary step for our project.
So let’s run the following command
npm install --save-dev hardhat && npx hardhat
This will install the package and launch the interactive interface to create the project. We’ll select > Create an advanced sample project
and proceed.
Hardhat will generate a couple of files and folders, but we are only interested in a few of them.
The contracts
folder will host our smart contract and the test
folder our test and hardhat.config.js
the generated configuration from hardhat — for which we need to make sure that the solidity version defined here matches the specified version of our contract.
Writing the smart contract
We need these basic specifics for this smart contract:
First, we need to define the shape of our Vault
The contract is governed by an owner, who is specified in the constructor function when the contract is deployed. The owner has the ability to change the ownership of the contract and withdraw funds from the contract. The contract also includes a feePercent variable, which is set to 10 by default and is used to calculate the transaction fee for each deposit.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Vault {
address payable public owner;
uint256 public feePercent = 10;
uint256 public lockDuration = 86400; // duration in seconds (1 day)
}
constructor() {
owner = payable(msg.sender);
}
Next, we’ll need to declare a variable that will help store and map.
The purpose of the `balance` mapping is to keep track of the amount of Ethers deposited by each user in the `Vault` contract. The `public` visibility modifier is used to allow external contracts and accounts to read the `balances` mapping.
This sort of mapping helps to manipulate and access our data.
contract Vault {
mapping(address => uint256) public balances;
}
The deposit function
This function allows users to deposit funds into the Vault. It checks that the deposited amount is positive, calculates the fee, transfers the fee to the owner, and adds the remaining amount to the user's balance.
contract Vault {
constructor() {
owner = payable(msg.sender);
}
function deposit() public payable {
require(msg.value > 0, "Amount must be positive");
uint256 fee = (msg.value * feePercent) / 100;
address payable ownerPayable = owner;
ownerPayable.transfer(fee);
uint256 depositAmount = msg.value - fee;
balances[msg.sender] += depositAmount;
}
}
Check balance function
This function returns the balance of the calling user.
contract Vault {
function checkBalance() public view returns (uint256) {
return balances[msg.sender];
}
}
Change owner function
This function allows the owner to change to a new owner.
contract Vault {
function changeOwner(address payable newOwner) public {
require(msg.sender == owner, "Only owner can change it");
owner = newOwner;
}
}
Owner withdraw function
This function allows the owner to withdraw funds from the Vault. It checks that the caller is the owner and that the owner has enough balance.
contract Vault {
function ownerWithdraw(uint256 amount) public payable {
require(msg.sender == owner, "Only owner can withdraw");
require(balances[owner] >= amount, "Not enough balance");
owner.transfer(amount);
balances[owner] -= amount;
}
}
Release amount function
This function allows the user to release their funds after a lock duration. It checks that the funds are not locked and that the user has enough balance to withdraw. It then transfers the funds to the user and sets the balance to 0.
contract {
function releaseAmount() public payable {
require(block.timestamp >= lockDuration, "Amount is still locked");
require(balances[msg.sender] > 0, "Balance is low to withdraw");
payable(msg.sender).transfer(balances[msg.sender]);
balances[msg.sender] = 0;
}
}
Testing the smart contract
The test cases for the Vault contract are written using the Hardhat testing framework and the Chai assertion library. The tests ensure that the contract behaves as expected by checking the values of various variables and testing different functions of the contract.
Now we’ll create a file inside the test
folder as Vault.test.js
then import from chai
, ethers
, and hardhat
.
const { ethers } = require('hardhat');
const { assert, expect } = require('chai');
const { utils } = require('ethers');
Next, we’ll declare our test suit and deploy our contract in beforeEach
. This will deploy a new contract every time we run a new test case so that we always have a blank contract to work with.
describe('Vault', (accounts) => {
let vault, vaultFactory;
let depositAmount = utils.parseEther('1');
let alice;
beforeEach(async () => {
vaultFactory = await ethers.getContractFactory('Vault');
vault = await vaultFactory.deploy();
owner = await web3.eth.getAccounts()[0];
});
Should have the correct feePercent value
it('should have the correct feePercent value', async () => {
const feePercent = await vault.feePercent();
expect(feePercent.toString()).to.equal('10');
});
Should have the correct lockDuration value
it('should have the correct lockDuration value', async () => {
const lockDuration = await vault.lockDuration();
expect(lockDuration).to.equal('86400');
});
Should have the correct owner
it('should have the correct owner', async () => {
const owner = await vault.owner();
assert.equal(owner.toString(), owner);
});
Should deposit the funds
it('Should deposit the funds', async function () {
await vault.deposit({ value: depositAmount });
const balance = await vault.checkBalance();
assert.equal(
balance.toString(),
depositAmount - (depositAmount * 0.1).toString()
);
});
Should change the owner
it('Should change the owner', async function () {
const accounts = await ethers.getSigners();
const newOwner = await accounts[1].getAddress();
await vault.changeOwner(newOwner);
const ownerAddress = await vault.owner();
assert.equal(ownerAddress, newOwner);
});
Should allow the owner to withdraw funds
it('Should allow owner to withdraw funds', async function () {
const accounts = await ethers.getSigners();
const owner = await vault.owner();
assert.equal(owner, await accounts[0].getAddress());
const depositAmount = 1000;
await vault.deposit({ value: depositAmount });
const balanceBeforeWithdraw = await vault.balances(owner);
const withdrawAmount = 100;
await vault.ownerWithdraw(withdrawAmount);
const balanceAfterWithdraw = await vault.balances(owner);
assert.equal(
balanceBeforeWithdraw.sub(withdrawAmount).toString(),
balanceAfterWithdraw.toString()
);
});
Should not allow non-owner to withdraw funds
it('Should not allow non-owner to withdraw funds', async () => {
const depositAmount = ethers.utils.parseEther('1');
const withdrawAmount = ethers.utils.parseEther('0.5');
await vault.deposit({ value: depositAmount, from: alice });
try {
await vault.ownerWithdraw(withdrawAmount, { from: alice });
} catch (error) {
assert.include(error.message, 'Only owner can withdraw');
}
});
Should allow the user to release their funds after the lock duration
it('should allow the user to release their funds after lock duration', async function () {
const accounts = await ethers.getSigners();
const owner = await vault.owner();
assert.equal(owner, await accounts[0].getAddress());
const depositAmount = 1000;
await vault.deposit({ value: depositAmount });
await time.increase(86400);
const initialBalance = await vault.balances(owner);
await vault.releaseAmount();
const finalBalance = await vault.balances(owner);
expect(finalBalance > initialBalance);
});
Overall, the Vault contract and its test cases demonstrate the basic functionality of a smart contract that allows users to deposit and withdraw funds securely. The tests ensure that the contract behaves as expected and that users' funds are protected from unauthorized access.
Thank you!