Skip to content

Commit 97164a5

Browse files
Merge pull request OpenZeppelin#680 from ajsantander/azavalla-feature/inheritable-contract
Azavalla feature/inheritable contract
2 parents 2cfb515 + 5868862 commit 97164a5

File tree

4 files changed

+282
-0
lines changed

4 files changed

+282
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
pragma solidity ^0.4.11;
2+
3+
import "../ownership/Heritable.sol";
4+
5+
6+
/**
7+
* @title SimpleSavingsWallet
8+
* @dev Simplest form of savings wallet whose ownership can be claimed by a heir
9+
* if owner dies.
10+
* In this example, we take a very simple savings wallet providing two operations
11+
* (to send and receive funds) and extend its capabilities by making it Heritable.
12+
* The account that creates the contract is set as owner, who has the authority to
13+
* choose an heir account. Heir account can reclaim the contract ownership in the
14+
* case that the owner dies.
15+
*/
16+
contract SimpleSavingsWallet is Heritable {
17+
18+
event Sent(address indexed payee, uint256 amount, uint256 balance);
19+
event Received(address indexed payer, uint256 amount, uint256 balance);
20+
21+
22+
function SimpleSavingsWallet(uint256 _heartbeatTimeout) Heritable(_heartbeatTimeout) public {}
23+
24+
/**
25+
* @dev wallet can receive funds.
26+
*/
27+
function () public payable {
28+
Received(msg.sender, msg.value, this.balance);
29+
}
30+
31+
/**
32+
* @dev wallet can send funds
33+
*/
34+
function sendTo(address payee, uint256 amount) public onlyOwner {
35+
require(payee != 0 && payee != address(this));
36+
require(amount > 0);
37+
payee.transfer(amount);
38+
Sent(payee, amount, this.balance);
39+
}
40+
}

contracts/ownership/Heritable.sol

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
pragma solidity ^0.4.11;
2+
3+
4+
import "./Ownable.sol";
5+
6+
7+
/**
8+
* @title Heritable
9+
* @dev The Heritable contract provides ownership transfer capabilities, in the
10+
* case that the current owner stops "heartbeating". Only the heir can pronounce the
11+
* owner's death.
12+
*/
13+
contract Heritable is Ownable {
14+
address public heir;
15+
16+
// Time window the owner has to notify they are alive.
17+
uint256 public heartbeatTimeout;
18+
19+
// Timestamp of the owner's death, as pronounced by the heir.
20+
uint256 public timeOfDeath;
21+
22+
event HeirChanged(address indexed owner, address indexed newHeir);
23+
event OwnerHeartbeated(address indexed owner);
24+
event OwnerProclaimedDead(address indexed owner, address indexed heir, uint256 timeOfDeath);
25+
event HeirOwnershipClaimed(address indexed previousOwner, address indexed newOwner);
26+
27+
28+
/**
29+
* @dev Throw an exception if called by any account other than the heir's.
30+
*/
31+
modifier onlyHeir() {
32+
require(msg.sender == heir);
33+
_;
34+
}
35+
36+
37+
/**
38+
* @notice Create a new Heritable Contract with heir address 0x0.
39+
* @param _heartbeatTimeout time available for the owner to notify they are alive,
40+
* before the heir can take ownership.
41+
*/
42+
function Heritable(uint256 _heartbeatTimeout) public {
43+
setHeartbeatTimeout(_heartbeatTimeout);
44+
}
45+
46+
function setHeir(address newHeir) public onlyOwner {
47+
require(newHeir != owner);
48+
heartbeat();
49+
HeirChanged(owner, newHeir);
50+
heir = newHeir;
51+
}
52+
53+
/**
54+
* @dev set heir = 0x0
55+
*/
56+
function removeHeir() public onlyOwner {
57+
heartbeat();
58+
heir = 0;
59+
}
60+
61+
/**
62+
* @dev Heir can pronounce the owners death. To claim the ownership, they will
63+
* have to wait for `heartbeatTimeout` seconds.
64+
*/
65+
function proclaimDeath() public onlyHeir {
66+
require(ownerLives());
67+
OwnerProclaimedDead(owner, heir, timeOfDeath);
68+
timeOfDeath = now;
69+
}
70+
71+
/**
72+
* @dev Owner can send a heartbeat if they were mistakenly pronounced dead.
73+
*/
74+
function heartbeat() public onlyOwner {
75+
OwnerHeartbeated(owner);
76+
timeOfDeath = 0;
77+
}
78+
79+
/**
80+
* @dev Allows heir to transfer ownership only if heartbeat has timed out.
81+
*/
82+
function claimHeirOwnership() public onlyHeir {
83+
require(!ownerLives());
84+
require(now >= timeOfDeath + heartbeatTimeout);
85+
OwnershipTransferred(owner, heir);
86+
HeirOwnershipClaimed(owner, heir);
87+
owner = heir;
88+
timeOfDeath = 0;
89+
}
90+
91+
function setHeartbeatTimeout(uint256 newHeartbeatTimeout) internal onlyOwner {
92+
require(ownerLives());
93+
heartbeatTimeout = newHeartbeatTimeout;
94+
}
95+
96+
function ownerLives() internal view returns (bool) {
97+
return timeOfDeath == 0;
98+
}
99+
}

test/Heritable.test.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import increaseTime from './helpers/increaseTime';
2+
import expectThrow from './helpers/expectThrow';
3+
4+
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
5+
6+
const Heritable = artifacts.require('../contracts/ownership/Heritable.sol');
7+
8+
contract('Heritable', function (accounts) {
9+
let heritable;
10+
let owner;
11+
12+
beforeEach(async function () {
13+
heritable = await Heritable.new(4141);
14+
owner = await heritable.owner();
15+
});
16+
17+
it('should start off with an owner, but without heir', async function () {
18+
const heir = await heritable.heir();
19+
20+
assert.equal(typeof (owner), 'string');
21+
assert.equal(typeof (heir), 'string');
22+
assert.notStrictEqual(
23+
owner, NULL_ADDRESS,
24+
'Owner shouldn\'t be the null address'
25+
);
26+
assert.isTrue(
27+
heir === NULL_ADDRESS,
28+
'Heir should be the null address'
29+
);
30+
});
31+
32+
it('only owner should set heir', async function () {
33+
const newHeir = accounts[1];
34+
const someRandomAddress = accounts[2];
35+
assert.isTrue(owner !== someRandomAddress);
36+
37+
await heritable.setHeir(newHeir, { from: owner });
38+
await expectThrow(heritable.setHeir(newHeir, { from: someRandomAddress }));
39+
});
40+
41+
it('owner can remove heir', async function () {
42+
const newHeir = accounts[1];
43+
await heritable.setHeir(newHeir, { from: owner });
44+
let heir = await heritable.heir();
45+
46+
assert.notStrictEqual(heir, NULL_ADDRESS);
47+
await heritable.removeHeir();
48+
heir = await heritable.heir();
49+
assert.isTrue(heir === NULL_ADDRESS);
50+
});
51+
52+
it('heir can claim ownership only if owner is dead and timeout was reached', async function () {
53+
const heir = accounts[1];
54+
await heritable.setHeir(heir, { from: owner });
55+
await expectThrow(heritable.claimHeirOwnership({ from: heir }));
56+
57+
await heritable.proclaimDeath({ from: heir });
58+
await increaseTime(1);
59+
await expectThrow(heritable.claimHeirOwnership({ from: heir }));
60+
61+
await increaseTime(4141);
62+
await heritable.claimHeirOwnership({ from: heir });
63+
assert.isTrue(await heritable.heir() === heir);
64+
});
65+
66+
it('heir can\'t claim ownership if owner heartbeats', async function () {
67+
const heir = accounts[1];
68+
await heritable.setHeir(heir, { from: owner });
69+
70+
await heritable.proclaimDeath({ from: heir });
71+
await heritable.heartbeat({ from: owner });
72+
await expectThrow(heritable.claimHeirOwnership({ from: heir }));
73+
74+
await heritable.proclaimDeath({ from: heir });
75+
await increaseTime(4141);
76+
await heritable.heartbeat({ from: owner });
77+
await expectThrow(heritable.claimHeirOwnership({ from: heir }));
78+
});
79+
80+
it('should log events appropriately', async function () {
81+
const heir = accounts[1];
82+
83+
const setHeirLogs = (await heritable.setHeir(heir, { from: owner })).logs;
84+
const setHeirEvent = setHeirLogs.find(e => e.event === 'HeirChanged');
85+
86+
assert.isTrue(setHeirEvent.args.owner === owner);
87+
assert.isTrue(setHeirEvent.args.newHeir === heir);
88+
89+
const heartbeatLogs = (await heritable.heartbeat({ from: owner })).logs;
90+
const heartbeatEvent = heartbeatLogs.find(e => e.event === 'OwnerHeartbeated');
91+
92+
assert.isTrue(heartbeatEvent.args.owner === owner);
93+
94+
const proclaimDeathLogs = (await heritable.proclaimDeath({ from: heir })).logs;
95+
const ownerDeadEvent = proclaimDeathLogs.find(e => e.event === 'OwnerProclaimedDead');
96+
97+
assert.isTrue(ownerDeadEvent.args.owner === owner);
98+
assert.isTrue(ownerDeadEvent.args.heir === heir);
99+
100+
await increaseTime(4141);
101+
const claimHeirOwnershipLogs = (await heritable.claimHeirOwnership({ from: heir })).logs;
102+
const ownershipTransferredEvent = claimHeirOwnershipLogs.find(e => e.event === 'OwnershipTransferred');
103+
const heirOwnershipClaimedEvent = claimHeirOwnershipLogs.find(e => e.event === 'HeirOwnershipClaimed');
104+
105+
assert.isTrue(ownershipTransferredEvent.args.previousOwner === owner);
106+
assert.isTrue(ownershipTransferredEvent.args.newOwner === heir);
107+
assert.isTrue(heirOwnershipClaimedEvent.args.previousOwner === owner);
108+
assert.isTrue(heirOwnershipClaimedEvent.args.newOwner === heir);
109+
});
110+
});

test/SimpleSavingsWallet.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
import expectThrow from './helpers/expectThrow';
3+
4+
const SimpleSavingsWallet = artifacts.require('../contracts/examples/SimpleSavingsWallet.sol');
5+
6+
contract('SimpleSavingsWallet', function (accounts) {
7+
let savingsWallet;
8+
let owner;
9+
10+
const paymentAmount = 4242;
11+
12+
beforeEach(async function () {
13+
savingsWallet = await SimpleSavingsWallet.new(4141);
14+
owner = await savingsWallet.owner();
15+
});
16+
17+
it('should receive funds', async function () {
18+
await web3.eth.sendTransaction({ from: owner, to: savingsWallet.address, value: paymentAmount });
19+
assert.isTrue((new web3.BigNumber(paymentAmount)).equals(web3.eth.getBalance(savingsWallet.address)));
20+
});
21+
22+
it('owner can send funds', async function () {
23+
// Receive payment so we have some money to spend.
24+
await web3.eth.sendTransaction({ from: accounts[9], to: savingsWallet.address, value: 1000000 });
25+
await expectThrow(savingsWallet.sendTo(0, paymentAmount, { from: owner }));
26+
await expectThrow(savingsWallet.sendTo(savingsWallet.address, paymentAmount, { from: owner }));
27+
await expectThrow(savingsWallet.sendTo(accounts[1], 0, { from: owner }));
28+
29+
const balance = web3.eth.getBalance(accounts[1]);
30+
await savingsWallet.sendTo(accounts[1], paymentAmount, { from: owner });
31+
assert.isTrue(balance.plus(paymentAmount).equals(web3.eth.getBalance(accounts[1])));
32+
});
33+
});

0 commit comments

Comments
 (0)