From 4fe2157e36e9292a7890d6a208ad0bcf95c5c32b Mon Sep 17 00:00:00 2001 From: zava Date: Thu, 23 Nov 2017 17:50:27 -0300 Subject: [PATCH] Inheritable contract --- contracts/ownership/Inheritable.sol | 91 ++++++++++++++++++ test/Inheritable.js | 139 ++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 contracts/ownership/Inheritable.sol create mode 100644 test/Inheritable.js diff --git a/contracts/ownership/Inheritable.sol b/contracts/ownership/Inheritable.sol new file mode 100644 index 000000000..b5a663b6d --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/test/Inheritable.js b/test/Inheritable.js new file mode 100644 index 000000000..14b49fe71 --- /dev/null +++ b/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) + }) +}) \ No newline at end of file