Build A Personal Crypto Savings Dapp (Part 1)

oleanji

Adebayo Olamilekan

Posted on February 29, 2024

Build A Personal Crypto Savings Dapp (Part 1)

Having a personal savings app for locking crypto (ether) can be fascinating and peaceful, but for those seeking returns (APY/APR), researching staking platforms becomes crucial to avoid scams. Interested? Let's build it together. Not interested? No harm; learn or critique. Let's Build!!

pudgy penguinss build

Table of contents

  1. Introduction and Prerequisites
  2. Contract Initialization
  3. Variables, Mappings and Structs
  4. Constructor Function
  5. Main Functions
  6. The Getter Functions
  7. Contract Wrap Up
  8. Contract Testing
  9. Contract Deployment and Verification
  10. Conclusion

Introduction and Prerequisites

This is the first part of a three-part series of building the savings dapp, In this journey, I'm adopting a backend-first approach, a preference rooted in the advantages it offers like precision in frontend parameters and Optimized development workflow.

The following tools and technologies will be used in the writing, test, and deployment of this smart contract:

Having an idea of how any the above works will make this more understandable, but if you're a total beginner then fear not, dearest gentle reader [In Lady Whisledown's Voice].

Feel free to explore the completed contract's repository here – clone it and follow the steps. Don't hesitate to give the repository a star! 😉. Additionally, you can inspect the deployed contract on the Polygon Mumbai Explorer here.

If you wish to try starting from scratch and walk your to the top then, run the following commands to set up the project:

forge init crypto-saves
Enter fullscreen mode Exit fullscreen mode

If the command throws an error, maybe you don't have forge installed on your PC then you can start by following the installation process in a previous tutorial or on the official site .


After the initialization of the project, we will then install the Openzeppelin contracts library - this gives us some ready-to-use contracts that will be needed later on.

cd crypto-saves
forge install OpenZeppelin/openzeppelin-contracts
Enter fullscreen mode Exit fullscreen mode

Then open your project in VS Code with code . In the foundry.toml file in your root directory replace this with the one currently in there;

[profile.default]
optimizer = true
optimizer_runs = 20000
solc_version = '0.8.20'

[profile.ci]
fuzz-runs = 10_000

[fmt]
line_length = 120

Enter fullscreen mode Exit fullscreen mode

Then create a new file in the src folder named CryptoSaves.sol. In this file we are going to write out all the functions and security checks the savings dapps would need; some of the main ones are:

  • lockEther: To lock the crypto (ether)
  • unlockEther: To unlock your ether after saving
  • extendLockTime: Increases the locking period of a savings
  • withdrawAllEther: Withdraws all your crypto as long as the first lockup time has passed.
  • emergencyWithdraw: Allows you to withdraw your ether as long as the emergency time set has passed.

Vs code file hierachy

Contract Initialization

To set up the contract, In the CryptoSaves.sol file write the below block of code;

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

import {Ownable} from "../lib/openzeppelin-contracts/contracts/access/Ownable.sol";

/// @title Crypto Saving Contract
/// @author Oleanji 
/// @notice A contracts that locks funds for a a particular period of time

contract CryptoSaves is Ownable(msg.sender) {
// Rest of the contract will be written here
}
Enter fullscreen mode Exit fullscreen mode

The code begins by specifying the license (MIT), and then indicates that we are using a version of the Solidity compiler equal to or greater than 0.8.13 (^0.8.13). This Solidity code defines a contract named CryptoSaves that imports the Ownable contract from the OpenZeppelin library and makes it the parent contract.

We are passing the msg.sender argument to indicate that the deployer of the contract will become its initial owner.

Variables, Mappings, and Structs

These are essentials in writing the functions for this contract, so next we define them before the main parts of the contract:

    error CannotLockZeroEther();
    error LockupIsntLocked();
    error UnlockTimeHasNotReached();
    error AdditionalMonthsShouldBeMoreThanZero();
    error NoLockupHasBeenDone();

    event EtherLocked(
        uint256 indexed id,
        string name,
        address owner,
        uint256 amount,
        uint256 releaseTime,
        string lockType
    );
    event EtherUnlocked(
        uint256 indexed id,
        string name,
        address owner,
        uint256 amount
    );
    event LockupTimeExtended(
        uint256 indexed id,
        string name,
        address owner,
        uint256 releaseTime
    );

    struct Lockup {
        uint256 lockId;
        string name;
        string lockType;
        uint256 amount;
        uint256 releaseTime;
        bool locked;
    }

    mapping(uint256 => Lockup) public lockups;

    uint256 public emergencyUnlockTimestamp;
    uint private lockIdTracker = 0;
Enter fullscreen mode Exit fullscreen mode

The five custom error messages are defined at the beginning of the contract, these errors will be thrown when appropriate conditions are not met during contract execution. After Three events are defined:

  • EtherLocked: Fired upon successful ether lock-up,
  • EtherUnlocked: Emitted when ether is unlocked,
  • LockupTimeExtended: Triggered when extending lockup durations,

Events serve as a mechanism to notify interested parties about significant changes occurring within a smart contract.

Next, a struct called Lockup is declared, which contains several fields; This struct represents a single lockup entry, storing information about how much Ether was deposited, when it should be unlocked, and whether it is currently locked or not. [Like a savings box].

Following that, a mapping called lockups is created, which maps each lock ID to its corresponding Lockup struct. This allows easy lookups of specific lock entries based on their ID numbers.

Two variables are declared next: emergencyUnlockTimestamp and lockIdTracker. emergencyUnlockTimestamp stores the timestamp at which an emergency unlock function becomes available, while lockIdTracker keeps track of the last assigned lock ID number and is used to generate unique IDs for new lockups [Using this in place of Counters.sol].

Constructor Function

constructor(uint256 _months) {
        lockIdTracker += 1;
        emergencyUnlockTimestamp = block.timestamp + (_months * 30 days);
    }
Enter fullscreen mode Exit fullscreen mode

The constructor function is a special function that gets executed only once - when the contract is deployed. Here it initializes the contract with a lockup period of _months months. It increments the lockIdTracker and calculates the emergencyUnlockTimestamp based on the input _months. Once set, the emergencyUnlockTimestamp cannot be changed.

Main Functions

  1. LockEther Function: The lockEther function permits the contract owner to lock ether for a specified duration. Parameters accepted are:
  • _months: Specifies the lockup duration in months.
  • _name: Provides a descriptive label for the lockup.
  • _lockType: Represents the type of lockup being initiated.

When invoked with valid inputs, it creates a new lockup entry with a unique ID, computes the release time, and emits an "Ether Locked" event.

function lockEther(
        uint256 _months,
        string memory _name,
        string memory _lockType
    ) external payable onlyOwner {
        if (msg.value <= 0) revert CannotLockZeroEther();
        uint256 releaseTime = block.timestamp + (_months * 30 days);
        uint256 lockId = lockIdTracker;
        lockups[lockId] = Lockup(
            lockId,
            _name,
            _lockType,
            msg.value,
            releaseTime,
            true
        );
        lockIdTracker += 1;
        emit EtherLocked(
            lockId,
            _name,
            msg.sender,
            msg.value,
            releaseTime,
            _lockType
        );
    }
Enter fullscreen mode Exit fullscreen mode
  1. UnlockEther Function: The unlockEther function unlocks an already locked ether in the contract and sends the ether back to the owner; as long as the unlock time time has passed; It accepts a single parameter: _lockID: the unique identifier for each lockup made in the contract. It also emits an EtherUnlocked event after the ether has been transferred to the owner.
 function unlockEther(uint256 _lockId) external onlyOwner {
        if (!lockups[_lockId].locked) revert LockupIsntLocked();
        if (block.timestamp < lockups[_lockId].releaseTime)
            revert UnlockTimeHasNotReached();
        uint256 amountToTransfer = lockups[_lockId].amount;
        lockups[_lockId].amount = 0;
        lockups[_lockId].locked = false;
        payable(msg.sender).transfer(amountToTransfer);
        emit EtherUnlocked(
            _lockId,
            lockups[_lockId].name,
            msg.sender,
            amountToTransfer
        );
    }
Enter fullscreen mode Exit fullscreen mode
  1. ExtendLockTime Function: Sometimes we are just not ready to open our piggy banks, so we extend the day we properly open it to become very rich [like I thought when I was a kid]. This function essentially does the same by accepting the additional months then adding that to the already to the releaseTime, then emitting an LockupTimeExtended event.
function extendLockTime(
        uint256 _additionalMonths,
        uint256 _lockId
    ) external onlyOwner {
        if (_additionalMonths <= 0)
            revert AdditionalMonthsShouldBeMoreThanZero();
        if (!lockups[_lockId].locked) revert LockupIsntLocked();
        uint256 newReleaseTime = lockups[_lockId].releaseTime +
            (_additionalMonths * 30 days);
        lockups[_lockId].releaseTime = newReleaseTime;
        emit LockupTimeExtended(
            _lockId,
            lockups[_lockId].name,
            msg.sender,
            newReleaseTime
        );
    }
Enter fullscreen mode Exit fullscreen mode

The Getter Functions

The are functions that just return the data from the blockain and do not modify or change the state of the contract also known as view functions.

  1. getAllLockUps(): Retrieves complete lockup inventory maintained by the contract. Returns an array of Lockup instances, encompassing all entries. Parameterless, designed as a public view function.

  2. getLockupDetailsById(): Extracts granular information regarding a specific lockup identified via its unique ID.
    Accepts a single parameter, _lockId, identifying the target lockup entry.
    Returns detailed information encapsulating the requested lockup as a Lockup instance.

Contract Wrap Up

The last part of the smart contract code is a receive function fallback function specifically crafted to accept incoming ether transfers.

receive() external payable {}
Enter fullscreen mode Exit fullscreen mode

After this, the whole smart contract should look like the below,
I added some comments to better explain the code.

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

import {Ownable} from "../lib/openzeppelin-contracts/contracts/access/Ownable.sol";

/// @title Crypto Saving Contract
/// @author Oleanji
/// @notice A contracts that locks funds for a a particular period of time

contract CryptoSaves is Ownable(msg.sender) {
    /// -----------------------------------------------------------------------
    /// Errors
    /// -----------------------------------------------------------------------
    error CannotLockZeroEther();
    error LockupIsntLocked();
    error UnlockTimeHasNotReached();
    error AdditionalMonthsShouldBeMoreThanZero();
    error NoLockupHasBeenDone();

    /// -----------------------------------------------------------------------
    /// Events
    /// -----------------------------------------------------------------------

    event EtherLocked(
        uint256 indexed id,
        string name,
        address owner,
        uint256 amount,
        uint256 releaseTime,
        string lockType
    );
    event EtherUnlocked(
        uint256 indexed id,
        string name,
        address owner,
        uint256 amount
    );
    event LockupTimeExtended(
        uint256 indexed id,
        string name,
        address owner,
        uint256 releaseTime
    );

    /// -----------------------------------------------------------------------
    /// Structs
    /// -----------------------------------------------------------------------
    struct Lockup {
        uint256 lockId;
        string name;
        string lockType;
        uint256 amount;
        uint256 releaseTime;
        bool locked;
    }

    /// -----------------------------------------------------------------------
    /// Mappings
    /// -----------------------------------------------------------------------
    mapping(uint256 => Lockup) public lockups;

    /// -----------------------------------------------------------------------
    /// Variables
    /// -----------------------------------------------------------------------
    uint256 public emergencyUnlockTimestamp;
    uint private lockIdTracker = 0;

    /// -----------------------------------------------------------------------
    /// Constructor
    /// -----------------------------------------------------------------------
    constructor(uint256 _months) {
        lockIdTracker += 1;
        emergencyUnlockTimestamp = block.timestamp + (_months * 30 days);
    }

    /// -----------------------------------------------------------------------
    /// External functions
    /// -----------------------------------------------------------------------

    /// @notice locks your ether for a specific amount of month
    /// @param _months the number of months to lock the ether for
    function lockEther(
        uint256 _months,
        string memory _name,
        string memory _lockType
    ) external payable onlyOwner {
        if (msg.value <= 0) revert CannotLockZeroEther();
        uint256 releaseTime = block.timestamp + (_months * 30 days);
        uint256 lockId = lockIdTracker;
        lockups[lockId] = Lockup(
            lockId,
            _name,
            _lockType,
            msg.value,
            releaseTime,
            true
        );
        lockIdTracker += 1;
        emit EtherLocked(
            lockId,
            _name,
            msg.sender,
            msg.value,
            releaseTime,
            _lockType
        );
    }

    /// @notice unlocks ether when the unlock date has reached
    /// @param _lockId the Id of the lockUp you want to unlock
    function unlockEther(uint256 _lockId) external onlyOwner {
        if (!lockups[_lockId].locked) revert LockupIsntLocked();
        if (block.timestamp < lockups[_lockId].releaseTime)
            revert UnlockTimeHasNotReached();
        uint256 amountToTransfer = lockups[_lockId].amount;
        lockups[_lockId].amount = 0;
        lockups[_lockId].locked = false;
        payable(msg.sender).transfer(amountToTransfer);
        emit EtherUnlocked(
            _lockId,
            lockups[_lockId].name,
            msg.sender,
            amountToTransfer
        );
    }

    /// @notice extends lock time ether when the unlock date has reached
    /// @param _lockId the Id of the lockUp you want to edit its date
    /// @param _additionalMonths the number of months to increase the lock up for
    function extendLockTime(
        uint256 _additionalMonths,
        uint256 _lockId
    ) external onlyOwner {
        if (_additionalMonths <= 0)
            revert AdditionalMonthsShouldBeMoreThanZero();
        if (!lockups[_lockId].locked) revert LockupIsntLocked();
        uint256 newReleaseTime = lockups[_lockId].releaseTime +
            (_additionalMonths * 30 days);
        lockups[_lockId].releaseTime = newReleaseTime;
        emit LockupTimeExtended(
            _lockId,
            lockups[_lockId].name,
            msg.sender,
            newReleaseTime
        );
    }

    /// @notice Withdraws all amount in the contract as long as you have locked once
    function withdrawAllEther() external onlyOwner {
        uint256 currentId = lockIdTracker;
        if (currentId == 0 || address(this).balance <= 0)
            revert NoLockupHasBeenDone();
        if (block.timestamp < lockups[1].releaseTime)
            revert UnlockTimeHasNotReached();
        uint256 amountToTransfer = address(this).balance;
        payable(msg.sender).transfer(amountToTransfer);
    }

    /// @notice Withdraws all amount in the contract as long as the emergency lock period has passed
    function emergencyWithdraw() external onlyOwner {
        uint256 currentId = lockIdTracker;
        if (currentId == 0 || address(this).balance <= 0)
            revert NoLockupHasBeenDone();
        if (
            emergencyUnlockTimestamp > lockups[currentId].releaseTime &&
            emergencyUnlockTimestamp > block.timestamp
        ) revert UnlockTimeHasNotReached();
        uint256 amountToTransfer = address(this).balance;
        payable(msg.sender).transfer(amountToTransfer);
    }

     /// @notice Gets all the Lockups created
    function getAllLockUps() external view returns (Lockup[] memory) {
        uint256 total = lockIdTracker;
        Lockup[] memory lockup = new Lockup[](total);
        for (uint256 i = 1; i < total; i++) {
            lockup[i] = lockups[i];
        }
        return lockup;
    }

    /// @notice Gets specific lock up details
    /// @param _lockId the Id of the lockUp you want details for
    function getLockupDetailsById(
        uint256 _lockId
    ) external view returns (Lockup memory) {
        Lockup memory lockDetails = lockups[_lockId];
        return (lockDetails);
    }

    receive() external payable {}
}

Enter fullscreen mode Exit fullscreen mode

Run this to build and compile the contract written so far.

forge build
Enter fullscreen mode Exit fullscreen mode

Contract Testing

To test the CryptoSaves contract already written we will need to write a test script for this in the test folder named CryptoSaves.t.sol, In this script we will test each of the following:

  • Creating a lockup using the lockEther Function
  • Unlocking a created lockup using the unlockEther Function
  • Extending the lockup time for a lockup
  • Getting all Lockups data with the GetAllLockUps() function
  • Getting a single lockup data with the getLockupDetailsById() function
  • Then replicating all the errors listed in the contract.

This is all shown in the below script with more comments to explain each line.

Use forge test to run the test script, add a v flag to increase the verbosity i.e for more details from running the test script so I mostly use
forge test -vvvv

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

import "forge-std/Test.sol";
import "../src/CryptoSaves.sol";
import "forge-std/console.sol";

contract CryptoSavesCheatsTest is Test {
    CryptoSaves public cryptoSaves;
    address alice = vm.addr(0x2);

    function setUp() public {
        vm.startPrank(alice);
        cryptoSaves = new CryptoSaves(8); //using 8 months as my emergency time
        vm.stopPrank();
    }

    function testLockEther() public {
        vm.startPrank(alice);

        /// crediting aka dealing 200 ethers to alice account
        vm.deal(alice, 200 ether);

        // first lockup
        cryptoSaves.lockEther{value: 5 ether}(3, "LockUpOne", "Fees");
        assertEq(cryptoSaves.getLockupDetailsById(1).amount, 5e18);

        // second lockup
        cryptoSaves.lockEther{value: 50 ether}(6, "LockUpTwo", "Family");
        assertEq(cryptoSaves.getLockupDetailsById(2).amount, 50e18);

        vm.stopPrank();
    }

    function testUnlockEther() public {
        vm.startPrank(alice);

        vm.deal(alice, 60 ether);

        // Locking up 5 ether for 3*30 days
        cryptoSaves.lockEther{value: 5 ether}(3, "My First LockUp", "Fees");
        assertEq(cryptoSaves.getLockupDetailsById(1).amount, 5e18);

        // Locking up 50 ether for 6*30 days
        cryptoSaves.lockEther{value: 50 ether}(6, "My Second LockUp", "Travel");
        assertEq(cryptoSaves.getLockupDetailsById(2).amount, 50e18);

        // skips forward into the future to  3*30 days from now to be able to unlock
        skip(7776001);

        // unlocks
        cryptoSaves.unlockEther(1);
        assertEq(cryptoSaves.getLockupDetailsById(1).locked, false);
        assertEq(address(alice).balance, 10e18);

        // skips more forward into the future to  3*30 days
        // so a total of 6*30 days from now to be able to unlock
        skip(7776001);

        //second unlock
        cryptoSaves.unlockEther(2);
        assertEq(cryptoSaves.getLockupDetailsById(2).locked, false);
        assertEq(address(alice).balance, 60e18);

        vm.stopPrank();
    }

    function testExtendLockTime() public {
        vm.startPrank(alice);

        vm.deal(alice, 60 ether);

        // Locking up 5 ether for 3*30 days
        cryptoSaves.lockEther{value: 5 ether}(3, "LockUpOne", "Fees");
        assertEq(cryptoSaves.getLockupDetailsById(1).amount, 5e18);

        cryptoSaves.extendLockTime(3, 1);
        assertGt(cryptoSaves.getLockupDetailsById(1).releaseTime, 7776001);
    }

    function testGetAllLockUps() public {
        vm.startPrank(alice);
        vm.deal(alice, 60 ether);

        // third lockup
        cryptoSaves.lockEther{value: 5 ether}(3, "LockUpOne", "Fees");
        // second lockup
        cryptoSaves.lockEther{value: 7 ether}(6, "LockUpTwo", "Fees");
        //third lockup
        cryptoSaves.lockEther{value: 21 ether}(6, "LockUpThree", "Pets");
        //fourth lockup
        cryptoSaves.lockEther{value: 11 ether}(6, "LockUpFour", "Festivals");

        // now check for all lockupdetails

        //first lockup
        assertEq(cryptoSaves.getAllLockUps()[1].amount, 5e18);
        assertEq(cryptoSaves.getAllLockUps()[1].locked, true);

        //second lockup
        assertEq(cryptoSaves.getAllLockUps()[2].amount, 7e18);
        assertEq(cryptoSaves.getAllLockUps()[2].locked, true);
        //third lockup
        assertEq(cryptoSaves.getAllLockUps()[4].amount, 11e18);
        assertEq(cryptoSaves.getAllLockUps()[4].locked, true);

        vm.stopPrank();
    }

    function testGetLockupDetailsById() public {
        vm.startPrank(alice);
        vm.deal(alice, 10 ether);

        // Locking up 5 ether for 3*30 days
        cryptoSaves.lockEther{value: 10 ether}(3, "LockUpOne", "Fees");
        assertEq(cryptoSaves.getLockupDetailsById(1).amount, 10e18);
        assertEq(cryptoSaves.getLockupDetailsById(1).locked, true);
        assertEq(cryptoSaves.getLockupDetailsById(1).lockId, 1);

        assertGt(
            cryptoSaves.getLockupDetailsById(1).releaseTime,
            3 * 30 * 24 * 60 * 60
        );
        // the months * days*hrs*min*sec
    }

    function testWithdrawAllEther() public {
        vm.deal(address(cryptoSaves), 4 ether);

        vm.startPrank(alice);

        vm.deal(alice, 60 ether);

        cryptoSaves.lockEther{value: 5 ether}(3, "LockUpOne", "Fees");
        assertEq(cryptoSaves.getLockupDetailsById(1).locked, true);

        skip(7776001);

        //withdraws all the money in the contract
        cryptoSaves.withdrawAllEther();
        assertEq(address(alice).balance, 64e18);

        vm.stopPrank();
    }

    function testEmergencyWithdraw() public {
        vm.deal(address(cryptoSaves), 48 ether);
        vm.startPrank(alice);
        vm.deal(alice, 10 ether);

        cryptoSaves.lockEther{value: 6 ether}(3, "My First LockUp", "Fees");
        assertEq(cryptoSaves.getLockupDetailsById(1).amount, 6e18);
        assertEq(
            cryptoSaves.getLockupDetailsById(1).amount + address(alice).balance,
            10e18
        );
        // the emergency withdraw time is after 8*30 daysso to to skip intime after then

        skip(8 * 30 * 24 * 60 * 60 + 1);

        //withdraws all the money in the contract for emergencies
        cryptoSaves.emergencyWithdraw();

        assertEq(address(alice).balance, 58e18);

        vm.stopPrank();
    }

    function testCannotLockZeroEther() public {
        vm.startPrank(alice);
        vm.expectRevert(CryptoSaves.CannotLockZeroEther.selector);
        cryptoSaves.lockEther{value: 0 ether}(3, "First Locking", "Pets");
        vm.stopPrank();
    }

    function testLockupIsntLocked() public {
        vm.startPrank(alice);
        vm.deal(alice, 10 ether);

        cryptoSaves.lockEther{value: 6 ether}(3, "LockUp", "Fees");
        assertEq(cryptoSaves.getLockupDetailsById(1).locked, true);

        skip(3 * 30 * 24 * 60 * 60 + 1);
        cryptoSaves.unlockEther(1);
        vm.expectRevert(CryptoSaves.LockupIsntLocked.selector);
        cryptoSaves.extendLockTime(1, 1);
        vm.stopPrank();
    }

    function testUnlockTimeHasNotReached() public {
        vm.startPrank(alice);
        vm.deal(alice, 10 ether);

        cryptoSaves.lockEther{value: 6 ether}(3, "LockUp No1", "Fees");

        vm.expectRevert(CryptoSaves.UnlockTimeHasNotReached.selector);
        cryptoSaves.unlockEther(1);

        //skip by a month to test if it will go through
        skip(1 * 30 * 24 * 60 * 60 + 1);

        vm.expectRevert(CryptoSaves.UnlockTimeHasNotReached.selector);
        cryptoSaves.withdrawAllEther();
        //skip by another month totest if it will go through
        skip(1 * 30 * 24 * 60 * 60 + 1);

        vm.expectRevert(CryptoSaves.UnlockTimeHasNotReached.selector);
        cryptoSaves.emergencyWithdraw();

        vm.stopPrank();
    }

    function testAdditionalMonthsShouldBeMoreThanZero() public {
        vm.startPrank(alice);
        vm.deal(alice, 10 ether);

        cryptoSaves.lockEther{value: 6 ether}(3, "My First LockUp", "Fees");

        vm.expectRevert(
            CryptoSaves.AdditionalMonthsShouldBeMoreThanZero.selector
        );
        cryptoSaves.extendLockTime(0, 1);
        vm.stopPrank();
    }

    function testNoLockupHasBeenDone() public {
        vm.startPrank(alice);
        assertEq(address(cryptoSaves).balance, 0);

        vm.expectRevert(CryptoSaves.NoLockupHasBeenDone.selector);
        cryptoSaves.emergencyWithdraw();

        vm.stopPrank();
    }
}

Enter fullscreen mode Exit fullscreen mode

Contract Deployment and Verification

This is the last part of building this project, here we will deploy and verify the contract to the polygon Mumbai test network, Before that we need to write our deployment script which essentially shows the deployment of the CryptoSaves contract, along with an eight-month emergency lock period.

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

import {Script} from "../lib/forge-std/src/Script.sol";
import {console} from "../lib/forge-std/src/console.sol";
import {CryptoSaves} from "../src/CryptoSaves.sol";

contract CryptoSavesScript is Script {
    function run() external {
        vm.startBroadcast();
        console.log(
            "....Deploying CryptoSaves deployer with 8 months emergency lock period.... "
        );
        CryptoSaves cryptoSave = new CryptoSaves(8);
        console.log("CryptoSaves Contract Deployed To:", address(cryptoSave));
        vm.stopBroadcast();
    }
}

Enter fullscreen mode Exit fullscreen mode

To run this script, we will use the below command which not only deploys the contract but also verifies it on the polygon mumbai test network.

forge script script/CryptoSaves.s.sol:CryptoSavesScript --rpc-url RPC_URL  --private-key YOUR_PRIVATE_KEY --broadcast --verify --etherscan-api-key YOUR_POLYGONSCAN_API -vvvv --gas-price 60 --legacy
Enter fullscreen mode Exit fullscreen mode

To use this command you will need:

  • RPC_URL : This is a url needed to deploy the contract and you can get it from different providers, but in this tutorial I am using Ankr.

  • YOUR_PRIVATE_KEY: This is the key of the account you want to use for deployment which shouldn't be shared with anyone. You can get some mumbai test token to use for deployment here

  • YOUR_POLYGONSCAN_API: In order to verify the contract you will need an api key from polygon, you can get that by creating and account then an api key.

After getting all the keys and replacing them you can now run the command to deploy and verify your crypto saves contract.

deployed result

From the result obtained here, the contract was deployed to:

Conclusion

This marks the end of our journey in this tutorial. I hope you found this helpful and resourceful, have any questions? leave a comment.

Now you have a fully deployed and verified personal crypto savings contract. Feels good right!

Yesss

Note: The code for this project is here.

💖 💪 🙅 🚩
oleanji
Adebayo Olamilekan

Posted on February 29, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related