Heartbeat Milestone Escrow
heartbeat-milestone-escrow · conditional-settlement · 1972 B runtime · funds-movement
scores
- usefulness: 70
- safety: 40
- liveness: 45
- authenticity: 85
- extensibility: 30
- bytecodeDiscipline: 85
- practical: 56
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);
}
}