Tontines Are Death Contracts



tontines are an old-world financial idea where participants pool money, and as members die or drop out, the survivors split the pot. it's straightforward, morbid, and historically controversial. i’m experimenting with this concept on ethereum using a smart contract, but let me be clear upfront: i’ve never programmed a smart contract before. i don’t know the best practices for writing, testing, or deploying one. this project is an experiment, and you absolutely should not use this smart contract.
in my contract, participants deposit ethereum—say, 1 eth each. after a set period, everyone still in the game declares their intent to withdraw. those who declare split the pool. if someone doesn’t declare, they’re out. the remaining participants get a slightly larger share. once the pool is divided, the contract resets, and the process repeats indefinitely. imagine 100 users join and deposit 1 eth each. 5 fail to withdraw. now the remaining 95 split the 100 eth pool, each getting around 1.05 eth. the fewer people left, the bigger the payout for those who stay.
the appeal of tontines is their simplicity and the way they gamify mortality. in the traditional world, they’ve been banned in many places for legal and ethical reasons, but smart contracts sidestep centralized control, relying on code to enforce the rules. that’s part of what makes this such an interesting experiment: what happens when you take an old concept and run it on decentralized infrastructure?
financially incentivized longevity is an interesting idea. instead of just focusing on personal health for its own sake, you’d have a direct financial reward for staying alive. a tontine-like contract could encourage people to take their health and longevity more seriously, whether that’s through better choices, adopting new tech, or investing in anti-aging research. someone like bryan johnson (@bryan_johnson) could use something like this to align his goals with a system that rewards exactly what he’s working toward—living longer and proving it has tangible value.
// Tontine.sol
pragma solidity ^0.8.0;
contract Tontine {
mapping(address => uint256) public deposits; // tracks deposits of each user
mapping(address => bool) public hasInitiatedWithdraw; // tracks whether a user has initiated a withdrawal
uint256 public poolCurrent; // pool total tracks current amount of funds in the pool
uint public poolMax; // pool max tracks total amount added so that we can correctly calculate the percentage of the pool that each user is entitled to
uint256 public lockTime = block.timestamp + 1 weeks; // this is when deposits stop and the contract enters the Locked phase
uint256 public standardLockTime = 52 weeks; // this is the standard amount of time the contract will wait before entering the Declaration phase
uint256 public standardDeclarationTime = 1 weeks; // this is the standard amount of time the contract will wait before entering the Withdrawal phase
uint256 public standardWithdrawTime = 1 weeks; // this is the standard amount of time the contract will wait before entering the next cycle
event Deposit(address indexed user, uint256 amount); // emitted when a user deposits funds
event Withdraw(address indexed user, uint256 amount); // emitted when a user withdraws funds
event NewCycleStarted(uint256 newUnlockTime); // emitted when a new cycle is started
constructor() {
startNewCycle();
}
function startNewCycle() internal {
// Start a new cycle, reset the deposits, hasInitiatedWithdraw, and lockTime
lockTime = block.timestamp + 1 weeks;
// reset deposits to empty mapping
delete deposits;
// reset hasInitiatedWithdraw to empty mapping
delete hasInitiatedWithdraw;
poolMax = poolCurrent; // set the poolMax to the poolCurrent from the previous cycle
emit NewCycleStarted(lockTime);
}
// users deposit funds during the deposit phase
// their contribution to the pool is tracked in the deposits mapping so that
// we can calculate their share of the poolCurrent when they withdraw
function deposit() public payable {
require(block.timestamp < lockTime, "Deposit phase has ended");
require(deposits[msg.sender] == 0, "You have already deposited");
deposits[msg.sender] = msg.value;
poolCurrent += msg.value;
poolMax = poolCurrent;
emit Deposit(msg.sender, msg.value);
}
// users can initiate a withdrawal an ammount during the deposit phase if they change their mind about participating in this cycle
// this does not effect their hasInitiatedWithdraw status
function initiateEarlyWithdraw(uint256 amount) public {
require(block.timestamp < lockTime, "Deposit phase has ended");
require(deposits[msg.sender] > 0, "You have no funds to withdraw"); // checks to see if user has funds to withdraw
require(
amount <= deposits[msg.sender],
"You cannot withdraw more than you deposited"
);
payable(msg.sender).transfer(amount);
poolCurrent -= amount;
deposits[msg.sender] -= amount;
poolMax = poolCurrent;
emit Withdraw(msg.sender, amount);
}
// after the standardLockTime has passed and before the standardDeclarationTime has passed, users can declare their intent to withdraw
function declareWithdraw() public {
require(
block.timestamp > lockTime + standardLockTime,
"Too early to declare withdrawal"
);
require(
block.timestamp <
lockTime + standardLockTime + standardDeclarationTime,
"Too late to declare withdrawal"
);
require(deposits[msg.sender] > 0, "You have no funds to withdraw"); // checks to see if user has funds to withdraw
require(
!hasInitiatedWithdraw[msg.sender],
"Already initiated withdrawal"
);
hasInitiatedWithdraw[msg.sender] = true;
}
// after the standardDeclarationTime has passed and before the standardWithdrawTime has passed, users can withdraw their funds
function withdraw() public {
require(
block.timestamp >
lockTime + standardLockTime + standardDeclarationTime,
"Withdrawal time not yet started"
);
require(
hasInitiatedWithdraw[msg.sender],
"You must declare withdrawal first"
);
// this calculates the percentage of the poolCurrent that the user is entitled to via all deposits (poolMax)
uint256 userShare = (deposits[msg.sender] * poolMax) / poolMax;
// Adjust the total pool and user's deposit
poolCurrent -= userShare;
deposits[msg.sender] = 0;
// Reset the withdrawal initiation flag for this user
hasInitiatedWithdraw[msg.sender] = false;
payable(msg.sender).transfer(userShare);
emit Withdraw(msg.sender, userShare);
}
// start a new cycle after the standardWithdrawTime has passed
function startNewCycleAfterWithdrawal() public {
require(
block.timestamp >
lockTime +
standardLockTime +
standardDeclarationTime +
standardWithdrawTime,
"Too early to start new cycle"
);
startNewCycle();
}
}