Time-locked Wallets: An Introduction to Ethereum Smart Contracts

Time-locked Wallets: An Introduction to Ethereum Smart Contracts

Blockchain and its applications are popular nowadays as never before. Ethereum in particular, offering smart contract capabilities, opens the doors to new ideas that can be implemented in a distributed, immutable, and trustless fashion.

Getting started in the Ethereum smart contract space can be a little overwhelming as the learning curve is quite steep. We hope that this article (and future articles in the Ethereum series) can alleviate this pain and get you up and running quickly.

Truffle, Solidity, and ĐApps

In this article, we assume you have some basic understanding of blockchain applications and Ethereum. If you feel like you need to brush up your knowledge, we recommend this Ethereum overview from the Truffle framework.

What’s covered in this post:

  • Applications of time-locked wallets
  • Development environment setup
  • Smart contract development with the Truffle framework
    • Description of Solidity contracts
    • How to compile, migrate, and test smart contracts
  • Using a ÐApp to interact with smart contracts from the browser
    • Browser setup with MetaMask
    • A run-through of the main use case

Time-locked Wallets: Use Cases

There are many different applications of Ethereum smart contracts. The most popular at the moment are cryptocurrencies (implemented as ERC20 tokens) and crowdfunding token sales (a.k.a. initial coin offerings, or ICOs.) A good example of a utility ERC20 token is the Motoro Coin. In this blog post, we will explore something different: The idea of locking funds in crypto wallet contracts. This idea itself has various use cases.

Vesting for an ICO

There are several examples, but probably the most common reason at the moment to lock funds is called “vesting.” Imagine that you have just raised a successful ICO and your company still holds a majority of tokens distributed between your team members.

It would be beneficial to all parties involved to ensure that the tokens held by employees cannot be traded straightaway. If there are no controls in place, any given employee might take action by selling all their tokens, cashing out, and quitting the company. This would negatively affect the market price and make all remaining contributors to the project unhappy.

Crypto-based “Last Will and Testament”

Another idea is to use a smart contract as a crypto-will. Imagine we would like to store our cryptocurrency savings in a contract which will be accessible by members of the family, but only after something has happened to us. Let’s say we should “check in” with the wallet, by evoking some contract call every so-often.

If we don’t check in on time, something presumably happened to us and they can withdraw the funds. The proportion of funds they would each receive could either be explicitly set in the contract, or it could be left to be decided by consensus among the family members.

A Pension or Trust Fund

Another application of locking funds could be to create a small pension fund or time-based savings account, i.e., one that prevents the owner from withdrawing any funds before a certain time in the future. (It could be particularly useful for addicted crypto traders in helping keep their ether intact.)

The use case we’ll explore for the rest of this blog post is similar: To put some crypto-money away for later for someone else, like a future birthday gift.

Let’s imagine we would like to gift one ether to someone for their 18th birthday. We could write down on a piece of paper the account’s private key and the address of the wallet holding the funds and hand it over to them in an envelope. The only thing they would have to do is call a function on the contract from their account once they are 18 and all the funds will be transferred to them. Or, instead, we could just use a simple ÐApp. Sounds good? Let’s get started!

Ethereum Development Setup

Before you forge ahead with smart contract development, you need to have Node.js and Git installed on your machine. In this blog, we are going to be using the Truffle framework. Even though you can do without it, Truffle significantly reduces the entry barrier to the development, testing, and deployment of Ethereum smart contracts. We totally agree with their statement:

Truffle is the most popular development framework for Ethereum with a mission to make your life a whole lot easier.

To install it, run the following command:

npm install -g truffle

Now, get the code of this project:

git clone https://github.com/radek1st/time-locked-wallets
cd time-locked-wallets

It is important to note that the project follows the standard Truffle project structure and the directories of interest are:

  • contracts: Holds all the Solidity contracts
  • migrations: Contains scripts describing the steps of migration
  • src: Includes the ÐApp code
  • test: Stores all the contract tests

Overview of the Included Smart Contracts

There are several contracts included in this project. Here’s the rundown:

  • TimeLockedWallet.sol is the main contract of this project and it is described in detail below.
  • TimeLockedWalletFactory.sol is the factory contract which lets anyone easily deploy their own TimeLockedWallet.
  • ERC20.sol is an interface of the ERC20 standard for Ethereum tokens.
  • ToptalToken.sol is a customized ERC20 token.
  • SafeMath.sol is a small library used by ToptalToken for performing safe arithmetic operations.
  • Migrations.sol is an internal Truffle contract facilitating migrations.

For any questions on writing Ethereum contracts, please refer to the official Solidity smart contract docs.


Our TimeLockedWallet.sol Solidity contract looks like this:

pragma solidity ^0.4.18;

The above line indicates the minimum version of the Solidity compiler required for this contract.

import "./ERC20.sol";

Here, we import other contract definitions, used later in the code.

contract TimeLockedWallet {

The above is our main object. contract scopes the code of our contract. The code described below is from inside the curly brackets.

address public creator;
address public owner;
uint public unlockDate;
uint public createdAt;

Here we define several public variables which by default generate corresponding getter methods. A couple of them are of type uint (unsigned integers) and a couple are address (16-character long Ethereum addresses.)

modifier onlyOwner {
  require(msg.sender == owner);

In simple terms, modifier is a precondition that has to be met before even starting the execution of the function it is attached to.

function TimeLockedWallet(
    address _creator, address _owner, uint _unlockDate
) public {
    creator = _creator;
    owner = _owner;
    unlockDate = _unlockDate;
    createdAt = now;

This is our first function. As the name is exactly the same as our contract name, it is the constructor and gets called only once when the contract is created.

Note that if you were to change the name of the contract, this would become a normal function callable by anyone and form a backdoor in your contract like was the case in the Parity Multisig Wallet bug. Additionally, note that the case matters too, so if this function name were in lowercase it would also become a regular function—again, not something you want here.

function() payable public { 
  Received(msg.sender, msg.value);

The above function is of a special type and is called the fallback function. If someone sends any ETH to this contract, we will happily receive it. The contract’s ETH balance will increase and it will trigger a Receivedevent. To enable any other functions to accept incoming ETH, you can mark them with the payable keyword.

function info() public view returns(address, address, uint, uint, uint) {
    return (creator, owner, unlockDate, createdAt, this.balance);

This is our first regular function. It has no function parameters and defines the output tuple to be returned. Note that this.balance returns the current ether balance of this contract.

function withdraw() onlyOwner public {
   require(now >= unlockDate);
   Withdrew(msg.sender, this.balance);

The above function can only be executed if onlyOwner modifier defined earlier is satisfied. If the requirestatement is not true, the contract exits with an error. That’s where we check if the unlockDate has gone by. msg.sender is the caller of this function and it gets transferred the entire ether balance of the contract. In the last line, we also fire a Withdrew event. Events are described a bit later.

Interestingly, now—which is equivalent to block.timestamp—may not be as accurate as one may think. It is up to the miner to pick it, so it could be up to 15 minutes (900 seconds) off as explained in the following formula:

parent.timestamp >= block.timestamp <= now + 900 seconds

As a consequence, now shouldn’t be used for measuring small time units.

function withdrawTokens(address _tokenContract) onlyOwner public {
   require(now >= unlockDate);
   ERC20 token = ERC20(_tokenContract);
   uint tokenBalance = token.balanceOf(this);
   token.transfer(owner, tokenBalance);
   WithdrewTokens(_tokenContract, msg.sender, tokenBalance);

Here is our function for withdrawing ERC20 tokens. As the contract itself is unaware of any tokens assigned to this address, we must pass in the address of the deployed ERC20 token we want to withdraw. We instantiate it with ERC20(_tokenContract) and then find and transfer the entire token balance to the recipient. We also fire a WithdrewTokens event.

event Received(address _from, uint _amount);
event Withdrew(address _to, uint _amount);
event WithdrewTokens(address _tokenContract, address _to, uint _amount);

In this snippet, we are defining several events. Triggered events are basically log entries attached to the transaction receipts on the blockchain. Each transaction can attach zero or more log entries. The main uses of events are debugging and monitoring.

That’s all we need to time-lock ether and ERC20 tokens—just a few lines of code. Not too bad, huh? Now let’s have a look at our other contract, TimeLockedWalletFactory.sol.


There are two main reasons behind creating a higher-level factory contract. The first one is a security concern. By separating the funds in different wallets, we won’t end up with just one contract with a massive amount of ether and tokens. This will give 100% of control to just the wallet owner and hopefully discourage the hackers from trying to exploit it.

Secondly, a factory contract allows easy and effortless TimeLockedWallet contract creation, without the requirement of having any development setup present. All you need to do is to call a function from another wallet or ĐApp.

pragma solidity ^0.4.18;
import "./TimeLockedWallet.sol";
contract TimeLockedWalletFactory {

The above is straightforward and very similar to the previous contract.

mapping(address => address[]) wallets;

Here, we define a mapping type, which is like a dictionary or a map, but with all the possible keys preset and pointing to default values. In the case of the address type, the default is a zero address 0x00. We also have an array type, address[], which is holding addresses.

In the Solidity language, Arrays always contain one type and can have a fixed or variable length. In our case, the array is unbounded.

To sum up our business logic here, we define a mapping called wallets which consists of user addresses—contract creators and owners alike—each pointing to an array of associated wallet contract addresses.

function getWallets(address _user) 
    return wallets[_user];

Here, we are using the above function to return all contract wallets a _user created or has rights to. Note that view (in older complier versions called constant) denotes that this is a function that doesn’t change the blockchain state and can be therefore called for free, without spending any gas.

function newTimeLockedWallet(address _owner, uint _unlockDate)
    returns(address wallet)
    wallet = new TimeLockedWallet(msg.sender, _owner, _unlockDate);
    if(msg.sender != _owner){
    Created(wallet, msg.sender, _owner, now, _unlockDate, msg.value);

This is the most important part of the contract: The factory method. It lets us create a new time-locked wallet on the fly, by calling its constructor: new TimeLockedWallet(msg.sender, _owner, _unlockDate). We then store its address for the creator and the recipient. Later we transfer all the optional ether passed in this function execution to the newly created wallet address. Finally, we signal the Create event, defined as:

event Created(address wallet, address from, address to, uint createdAt, uint unlockDate, uint amount);


This tutorial wouldn’t be so much fun if we didn’t create our own Ethereum token, so for completeness, we are bringing to life ToptalTokenToptalToken is a standard ERC20 token implementing the interface presented below:

contract ERC20 {
  uint256 public totalSupply;
  function balanceOf(address who) public view returns (uint256);
  function transfer(address to, uint256 value) public returns (bool);
  function allowance(address owner, address spender) public view returns (uint256);
  function transferFrom(address from, address to, uint256 value) public returns (bool);
  function approve(address spender, uint256 value) public returns (bool);
  event Approval(address indexed owner, address indexed spender, uint256 value);
  event Transfer(address indexed from, address indexed to, uint256 value);

What distinguishes it from other tokens is defined below:

string public constant name = "Toptal Token";
string public constant symbol = "TTT";
uint256 public constant decimals = 6;
totalSupply = 1000000 * (10 ** decimals);

We gave it a name, a symbol, total supply of one million, and made it divisible up to six decimals.

To discover different variations of token contracts, feel free to explore the OpenZeppelin repo.



Copyrights © 2021 The Unlock Company DMCC

Stay updated with the latest Blockchain news. Subscribe and never miss a story!