Olga, a death-proof Ethereum wallet

This little side gig started from a conversation by the beach in July 2025. We were playing nomadicards (conversation cards fro digital nomads), and this question came up: "What would you do today if you knew you would die tomorrow?"

Answers started getting serious and rational. One obvious task on the to-do list was handling money inheritance, and a girl said: "I would give my Ethereum wallet key to my mom." That made me think that crypto inheritance is actually a real problem, and people don’t always get to know in advance when they are going to pass away.

I am aware that there are a few solutions that try to solve crypto inheritance in many ways, but since that day I've been thinking that—especially on Ethereum—this is a problem that can be solved 100% on-chain.

So, I wrote a smart contract for that and named it Olga (thanks for the inspiration, Olga), which can be used as a wallet or a safe-vault, and the owner is able to deposit, spend, and withdraw ETH at any time.

What makes it death-proof is the "consider me dead" logic, which works as follows: there is an "alive check" timestamp that gets automatically updated on every owner's activity, plus it can be manually updated if the contract is used as a dormant vault. After N years (defined upon creation) from the last activity, the owner is considered dead and the beneficiaries can take over the remaining balance.

The waiting time from the last activity can also be updated over time. For example, if you are young, you can set a generous 50 years, but if you are older or about to join a dangerous expedition somewhere in space, maybe it’s better to set it to two years :)

The list of beneficiaries can also be updated over time, just in case you break up with your partner or your best friend.

One thing to note is that your wallet stays yours, and the beneficiaries can only withdraw the funds and move them to a wallet they control—so they do not inherit your wallet, just your funds. After this final withdrawal, the contract gets locked forever, will reject any incoming transfers, and return your "final words," an epitaph, a final thought that you want to leave to this world, immutably... on-chain.

This is by far my favorite feature, and I am still thinking about what mine would be before deploying it :)

The beauty of Ethereum is that all of the above is wrapped into less than 100 lines of code. If you are a coder, you may want to check it out:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

contract Olga {
    address payable public immutable owner;
    address[] public beneficiaries;
    uint256 public unlockAfterYears;
    uint256 public lastAliveCheck;
    bool public lockedForever;
    string public epitaph;
    
    constructor(
        address[] memory _beneficiaries,
        uint256 _unlockAfterYears,
        string memory _epitaph
    ) {
        require(_unlockAfterYears > 0, "Abort: you need to define an unlock period");
        require(_beneficiaries.length > 0, "Abort: you need at least one beneficiary");

        owner = payable(msg.sender);
        beneficiaries = _beneficiaries;
        unlockAfterYears = _unlockAfterYears * 365 days;
        lastAliveCheck = block.timestamp;
        epitaph = _epitaph;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Abort: caller is not the owner");
        _;
    }

    /// @notice Ensures that the owner is considered dead (unlock period passed)
    modifier onlyIfDead() {
        require(block.timestamp > lastAliveCheck + unlockAfterYears, "Abort: owner is still alive");
        _;
    }

    /// @notice Prevents function execution if the contract has been permanently locked
    /// @dev `lockedForever` is set to true after final withdrawal
    modifier onlyIfNotLockedForever() {
        require(!lockedForever, epitaph);
        _;
    }

    /// @notice Restricts function access to addresses in the `beneficiaries` list
    /// @dev This is O(n) in the number of beneficiaries. Designed for small arrays (2–4). 
    ///      Not gas-optimal for large lists, but acceptable for the use case.
    modifier onlyBeneficiary() {
        bool isBeneficiary = false;
        for (uint i = 0; i < beneficiaries.length; i++) {
            if(beneficiaries[i] == msg.sender) {
                isBeneficiary = true;
                break;
            }
        }
        require(isBeneficiary, "Abort: not a beneficiary");
        _;
    }

    event Deposit(address indexed from, uint256 amount);
    event Withdraw(address indexed to, uint256 amount);
    event Transfer(address indexed to, uint256 amount);
    event LastAliveUpdated(uint256 timestamp);
    event UnlockInYearsUpdated(uint256 unlockYears);
    event BeneficiariesUpdated(address indexed beneficiary, string action);
    event GoodbyeWorld(string message);

    function transfer(address payable _to, uint256 _amount) external onlyOwner {
        require(address(this).balance >= _amount, "Abort: insufficient balance");

        updateLastAlive();

        (bool success, ) = _to.call{value: _amount}("");
        require(success, "Failed: ETH transfer failed");

        emit Transfer(_to, _amount);
    }

    function withdraw(uint256 _amount) external onlyOwner {
        require(_amount <= address(this).balance, "Abort: insufficient balance");

        updateLastAlive();

        (bool success, ) = owner.call{value: _amount}("");
        require(success, "Failed: ETH transfer failed");

        emit Withdraw(owner, _amount);
    }

    function finalWithdraw(address payable _to) external onlyBeneficiary onlyIfDead {
        uint256 balance = address(this).balance;

        (bool success, ) = _to.call{value: balance}("");
        require(success, "Failed: ETH transfer failed");

        lockedForever = true;

        emit Withdraw(_to, balance);
        emit GoodbyeWorld(epitaph);
    }

    function updateLastAlive() public onlyOwner {
        lastAliveCheck = block.timestamp;
        emit LastAliveUpdated(lastAliveCheck);
    }

    function updateUnlockYears(uint256 _unlockInYears) external onlyOwner {
        unlockAfterYears = _unlockInYears * 365 days;
        emit UnlockInYearsUpdated(_unlockInYears);
    }

    function addBeneficiary(address _beneficiary) external onlyOwner {
        beneficiaries.push(_beneficiary);

        emit BeneficiariesUpdated(_beneficiary, "added");
    }

    function removeBeneficiary(address _beneficiary) external onlyOwner {
        require(beneficiaries.length > 1, "Abort: cannot remove last beneficiary");

        for (uint i = 0; i < beneficiaries.length; i++) {
            if (beneficiaries[i] == _beneficiary) {
                beneficiaries[i] = beneficiaries[beneficiaries.length - 1];
                beneficiaries.pop(); // swap and pop

                emit BeneficiariesUpdated(_beneficiary, "removed");
                return;
            }
        }

        revert("Abort: beneficiary not found");
    }

    receive() external payable onlyIfNotLockedForever {
        emit Deposit(msg.sender, msg.value);
    }
}

Probabily the beneficiaries logic is not suitable for everyone, maybe you wanna leave a pre-defined % to different beneficiaries, or require all the beneficiaries to co-sign to unlock the funds (even if this would be riscky in case one loses his key) - and all these implementation would be doable, but I decided to keep Olga simple and open. You can set one single beneficiary or more, but the rationale is to assign people you trust.

On the GitHub repo you can find the code, mainnet/testnet addresses, and even a factory contract if you want to deploy your own.

To take the project further, a nice next step would be a simple UI that allows people to easily deploy their own Olga wallet (defining beneficiaries, unlock time, and the epitaph) and manage it (use funds, update beneficiaries). This wouldn’t be too hard; maybe one day I’ll come back to this blog post and link it up!

But for now, coding the smart contract logic was fun enough!

Until the next one,
Francesco