Time locked functions with Solidity
Tags: smartcontract, solidity, blockchain, ethereum
Published: 2023-03-12 19:15:59

# Time locked functions in smart contracts with Solidity

You may know that access control is a pretty big deal in smart contracts development and might have heard of OpenZeppelin [Ownable](https://docs.openzeppelin.com/contracts/3.x/api/access#Ownable) contract that provides a way to restrict function access to a single "owner" so that this user can perform administrative actions and whatnot, which is the go-to when it comes to access control in Solidity. Here we'll see how we can limit our users from calling a function again in a certain time span.  
Basically, we want our users to only be able to call a given function once in a given period.
This could be useful to limit some actions in community driven applications.

<hr>

Let's get to work! You can code along if you wish by cloning this [repository](https://github.com/Oli8/time-locked-functions).

Let's say you're a generous fellow and created a contract that allows anyone to call the `withdraw` function and get 0.1 ETH. 
Now your friends heard about it and they're a bunch of jerks so they want to get all the ethers for themselves, therefore you decide to limit the withdraw function to one call a day by user.

Here is the contract we'll start with:

```solidity
contract Vault {
    uint256 constant private _WITHDRAW_AMOUNT = 0.1 ether;

    function deposit() external payable { }

    function withdraw() external {
        require(
            address(this).balance >= _WITHDRAW_AMOUNT,
            "Insufficient funds"
        );

        _transferEth(msg.sender, _WITHDRAW_AMOUNT);
    }

    function _transferEth(address to, uint256 amount) private {
        (bool sent,) = to.call{value: amount}("");
        require(sent, "Transfer failed");
    }
}
```

We created a a payable deposit function so we can fund the contract, the remaining code is pretty straightforward, the `withdraw` function checks that the contract has enough funds before sending ethers to the caller.

 Here are our test cases:

```javascript
const Vault = artifacts.require('Vault')
const { expectRevert, time } = require('@openzeppelin/test-helpers')
const { web3 } = require('@openzeppelin/test-helpers/src/setup')
const { toWei } = web3.utils

contract('Vault', ([alice, bob]) => {
  let contract
  beforeEach(async () => {
    contract = await Vault.new()
    await contract.deposit({ from: alice, value: toWei('0.2', 'ether') })
  })

  it('should allow withdrawing', async () => {
    const withdraw = await contract.withdraw({ from: bob })

    expect(withdraw.receipt.status).to.be.true
  })

  it('should prevent from withdrawing again on the same day', async () => {
    await contract.withdraw({ from: bob })
    await time.increase(time.duration.hours(12))

    await expectRevert(contract.withdraw({ from: bob }), 'Account under timelock')
  })

  it('should allow another withdrawal the next day', async () => {
    await contract.withdraw({ from: bob })
    await time.increase(time.duration.hours(36))
    const lastWithdraw = await contract.withdraw({ from: bob })

    expect(lastWithdraw.receipt.status).to.be.true
  })
})

```

We deploy a new contract and fund it with 0.2 ETH before each test.
We use [openzeppelin test-helpers](https://docs.openzeppelin.com/test-helpers/0.5/) to simulate the passing of time.
You can guess from the code the features we will implement:
- Allowing a first withdrawal.
- Preventing another withdrawal within a 24 hours timespan.
- Allowing another withdrawal that happens more than 24 hours after the first one.

Let's start by adding a mapping that will keep track of the release time - at which point a user can call our function again - : 

```solidity
mapping(address => uint256) private _timestamps;
```

Now we need to update it in the `withdraw` function.
We add it at the end but before the `_transferEth` to avoid reentrancy vulnerabilities.
We set the release time for the caller to one day from now using `block.timestamp` - the current time - and the [days unit](https://docs.soliditylang.org/en/v0.8.18/units-and-global-variables.html#time-units) providing the number of seconds for a given amount.


```solidity
function withdraw() external {
    address caller = msg.sender;
    require(
        address(this).balance >= _WITHDRAW_AMOUNT,
        "Insufficient funds"
    );

    _timestamps[caller] = block.timestamp + 1 days;
    _transferEth(caller, _WITHDRAW_AMOUNT);
}
```

Note that we also added a `caller` variable as we'll use its value again later on.

Now that we have a record of the release time for a user, we can check it against the current time : 

```solidity
require(
    block.timestamp > _timestamps[caller],
    "Account under timelock"
);
```

If `block.timestamp` is greater than `_timestamps[caller]` then we've passed the release time, otherwise it is still ahead in the future.
If a user didn't call our function before then we've never set `_timestamps` for that address and  `_timestamps[caller]`would equal the default value for an `uint` type: 0, therefore passing our require clause.

The `withdraw` function now looks like this:
```solidity
function withdraw() external {
    address caller = msg.sender;
    require(
        block.timestamp > _timestamps[caller],
        "Account under timelock"
    );
    require(
        address(this).balance >= _WITHDRAW_AMOUNT,
        "Insufficient funds"
    );

    _timestamps[caller] = block.timestamp + 1 days;
    _transferEth(caller, _WITHDRAW_AMOUNT);
}
```

Let's run our tests with `truffle test`:

```
Contract: Vault
    ✔ should allow withdrawing
    ✔ should prevent from withdrawing again on the same day 
    ✔ should allow another withdrawal the next day

3 passing
```
 
Yeah, we did it! 
But what if we wanted to share the same time lock with another function? We'd have to rewrite pretty much the same code in each functions, let's see how we can extract that logic to another contract and write a `modifier`.

```solidity
abstract contract TimeLock {
    uint256 private _timeLockDuration;

    mapping(address => uint256) private _timestamps;

    constructor(uint256 timeLockDuration) {
        _timeLockDuration = timeLockDuration;
    }

    modifier timeLocked() {
        address caller = msg.sender;
        require(
            block.timestamp > _timestamps[caller],
            "TimeLock: Account under timelock"
        );
        _timestamps[caller] = block.timestamp + _timeLockDuration;
        _;
    }
}
```

We now set the duration in the constructor and made a `modifier`. 
Let's update our Vault contract:

```solidity
import "./TimeLock.sol";

contract Vault is TimeLock {
    uint256 constant private _WITHDRAW_AMOUNT = 0.1 ether;

    constructor() TimeLock(1 days) { }

    function deposit() external payable { }

    function withdraw() external timeLocked {
        require(
            address(this).balance >= _WITHDRAW_AMOUNT,
            "Insufficient funds"
        );

        _transferEth(msg.sender, _WITHDRAW_AMOUNT);
    }

    // [...]
}
```

We've changed a few things:
- Import the TimeLock contract: `import "./TimeLock.sol";`
- Inherit our new contract: `contract Vault is TimeLock {`
- Initialize the lock duration in the constructor: `constructor() TimeLock(1 days) { }`
- Add the modifier on the relevant function: `function withdraw() external timeLocked {`
- We remove the `_timestamps` mapping as it is now declared in the TimeLock contract

Let's run our tests again, hopefully we didn't break anything :D

```
Contract: Vault
    ✔ should allow withdrawing
    ✔ should prevent from withdrawing again on the same day 
    ✔ should allow another withdrawal the next day

3 passing
```
There we have it! A clean way to control access to our functions with a modifier.
You can check the code on the [`final`](https://github.com/Oli8/time-locked-functions/tree/final) branch of the repository.
Hopefully you've learned a few things about smart contracts and Solidity :)
Be wary that `block.timestamp` can be manipulated by miners therefore you shouldn't rely on it for a duration under 15 minutes or for critical features.

By the way, if you ever encounter such a use case, you're welcome to check out the [soliv](https://github.com/Oli8/soliv) library that contains a `TimeLock` and `TimeLockGroups` contract handling what we've seen with additional features ✌️