Introduction to Ethereum Smart Contracts: Time-locked Wallets

The world of blockchain technology and its diverse applications are experiencing unprecedented popularity, with Ethereum’s smart contract capabilities at the forefront. These smart contracts pave the way for innovative solutions by enabling implementation within a decentralized, tamper-proof, and trustless environment.

However, diving into Ethereum smart contract development can feel like navigating a maze, especially for beginners due to the steep learning curve. This article, along with upcoming installments in our Ethereum series, aims to simplify this journey and equip you with the necessary knowledge to confidently navigate this exciting domain.

Truffle, Solidity, and Decentralized Applications (ÐApps)

This article assumes you possess a fundamental understanding of blockchain applications and the Ethereum ecosystem. If you need a refresher or wish to strengthen your foundational knowledge, we recommend referring to this Ethereum overview from the Truffle framework.

Topics covered in this article include:

  • Exploring practical applications of time-locked wallets
  • Setting up your development environment
  • Developing smart contracts using the Truffle framework, including:
    • Understanding Solidity contracts
    • Compiling, migrating, and testing smart contracts
  • Interacting with smart contracts through a ÐApp within your browser, including:
    • Configuring your browser with MetaMask
    • A walkthrough of the primary use case

Time-locked Wallets: Unveiling Their Potential

Ethereum smart contracts offer a wide spectrum of applications, with cryptocurrencies (implemented as ERC20 tokens) and crowdfunding through token sales (commonly known as ICOs) currently dominating the landscape. A prime example of a utility ERC20 token is the Motoro Coin. In this article, we’ll delve into a different application: securing funds within crypto wallet contracts. This concept itself unlocks numerous use cases.

Vesting in the Context of ICOs

While many examples exist, “vesting” stands out as one of the most prevalent reasons for locking funds. Imagine a scenario where your company has successfully concluded an ICO, and the majority of the distributed tokens are held by your team members.

Implementing measures to prevent the immediate trading of tokens held by employees proves advantageous for all stakeholders. Without such controls, an employee could potentially sell their entire token allocation, cash out, and leave the company. This action would negatively impact the market price and lead to dissatisfaction among the remaining contributors.

Crypto-based “Last Will and Testament”

Smart contracts also hold the potential to revolutionize estate planning by serving as crypto-wills. Imagine safeguarding your cryptocurrency savings in a contract accessible only by family members after a specific event, such as your untimely demise. This could involve a periodic “check-in” mechanism where you invoke a contract call at regular intervals.

Failure to check in within the stipulated timeframe could trigger a mechanism allowing designated beneficiaries to withdraw the funds. The allocation percentages could be predefined within the contract or determined through consensus among family members.

A Modern Take on Pension or Trust Funds

The concept of locking funds extends to creating innovative financial instruments, such as personalized pension funds or time-based savings accounts. These accounts could prevent the owner from withdrawing funds before a predetermined future date. Such an application could prove particularly beneficial for impulsive crypto traders seeking to safeguard their ether holdings.

The use case we’ll explore throughout this article centers around a similar concept: setting aside cryptocurrency as a future gift for someone, such as a birthday present.

Let’s imagine you intend to gift one ether to someone on their 18th birthday. Traditionally, you might write down the account’s private key and the wallet address containing the funds on a piece of paper, placing it in an envelope for safekeeping. The recipient would then need to call a function on the contract from their account upon turning 18 to receive the funds. However, a more elegant solution involves using a ÐApp. Let’s dive in!

Setting Up Your Ethereum Development Environment

Before embarking on your smart contract development journey, ensure you have Node.js and Git installed on your system. This article leverages the power of the Truffle framework. While not strictly mandatory, Truffle significantly simplifies the process of developing, testing, and deploying Ethereum smart contracts. We resonate with their mission statement:

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

To install Truffle, execute the following command:

1
npm install -g truffle

Next, clone the project’s source code:

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

Adhering to the standard Truffle project structure is crucial. The directories of interest include:

  • contracts: This directory houses all Solidity contracts.
  • migrations: This directory contains scripts outlining the migration steps.
  • src: This directory contains the ÐApp code.
  • test: This directory stores all contract tests.

Dissecting the Included Smart Contracts

This project encompasses several contracts, each serving a specific purpose:

  • TimeLockedWallet.sol forms the core contract of this project, with a detailed explanation provided below.
  • TimeLockedWalletFactory.sol acts as the factory contract, enabling anyone to deploy their own TimeLockedWallet effortlessly.
  • ERC20.sol represents an interface adhering to the ERC20 standard for Ethereum tokens.
  • ToptalToken.sol represents a customized ERC20 token.
  • SafeMath.sol is a compact library utilized by ToptalToken for performing secure arithmetic operations.
  • Migrations.sol is an internal Truffle contract facilitating seamless migrations.

If you have any questions regarding writing Ethereum contracts, the official Solidity smart contract docs serves as a comprehensive resource.

TimeLockedWallet.sol: A Closer Look

Let’s examine the code of our TimeLockedWallet.sol Solidity contract:

1
pragma solidity ^0.4.18;

This line specifies the minimum Solidity compiler version required for this contract.

1
import "./ERC20.sol";

This section imports other contract definitions that will be referenced later in the code.

1
2
3
contract TimeLockedWallet {
    ...
}

This represents our primary object. The contract keyword defines the scope of our contract code. The code within the curly brackets forms the body of our contract.

1
2
3
4
address public creator;
address public owner;
uint public unlockDate;
uint public createdAt;

Here, we define several public variables, each automatically generating corresponding getter methods. Some variables have the uint type (unsigned integers), while others have the address type (16-character Ethereum addresses).

1
2
3
4
modifier onlyOwner {
  require(msg.sender == owner);
  _;
}

In essence, a modifier acts as a precondition that must be met before the function it’s attached to can be executed.

1
2
3
4
5
6
7
8
function TimeLockedWallet(
    address _creator, address _owner, uint _unlockDate
) public {
    creator = _creator;
    owner = _owner;
    unlockDate = _unlockDate;
    createdAt = now;
}

This is our constructor function. Since its name matches our contract name, it’s invoked only once during contract creation.

Changing the contract name would turn this into a regular function callable by anyone, potentially creating a vulnerability like the one exploited in the Parity Multisig Wallet bug. Additionally, note that case sensitivity matters. If this function name were in lowercase, it would also become a regular function—an undesirable outcome in this context.

1
2
3
function() payable public { 
  Received(msg.sender, msg.value);
}

This special function, known as the fallback function, handles incoming ETH transactions. Any ETH sent to this contract is accepted, increasing the contract’s ETH balance and triggering a Received event. To enable other functions to receive ETH, mark them with the payable keyword.

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

This is a regular function without any parameters. It defines the output tuple to be returned. this.balance retrieves the current ether balance of this contract.

1
2
3
4
5
function withdraw() onlyOwner public {
   require(now >= unlockDate);
   msg.sender.transfer(this.balance);
   Withdrew(msg.sender, this.balance);
}

This function can only be executed if the onlyOwner modifier defined earlier is satisfied. If the require statement evaluates to false, the contract terminates with an error. Here, we verify if the unlockDate has passed. msg.sender represents the caller of this function, and the contract’s entire ether balance is transferred to this address. Finally, a Withdrew event is emitted.

It’s important to note that now (equivalent to block.timestamp) might not be perfectly accurate. Miners have some leeway in setting it, potentially resulting in a discrepancy of up to 15 minutes (900 seconds), as explained in the following formula:

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

Therefore, avoid using now for precise time measurements.

1
2
3
4
5
6
7
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);
}

This function handles ERC20 token withdrawals. Since the contract itself lacks knowledge of tokens assigned to its address, you need to provide the address of the deployed ERC20 token to withdraw. We instantiate it with ERC20(_tokenContract), retrieve the entire token balance, and transfer it to the recipient. A WithdrewTokens event is emitted to record the transaction.

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

This snippet defines several events. Triggered events are essentially log entries associated with transaction receipts on the blockchain. Each transaction can have zero or more log entries attached. Events serve several primary purposes, including debugging and monitoring.

That’s all it takes to implement time-locked functionality for ether and ERC20 tokens—a testament to the elegance and conciseness of Solidity. Now, let’s shift our attention to our other contract, TimeLockedWalletFactory.sol.

TimeLockedWalletFactory.sol: The Architect

There are two primary reasons for creating a higher-level factory contract. First, from a security perspective, separating funds into different wallets prevents the accumulation of a massive amount of ether and tokens in a single contract. This approach distributes control and potentially deters hackers from targeting a single, high-value contract.

Second, a factory contract simplifies the process of creating TimeLockedWallet contracts. Users can effortlessly deploy these contracts without needing a development setup. All that’s required is calling a function from another wallet or ÐApp.

1
2
3
4
5
6
7
pragma solidity ^0.4.18;

import "./TimeLockedWallet.sol";

contract TimeLockedWalletFactory {
    ...
}

This section, similar to the previous contract, is straightforward.

1
mapping(address => address[]) wallets;

Here, we define a mapping type, which resembles a dictionary or a map, but with all possible keys pre-initialized and pointing to default values. For the address type, the default value is the zero address 0x00. We also have an array type, address[], designed to store address values.

In Solidity, arrays can only hold one type of element and can have fixed or dynamic lengths. Our array, in this case, is unbounded.

In summary, we define a mapping named wallets. It consists of user addresses (both creators and owners) pointing to arrays of associated wallet contract addresses.

1
2
3
4
5
6
7
function getWallets(address _user) 
    public
    view
    returns(address[])
{
    return wallets[_user];
}

This function utilizes the mapping to return all contract wallets created by or accessible to a given _user. The view keyword (or constant in older compiler versions) indicates that this function doesn’t modify the blockchain state and can be called without incurring gas costs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function newTimeLockedWallet(address _owner, uint _unlockDate)
    payable
    public
    returns(address wallet)
{
    wallet = new TimeLockedWallet(msg.sender, _owner, _unlockDate);
    wallets[msg.sender].push(wallet);
    if(msg.sender != _owner){
        wallets[_owner].push(wallet);
    }
    wallet.transfer(msg.value);
    Created(wallet, msg.sender, _owner, now, _unlockDate, msg.value);
}

This is the crux of the contract: the factory method. It enables the dynamic creation of time-locked wallets by invoking their constructor: new TimeLockedWallet(msg.sender, _owner, _unlockDate). The address of the newly created wallet is then stored for both the creator and the recipient. Any optional ether sent along with this function execution is transferred to the new wallet’s address. Finally, a Create event, defined below, is emitted:

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

ToptalToken.sol: Introducing Our Token

This tutorial wouldn’t be complete without creating our own Ethereum token. Enter ToptalToken. ToptalToken is a standard ERC20 token adhering to the following interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 sets it apart from other tokens is defined below:

1
2
3
4
5
string public constant name = "Toptal Token";
string public constant symbol = "TTT";
uint256 public constant decimals = 6;

totalSupply = 1000000 * (10 ** decimals);

We’ve given it a name, a symbol, a total supply of one million tokens, and divisibility up to six decimal places.

To explore various token contract implementations, the OpenZeppelin repo serves as a valuable resource.

Truffle Console: Your Gateway to Contract Interaction

To get up and running quickly, launch Truffle with its built-in blockchain:

1
truffle develop

You should see output similar to this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Truffle Develop started at http://localhost:9545/

Accounts:
(0) 0x627306090abab3a6e1400e9345bc60c78a8bef57
(1) 0xf17f52151ebef6c7334fad080c5704d77216b732
(2) 0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef
(3) 0x821aea9a577a9b44299b9c15c88cf3087f3b5544
(4) 0x0d1d4e623d10f9fba5db95830f7d3839406c6af2
(5) 0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e
(6) 0x2191ef87e392377ec08e7c08eb105ef5448eced5
(7) 0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5
(8) 0x6330a553fc93768f612722bb8c2ec78ac90b3bbc
(9) 0x5aeda56215b167893e80b4fe645ba6d5bab767de

Mnemonic: candy maple cake sugar pudding cream honey rich smooth crumble sweet treat

The mnemonic seed allows you to recreate your private and public keys. For instance, you can import it into MetaMask, as shown here:

RESTORE VAULT: Using a mnemonic wallet seed to import private and public keys into MetaMask.

To compile the contracts, run:

1
> compile

You should see the following output:

1
2
3
4
5
6
7
Compiling ./contracts/ERC20.sol...
Compiling ./contracts/Migrations.sol...
Compiling ./contracts/SafeMath.sol...
Compiling ./contracts/TimeLockedWallet.sol...
Compiling ./contracts/TimeLockedWalletFactory.sol...
Compiling ./contracts/ToptalToken.sol...
Writing artifacts to ./build/contracts

Next, define the contracts to deploy in migrations/2_deploy_contracts.js:

1
2
3
4
5
6
7
var TimeLockedWalletFactory = artifacts.require("TimeLockedWalletFactory");
var ToptalToken = artifacts.require("ToptalToken");

module.exports = function(deployer) {
  deployer.deploy(TimeLockedWalletFactory);
  deployer.deploy(ToptalToken);
};

First, import the TimeLockedWalletFactory and ToptalToken contract artifacts. Then, deploy them. Note the omission of TimeLockedWallet, as this contract is deployed dynamically. For more information on migrations, refer to the Truffle migrations documentation.

To migrate the contracts, run:

1
> migrate

This should produce output resembling the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Running migration: 1_initial_migration.js
     Deploying Migrations...
     ... 0x1c55ae0eb870ac1baae86eeb15f3aba3f521df46d9816e04400e9b5951ecc099
     Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
   Saving successful migration to network...
     ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
   Saving artifacts...
   Running migration: 2_deploy_contracts.js
     Deploying TimeLockedWalletFactory...
     ... 0xe9d9c37508bb58a1591d0f052d6870810118a0a19f728bf0cea4f4e5c17acd7a
     TimeLockedWalletFactory: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
     Deploying ToptalToken...
     ... 0x0469ce110735f27bbb1a85c85a77ba4b0ba0d5aa52c3d67164045b849d8b2ed6
     ToptalToken: 0xf25186b5081ff5ce73482ad761db0eb0d25abfbf
   Saving successful migration to network...
     ... 0x059cf1bbc372b9348ce487de910358801bbbd1c89182853439bec0afaee6c7db
   Saving artifacts...

Observe that both TimeLockedWalletFactory and ToptalToken have been deployed successfully.

Finally, ensure everything functions as expected by running tests. Tests reside in the test directory and correspond to the main contracts (TimeLockedWalletTest.js and TimeLockedWalletFactoryTest.js). We’ll skip the intricacies of writing tests for brevity, leaving it as an exercise for the reader. To execute the tests, simply run:

1
> test

You should see all tests passing, like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Contract: TimeLockedWalletFactory
  ✓ Factory created contract is working well (365ms)

Contract: TimeLockedWallet
  ✓ Owner can withdraw the funds after the unlock date (668ms)
  ✓ Nobody can withdraw the funds before the unlock date (765ms)
  ✓ Nobody other than the owner can withdraw funds after the unlock date (756ms)
  ✓ Owner can withdraw the ToptalToken after the unlock date (671ms)
  ✓ Allow getting info about the wallet (362ms)

6 passing (4s)

Bringing It Together with a Time-Locked Wallet ÐApp

It’s time to witness everything in action. Interacting with the blockchain is most user-friendly through distributed applications with web UIs, known as ÐApps (or “dapps”).

Setting Up Your Decentralized Application

Running this ÐApp requires an Ethereum-enabled browser. The simplest approach is installing the MetaMask Chrome plugin. Alternatively, you can use visual guide on installing and configuring MetaMask with Truffle.

Walking Through Our Smart Contract Scenario

Let’s introduce our actors: Alice, the creator of the time-locked wallet, and Bob, the recipient and eventual owner of the funds.

Accounts for Alice and Bob on MetaMask.

Scenario Outline:

  • Alice creates a time-locked wallet for Bob, depositing some ETH.
  • Alice sends additional ERC20 Toptal Tokens.
  • Bob views accessible and created wallets.
  • Bob attempts to withdraw funds before the time lock expires (and is prevented from doing so).
  • Bob successfully withdraws ETH after the lock expires.
  • Bob withdraws all ERC20 Toptal Tokens.

First, Alice creates a time-locked wallet for Bob, sending an initial one ether. Observe the creation of the new contract wallet owned by Bob:

Creating a wallet for Alice using the Time-Locked Wallets ĐApp.

At any point after creation, anyone can top up the wallet with ether or ERC20 tokens. Let’s have Alice send 100 Toptal Tokens to Bob’s new wallet, as depicted here:

Alice using the Time-Locked Wallets ĐApp to send Bob 100 Toptal Tokens.

From Alice’s perspective, the wallet will look like this after the top-up:

Alice's view of the topped-up wallet.

Switching roles, let’s log in as Bob. Bob should see all the wallets he has created or is a recipient of. However, he cannot withdraw any funds from the contract created by Alice, as it remains time-locked:

Bob's view of the topped-up wallet while it is still time-locked.

After patiently waiting for the lock to expire…

A now-unlocked wallet on the Time-Locked Wallets ĐApp.

…Bob can finally withdraw both ether and Toptal Tokens:

Bob withdrawing ether from the unlocked wallet.
Bob withdrawing Toptal Tokens from the unlocked wallet.

With the time-locked wallet now empty, Bob’s address balance increases, leaving him content and appreciative of Alice’s generosity:

The unlocked wallet, now empty.

Interacting with the Ethereum Network

You don’t have to run these contracts locally. We’ve deployed them to the Ethereum Rinkeby testnet. ToptalToken is deployed here, and TimeLockedWalletFactory is deployed here.

Use our deployed ÐApp, linked to the aforementioned contracts and hosted on GitHub Pages. Ensure MetaMask is installed and connected to the Rinkeby testnet.

Troubleshooting Tips

During development, we encountered a few minor hiccups. MetaMask in Chrome occasionally exhibited flakiness (e.g., complaining about an invalid nonce). Reinstalling the plugin usually resolved this.

Truffle sometimes lost sync when editing smart contracts, throwing an invalid number of solidity parameters error. Running rm -r build, followed by compiling and migrating, cleared this issue.

Ethereum Development: Embracing the Challenge

We hope this article has ignited your curiosity and inspired you to embark on your Ethereum development journey. The path might be demanding, but abundant resources are available to guide you, such as this one (which we found immensely helpful). Feel free to reach out in the comments below with any questions.

The source code for this project is available is available on GitHub.

To explore using a uPort mobile app instead of MetaMask, delve into the demo and source code of an alternative, award-winning implementation of this project.

For further exploration, we invite you to read the follow-up tutorial, which focuses on ÐApp development.

Credits

Special thanks to Maciek Zielinski for their invaluable contributions to this project.

Licensed under CC BY-NC-SA 4.0