PBITBlogAboutFollow Me

Tontines Are Death Contracts

Cover Image for Tontines Are Death Contracts
PBIT
PBIT

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();
    }
}