Escrowing ERC20 Tokens

[EDIT 2018-06-05] This post has been edited to introduce a transfer function.

This post describes a simple contract to escrow ERC20 tokens for a period of time. Keeping tokens (or ether) in escrow is a common pattern in smart contracts.

Escrow is particularly important for off-chain components that work with signed messages. Such a component often needs certainty that a token transfer promised via a signature can be fulfilled.

A simple procedure for escrowing tokens is as follows:

An account approves a token transfer to the trusted smart contract.

The smart contract transfers those tokens to itself, holding them in escrow.

An account cannot withdraw escrowed tokens without first signaling the desire to end escrow.

The escrow period ends a fixed amount of time after the signal to end escrow.

Once the escrow period ends, the account may withdraw any remaining tokens.

The pattern above was exploited in the
payment channel post, for instance, where ether was kept in escrow rather than ERC20 tokens.

Cancellation Period

This contract is parameterized with the escrow duration. When an account wishes to withdraw its tokens, this is how long it must wait before the withdrawal is allowed.

contractEscrow{uint256publicescrowTime;constructor(uint256_escrowTime)public{escrowTime=_escrowTime;}// more to come}

Tracking Per-Token Balances and Expirations

To make the escrow fully general, the contract does not assume a specific ERC20 token, so a single account may escrow different tokens at the same time. For each (account, token) pair, the contract tracks its balance and the time when escrow expires:

Deposit

Depositing tokens works with the familiar ERC20 pattern of approve()/transferFrom(). Once the depositor has approved the transfer, they can call deposit() to move the tokens into the escrow contract. The escrow expiration time is initialized to the maximum allowed time, making it effectively infinite.

After the escrow time has expired, all of the (remaining) escrowed tokens can be transferred back to their original owner:

functionwithdraw(IERC20Tokentoken)public{require(now>escrowExpiration[msg.sender][token],"Funds still in escrow.");uint256amount=escrowBalance[msg.sender][token];escrowBalance[msg.sender][token]=0;require(token.transfer(msg.sender,amount));}

Transferring Tokens

Contracts require escrowed tokens to assure the ability to transfer those tokens. Therefore, the contract supports a transfer function that enables transferring tokens between accounts. This routine would only be called by a subcontract of Escrow.

Escrowing Ether

This contract supports escrowing any ERC20 tokens, but it does not directly help escrow ether. One can, however,
wrap ether in an ERC20 token
to effectively escrow ether with this contract.

Summary

Escrow is a common pattern in smart contracts, especially those with off-chain components.

This contract can escrow any tokens of all ERC20 types for any account.

This contract doesn’t do anything other than escrow tokens—its value is in being incorporated in a contract that needs escrow functionality.

The Complete Contract

escrow.sol

pragma solidity^0.4.24;import"./ierc20token.sol";contractEscrow{uint256publicescrowTime;constructor(uint256_escrowTime)public{escrowTime=_escrowTime;}mapping(address=>mapping(address=>uint256))publicescrowBalance;mapping(address=>mapping(address=>uint256))publicescrowExpiration;functiondeposit(IERC20Tokentoken,uint256amount)public{require(token.transferFrom(msg.sender,this,amount));escrowBalance[msg.sender][token]+=amount;escrowExpiration[msg.sender][token]=2**256-1;}eventStartWithdrawal(addressindexedaccount,addresstoken,uint256time);functionstartWithdrawal(IERC20Tokentoken)public{uint256expiration=now+escrowTime;escrowExpiration[msg.sender][token]=expiration;emitStartWithdrawal(msg.sender,token,expiration);}functionwithdraw(IERC20Tokentoken)public{require(now>escrowExpiration[msg.sender][token],"Funds still in escrow.");uint256amount=escrowBalance[msg.sender][token];escrowBalance[msg.sender][token]=0;require(token.transfer(msg.sender,amount));}functiontransfer(addressfrom,addressto,IERC20Tokentoken,uint256tokens)internal{require(escrowBalance[from][token]>=tokens,"Insufficient balance.");escrowBalance[from][token]-=tokens;escrowBalance[to][token]+=tokens;}}