Testing With Time - Dev Log 7

Weekly Update

  1. I’m a Dad again(for the fourth time).  Weston James was born on Tuesday evening June the 20th.  Mom and baby are healthy.  Big Bro, Big Sis, and Big Sis 2 are super excited, and Dad is really proud.
  2. I guess I’m a professional solidity developer as someone paid me actual money(well ETH) for the contract we are discussing today.  Not a lot, but enough be significant.
  3. Work continues on the whitepaper for the first service offering that Catallax will be trying to launch.  Lawyers, accountants, legalese, etc., etc.

Today we are going to do some testing with time.  The is going to be very important once we start testing our demurrage code.  In our decaying token we used passing block numbers to calculate our decay periods.  In the example today we are going to use the actual time system in Ethereum.  This system isn’t an exact science since we are somewhat dependent on the Miners to give a truthful timestamp on each block.  You would not use this system if you were doing calculations on short time scales because a miner could attempt to cheat your contract.  Our example today uses month long periods so we should be fine using time.
 
The contract we are going to develop is called a DisciplineWallet.  Do you have lambo money from buying into the ethereum crowdsale two years ago?  Afraid you’re going to get spooked and sell?  Use the DisciplineWallet and restrict access to your ETH on a monthly basis.
 
When we first started talking about this contract the client wanted the payout to be consistent in USD.  We went back and forth and finally decided that we needed to do it in ETH because there isn’t really a good USD to ETH Oracle that is reliable over long periods of time.  If you know of one, please let me know and I’ll try to update this contract to do USD instead.
 
Our contract will do the following:

  • Be initialized with a term in months and an amount of ETH to pay out each month
  • Take Deposits in ETH
  • Expose a Withdraw function that can be called once a month to send the specified amount of ETH to the owner of the contract
  • Let us change the owner of the contract
  • Withdraw all the remaining ETH to a specified address once the term has run its course
  • Give us a safety function to move coins out of the contact if we accidentally send ERC20 tokens to the contract
  • Use the fallback function as a deposit function
  • Tell us when the next withdrawal is available

Once we write up our contract we are going to want to test it.  To do this we are going to need a blockchain environment that will let us make time go by quickly. Fortunately, the testrpc that is packaged with the truffle dev environment lets us do this with the following call:

web3.currentProvider.sendAsync({
       jsonrpc: "2.0",
       method: "evm_increaseTime",
       params: [86400 * 34],
       id: new Date().getTime()
     }


In the above example, we are moving the clock forward 34 days(86400 seconds in a day).  Our tests will do withdrawals and then move the clock forward and do withdrawals again.
 
Our contract will need to pass the following tests:
 
    ✓ should Deploy a wallet with owner (91ms)
    ✓ should be off when it starts and has no ether (69ms)
    ✓ should have a function Period that returns the length of a month (102ms)
    ✓ should have a function NextWithdraw that returns the first withdraw time (94ms)
    ✓ should turn on when sent ether (775ms)
    ✓ should fail if constructor sent ether
    ✓ should allow withdraw after 1 month and ether goes to owner (750ms)
    ✓ should fail on instant withdraw (255ms)
    ✓ should not allow more than term number of withdraws (955ms)
    ✓ should not allow withdraw if some time has passed, but not enough (382ms)
    ✓ should allow withdraw all after term is over (3389ms)
    ✓ should fail if withdrawAll is called before term is over (388ms)
    ✓ should reject payment if payout term is expired (2778ms)
    ✓ should deposit using deposit function (245ms)
    ✓ should allow for transfer of owner (1529ms)
    ✓ should reduce payout if payout would drain account before the end of the term (679ms)

The code and truffle set up can be found on GitHub here. 
Here is our contact.

pragma solidity ^0.4.11;

contract DisciplineWallet {

  /**
  * @dev term is the number of months that a wallet will pay out
  */
  uint16 public term;


  /**
  * @dev currentTerm tracks the number of withdraws that have occured
  */
  uint16 public currentTerm;

  /**
  * @dev contractStart stores the timestamp(number of seconds since 1/1/1970) that the contract was created
  */
  uint public contractStart;

  /**
  * @dev payout is the number of wei(smallest unit of ETH) to pay out
  */
  uint public payout;


  /**
  * @dev bActive is tracks if the wallet is open for business or not
  */
  bool public bActive;
  address public owner;

  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    if (msg.sender != owner) {
      throw;
    }
    _;
  }

  /**
   * @dev constructor
   * @param termLength number of months that the contract will be active
   * @param weiOutputPerPeriod of wei to pay out
   */
  function DisciplineWallet(uint16 termLength, uint weiOutputPerPeriod) {

    if(termLength < 1) throw; //term cant be 0
    if(weiOutputPerPeriod < 1) throw; //we cant pay out 0

    //the owner will start as the address creating the contract
    owner = msg.sender;

    //set the term
    term = termLength;

    //record the time of the start contract
    contractStart = now;

    //start with the current term as 1
    currentTerm = 1;

    //set the payout per period
    payout = weiOutputPerPeriod;
  }


  /**
   * @dev calculates the length of a month
   */
  function Period() constant returns (uint32 lengthOfMonth){
    //put this in a constant function so that we don't have to use storage
    //execution takes 302 gas which means calculation is more efficent in any contract under 66 months.
    //not sure about deployment costs
    //the period is about 30.66 days so that leap year is taken into account every 4 years.
    return (1 years / 12) + (1 days / 4);
  }

  /**
   * @dev tells us what date we can call the withdraw function
   */
  function NextWithdraw() constant returns(uint secondsAfter1970){
    return contractStart + (Period() * currentTerm);
  }

  /**
   * @dev the fallback function will make a deposit
   */
  function () payable{
    Deposit();
  }

  /**
   * @dev Put ETH into the contract, also called by the fallback function
   */
  function Deposit() payable {
    //don't accept ether if the term is over
    if(currentTerm > term) throw;
    if(bActive == false){
      //first time we get ether, turn on the contract
      bActive = true;
    }
  }

  /**
   * @dev Withdraw will send the payout to the owner if enough time has passed
   */
  function Withdraw() onlyOwner returns(bool ok){
    if(currentTerm <= term && now > NextWithdraw()){

      //payout may not be more than balance / term or the account has been underfunded
      //if it is then use the lower calculatio
      if(payout < (this.balance / (term - currentTerm + 1))){
        owner.transfer(payout);
      }
      else{
        owner.transfer(this.balance / term);
      }

      currentTerm = currentTerm + 1;

      if (currentTerm > term){
        bActive = false;
      }

      return true;
    } else{
      throw;
    }

  }


  /**
   * @dev once all terms have expired you can move any remaining ETH to another account
   * @param targetAddress The address to transfer remaining ETH to.
   */
  function WithdrawAll(address targetAddress) onlyOwner returns(bool ok){
    if (targetAddress == 0x0) throw; //dont accidentally burn ether
    if(currentTerm > term && this.balance > 0){
      targetAddress.transfer(this.balance);
      bActive = false;
      return true;
    } else {
      throw;
    }

  }

  /**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function transferOwnership(address newOwner) onlyOwner {
    if (newOwner != address(0)) {
      owner = newOwner;
    }
  }

  /**
   * @dev Allows the owner to send tokens elsewhere that were accidentally sent to this account
   * @param Token The address of the ERC20 token
   * @param _to The address the tokens should end up at
   * @param _value The amount of tokens
   */
  function safetyTransferToken(address Token, address _to, uint _value) onlyOwner returns (bool ok){
    bytes4 sig = bytes4(sha3("transfer(address,uint256)"));
    return Token.call(sig, _to, _value);
  }

}


The tests are in CoffeeScript and can easily be compiled to js. The core test concept here is the passage of time.
 
In the "it should allow withdraw after 1 month and ether goes to owner" test we simulate the passing of 34 days via our testrpc evm_increaseTime function mentioned earlier.

Our test code needs to do this multiple time over a set amount of time.  I use a variant of the following function:

withdrawFunction = ()->
  return q.Promise (resolve, reject)->
    web3.currentProvider.sendAsync
      jsonrpc: "2.0",
      method: "evm_increaseTime",
      params: [86400 * 34],  # 86400 seconds in a day
      id: new Date().getTime()
    , (err)->
      i.Withdraw(from: accounts[0])
      .then (result)->
        resolve result
      .catch (err)->
        reject err

We can call it in a loop like this:

async.eachSeries [1..13], (item, done)->
        #console.log item
        withdrawFunction()
        .then (result)->
          i.currentTerm.call(from: accounts[0])
        .then (result)->
          #console.log result
          done()
        .catch (err)->
          #console.log err
          done(err)
    .then (result)->
      assert(false, "shouldnt be here")
    .catch (error)->
      if error.toString().indexOf("invalid op") > -1
        #console.log("We were expecting a Solidity throw (aka an invalid op), we got one. Test succeeded.")
        assert.equal error.toString().indexOf("invalid op") > -1, true, 'didnt find invalid op throw'
      else
        assert(false, error.toString())
  done()

In the above case we call withdraw 13 times and expect it to fail on the 13th try.
 
We now have a great way to test the passage of time when testing our solidity contracts.  Since Catallax will likely do monthly this avenue of calculation may work.  We could manipulate our Decay Token to use timestamps instead of block numbers.  On the other hand, using block numbers allows us more control if we need to do some emergency decay as a monetary lever.

If this is interesting to you and you'd like to see where we are going with Catallax, please pick up my book Immortality(Purchasing a hard or kindle copy helps support this project.

Donations always accepted at:

BTC: 1AAfkhg1NEQwGmwW36dwDZjSAvNLtKECas

ETH and Tokens: 0x148311c647ec8a584d896c04f6492b5d9cb3a9b0