Heartbeat Milestone Escrow

heartbeat-milestone-escrow · conditional-settlement · 1972 B runtime · funds-movement

scores

source

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

contract HeartbeatMilestoneEscrow {
    address public immutable client;
    address public immutable agent;
    uint256 public immutable rate;
    uint256 public immutable startBlock;

    uint256 public lastHeartbeat;
    bool public closed;

    uint256 public constant WINDOW = 512;

    event Heartbeat(address indexed agent, uint256 blockNumber);
    event Closed(address indexed closer, uint256 agentAmount, uint256 clientAmount);

    constructor(address _agent, uint256 _rate) payable {
        require(_rate > 0, "rate zero");
        require(msg.value > 0, "no deposit");
        client = msg.sender;
        agent = _agent;
        rate = _rate;
        startBlock = block.number;
        lastHeartbeat = block.number;
    }

    modifier onlyAgent() {
        require(msg.sender == agent, "not agent");
        _;
    }

    modifier notClosed() {
        require(!closed, "closed");
        _;
    }

    function isStale() public view returns (bool) {
        return block.number > lastHeartbeat + WINDOW;
    }

    function heartbeat() external onlyAgent notClosed {
        require(!isStale(), "stale");
        lastHeartbeat = block.number;
        emit Heartbeat(agent, block.number);
    }

    function close() external notClosed {
        require(msg.sender == client || msg.sender == agent, "not party");
        if (msg.sender == client) {
            require(isStale(), "not stale");
        }
        uint256 endBlock = isStale() ? lastHeartbeat + WINDOW : block.number;
        uint256 elapsed = endBlock - startBlock;
        uint256 balance = address(this).balance;
        uint256 maxElapsed = balance / rate;
        if (elapsed > maxElapsed) elapsed = maxElapsed;
        uint256 agentAmount = rate * elapsed;
        uint256 clientAmount = balance - agentAmount;

        closed = true;

        if (agentAmount > 0) {
            (bool ok, ) = agent.call{value: agentAmount}("");
            require(ok, "agent transfer failed");
        }
        if (clientAmount > 0) {
            (bool ok, ) = client.call{value: clientAmount}("");
            require(ok, "client transfer failed");
        }
        emit Closed(msg.sender, agentAmount, clientAmount);
    }
}