Browse Source

Inheritable contract

zava 8 years ago
parent
commit
4fe2157e36
2 changed files with 230 additions and 0 deletions
  1. 91 0
      contracts/ownership/Inheritable.sol
  2. 139 0
      test/Inheritable.js

+ 91 - 0
contracts/ownership/Inheritable.sol

@@ -0,0 +1,91 @@
+pragma solidity ^0.4.11;
+
+
+import './Ownable.sol';
+
+
+/**
+ * @title Inheritable
+ * @dev The Inheritable contract provides ownership transfer capabilities, in the
+ * case that the current owner stops "heartbeating". Only the heir can pronounce the
+ * owner's death.
+ */
+contract Inheritable2 is Ownable {
+  address public heir;
+
+  // Time window the owner has to notify she is alive.
+  uint public heartbeatTimeout;
+
+  // Timestamp of the owner's death, as pronounced by the heir.
+  uint public timeOfDeath;
+
+
+  event OwnerPronouncedDead(address indexed owner, address indexed heir, uint indexed timeOfDeath);
+
+
+  /**
+   * @dev Throw an exception if called by any account other than the heir's.
+   */
+  modifier onlyHeir() {
+    require(msg.sender == heir);
+    _;
+  }
+
+
+  /**
+   * @notice Create a new Inheritable Contract with heir address 0x0.
+   * @param _heartbeatTimeout time available for the owner to notify she's alive,
+   * before the heir can take ownership.
+   */
+  function Inheritable(uint _heartbeatTimeout) public {
+    heartbeatTimeout = _heartbeatTimeout;
+  }
+
+  function setHeir(address newHeir) public onlyOwner {
+    heir = newHeir;
+  }
+
+  /**
+   * @dev set heir = 0x0
+   */
+  function removeHeir() public onlyOwner {
+    delete(heir);
+  }
+
+  function setHeartbeatTimeout(uint newHeartbeatTimeout) public onlyOwner {
+    require(ownerLives());
+    heartbeatTimeout = newHeartbeatTimeout;
+  }
+
+  /**
+   * @dev Heir can pronounce the owners death. To inherit the ownership, he will
+   * have to wait for `heartbeatTimeout` seconds.
+   */
+  function pronounceDeath() public onlyHeir {
+    require(ownerLives());
+    timeOfDeath = now;
+    OwnerPronouncedDead(owner, heir, timeOfDeath);
+  }
+
+  /**
+   * @dev Owner can send a heartbeat if she was mistakenly pronounced dead.
+   */
+  function heartbeat() public onlyOwner {
+    delete(timeOfDeath);
+  }
+
+  /**
+   * @dev Allows heir to transfer ownership only if heartbeat has timed out.
+   */
+  function inherit() public onlyHeir {
+    require(!ownerLives());
+    require(now >= timeOfDeath + heartbeatTimeout);
+    OwnershipTransferred(owner, heir);
+    owner = heir;
+    delete(timeOfDeath);
+  }
+
+  function ownerLives() internal returns (bool) {
+    return timeOfDeath == 0;
+  }
+}

+ 139 - 0
test/Inheritable.js

@@ -0,0 +1,139 @@
+'use strict'
+import { advanceBlock } from './helpers/advanceToBlock'
+import increaseTime from './helpers/increaseTime'
+import { increaseTimeTo, duration } from './helpers/increaseTime'
+import assertJump from './helpers/assertJump'
+
+
+const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'
+
+const Inheritable = artifacts.require('../contracts/ownership/Inheritable2.sol')
+
+contract('Inheritable', function(accounts) {
+  let inheritable
+  let owner
+
+  beforeEach(async function() {
+    inheritable = await Inheritable.new()
+    owner = await inheritable.owner()
+  })
+
+  it('should start off with an owner, but without heir', async function() {
+    const heir = await inheritable.heir()
+
+    assert.equal(typeof(owner), 'string')
+    assert.equal(typeof(heir), 'string')
+    assert.notStrictEqual(
+      owner, NULL_ADDRESS,
+      "Owner shouldn't be the null address"
+      )
+    assert.isTrue(
+      heir === NULL_ADDRESS,
+      "Heir should be the null address"
+    )
+   })
+
+  it('only owner should set heir', async function() {
+    const newHeir = accounts[1]
+    const someRandomAddress = accounts[2]
+    assert.isTrue(owner !== someRandomAddress)
+
+    await inheritable.setHeir(newHeir, {from: owner})
+    try {
+      await inheritable.setHeir(newHeir, {from: someRandomAddress})
+      assert.fail('should have thrown before')
+    } catch(error) {
+      assertJump(error)
+    }
+  })
+
+  it('owner can remove heir', async function() {
+    const newHeir = accounts[1]
+    await inheritable.setHeir(newHeir, {from: owner})
+    let heir = await inheritable.heir()
+
+    assert.notStrictEqual(heir, NULL_ADDRESS)
+    await inheritable.removeHeir()
+    heir = await inheritable.heir()
+    assert.isTrue(heir === NULL_ADDRESS)
+  })
+
+  it('owner can set heartbeatTimeout only if she\'s alive', async function() {
+    const newTimeout = 41414141
+    await inheritable.setHeartbeatTimeout(newTimeout, {from: owner})
+
+    assert.isTrue((await inheritable.heartbeatTimeout()).equals(new web3.BigNumber(newTimeout)))
+
+    const heir = accounts[1]
+    await inheritable.setHeir(heir, {from: owner})
+    await inheritable.pronounceDeath({from: heir})
+
+    try {
+      await inheritable.setHeartbeatTimeout(newTimeout, {from: owner})
+      assert.fail('should have thrown before')
+    } catch(error) {
+      assertJump(error)
+    }
+  })
+
+  it('heir can inherit only if owner is dead and timeout was reached', async function() {
+    const heir = accounts[1]
+    await inheritable.setHeir(heir, {from: owner})
+    await inheritable.setHeartbeatTimeout(4141, {from: owner})
+
+    try {
+      await inheritable.inherit({from: heir})
+      assert.fail('should have thrown before')
+    } catch(error) {
+      assertJump(error)
+    }
+
+    await inheritable.pronounceDeath({from: heir})
+    await increaseTime(1)
+    try {
+      await inheritable.inherit({from: heir})
+      assert.fail('should have thrown before')
+    } catch(error) {
+      assertJump(error)
+    }
+
+    await increaseTime(4141)
+    await inheritable.inherit({from: heir})
+
+  })
+
+  it('heir can\'t inherit if owner heartbeats', async function() {
+    const heir = accounts[1]
+    await inheritable.setHeir(heir, {from: owner})
+    await inheritable.setHeartbeatTimeout(4141, {from: owner})
+      
+    await inheritable.pronounceDeath({from: heir})
+    await inheritable.heartbeat({from: owner})
+    try {
+      await inheritable.inherit({from: heir})
+      assert.fail('should have thrown before')
+    } catch(error) {
+      assertJump(error)
+    }
+
+    await inheritable.pronounceDeath({from: heir})
+    await increaseTime(4141)
+    await inheritable.heartbeat({from: owner})
+    try {
+      await inheritable.inherit({from: heir})
+      assert.fail('should have thrown before')
+    } catch(error) {
+      assertJump(error)
+    }
+  })
+
+  it('should log owner dead and ownership transfer', async function() {
+    const heir = accounts[1]
+    await inheritable.setHeir(heir, {from: owner})
+    const { logs } = await inheritable.pronounceDeath({from: heir})
+    const event = logs.find(e => e.event === 'OwnerPronouncedDead')
+
+    assert.isTrue(event.args.owner === owner)
+    assert.isTrue(event.args.heir === heir)
+  })
+})