How to create time-locked functions using Builder and Solidity

A step by step guide.

Crisgarner
Cryptex Finance
5 min readSep 22, 2020

--

I’ve been working at Cryptex for several months now building the contracts for the World’s First Total Cryptocurrency Market Capitalization Token, TCAP. Part of my job consists of making sure the code is secure for all of our users. One way of doing this is by using a timelock.

What is a Timelock?

A timelock is a piece of code that locks functionality on an application until a certain amount of time has passed. It was widely used as a type of smart contract primitive in many Bitcoin scripts in order to lock-up bitcoins usually for periods of months or years.

Later, the ICO craze brought many scam projects where the founders dumped their tokens as soon as they could get some profit. Legit projects started implementing timelocks to signal commitment from the founders, as the timelock worked as a vesting mechanism widely used in the startup world.

Yield farming and DeFi have brought a new series of ponzinomics, dump and run schemes, and governance issues. Sushiswap had a recent episode where the founder dumped millions of dollars on Sushi Tokens from the dev pool and affected Sushi holders, wrong or not a timelock could have prevented this behavior.

Now I’ll take you through each step, along with examples of the code.

Set your Environment

For this tutorial, I’m using Buidler, a powerful developer framework for creating smart contracts on solidity. It has flexibility and nice features like console.log and stack traces, I would recommend you to give it a try. Here is the beginner's tutorial.

Simple Contract

Let’s say we have built a complex smart contract that requires the setup and management of a state in order to connect with other contracts.

pragma solidity ^0.7.0;contract DeFiProtocol {    address public ERC20Token;
address public uniswapPool;
uint256 public burnFee;
function setERC20(address _erc) public {
ERC20Token = _erc;
}

function setUniswapPool(address _pool) public {
uniswapPool = _pool;
}

function setFee(uint256 _fee) public {
burnFee = _fee;
}
}

Let’s do some improvements, first let’s create some variables that will help us handle the timelock. Also, we will need to add some security, only the administrators of the contract should be able to edit. We will add Open Zeppelin Library and use the Ownable contract.

pragma solidity ^0.7.0;import "@openzeppelin/contracts/access/Ownable.sol";contract DeFiProtocol is Ownable {    address public ERC20Token;
address public uniswapPool;
uint256 public burnFee;
enum Functions { ERC, POOL, FEE }
uint256 private constant _TIMELOCK = 1 days;
mapping(Functions => uint256) public timelock;
function setERC20(address _erc) public onlyOwner {
ERC20Token = _erc;
}

function setUniswapPool(address _pool) public onlyOwner {
uniswapPool = _pool;
}

function setFee(uint256 _fee) public onlyOwner {
burnFee = _fee;
}
}

Let’s review the changes:

  • Extending from Ownable, allow us to add the onlyOwner modifier to our functions which allow only the owner of the contract to execute it.
  • The enum allows us to access the time-locked function in a more readable way.
  • The _TIMELOCK constant defines how much we want our functions to wait until they can be executed.
  • We also create a timelock mapping which helps us to see which function is currently unlocked and create our own modifiers accordingly.
pragma solidity ^0.7.0;import "@openzeppelin/contracts/access/Ownable.sol";contract DeFiProtocol is Ownable { ...

modifier notLocked(Functions _fn) {
require(
timelock[_fn] != 0 && timelock[_fn] <= block.timestamp,
"Function is timelocked"
);
_;
}
//unlock timelock
function unlockFunction(Functions _fn) public onlyOwner {
timelock[_fn] = block.timestamp + _TIMELOCK;
}
//lock timelock
function lockFunction(Functions _fn) public onlyOwner {
timelock[_fn] = 0;
}
function setERC20(address _erc) public onlyOwner
notLocked(Functions.ERC) {
ERC20Token = _erc;
timelock[Functions.ERC] = 0;
}

function setUniswapPool(address _pool) public onlyOwner
notLocked(Functions.POOL) {
uniswapPool = _pool;
timelock[Functions.POOL] = 0;
}

function setFee(uint256 _fee) public onlyOwner
notLocked(Functions.FEE) {
burnFee = _fee;
timelock[Functions.FEE] = 0;
}
...}

This is what happened:

  • First, we created a modifier that checks if the current block timestamp (now) is greater than when we activated the unlock.
  • Then we created a unlock function that allows us to call our functions after 3 days (the _TIMELOCK constant we declared) has passed, and a lock function in case we want to cancel the time.
  • Finally, we check that the functions are unlocked and we lock them back after using them.

You can see the complete code here:

We can further optimize this code to check that the information we are going to set has been decided previously but for now, we will see how to create time-sensitive tests using buidler.

Tests

Smart contracts need to be tested, don’t let anyone fool you. Smart contracts handle a lot of money and are really difficult to fix bugs once they are deployed to mainnet, so you must squash all the bugs you can before doing so.

I won’t dive into detail on how to do proper testing or Test-Driven Development but you can read more about it on the buidler documentation.

For testing time-sensitive functions you can use the evm_increaseTime JSON-RPC method supported on Buidler in order to increase the time by seconds to the local Ethereum instance.

Here is a small snippet of how can it work;

//Gets the buidler runtime
const bre = require("@nomiclabs/buidler");
const THREE_DAYS = 259200;
const FEE_FN = 2;
describe("DeFi Contract", async function () {
...
it("...should test timelock", async () => { await expect(instance.setBurnFee(1)).to.be
.revertedWith("Function is timelocked");
await instance.unlockFunction(FEE_FN);
//fast-forward
bre.network.provider.send("evm_increaseTime", [THREE_DAYS]);
await instance.setBurnFee(1);
expect(1).to.eq(await instance.burnFee());
})...
}

Timelocks are an important tool that developers can use in order to prevent dumps, malicious actions, governance scams, and many more activities. As a smart contract developer try to use them often when implementing secure contracts that require some kind of admin keys.

You can see timelocks in action on our TCAP contracts, soon to be deployed on Rinkeby Testnet. If you want to try it out you can reach out to the Cryptex team on Reddit r/TotalCryptoMarketCap or Twitter @cryptexglobal. You can also reach out to me personally on Twitter @Crisgarner with any questions you may have. If you liked this post, you are free to share it and click on the👏 below so other people can see it on Medium.

--

--

Crisgarner
Cryptex Finance

Blockchain Development @CryptexFinance 👨‍💻, Devcon V Scholar ⛓️ @Ethereum, Founder @affogatoco☕.