258 lines
14 KiB
Solidity
258 lines
14 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
// Generated by the OSS Singularity contract lab (roadmap stage 06 first slice).
|
|
// SYNTHETIC EXAMPLE — Synthetic fair settlement example. Placeholder addresses, a digest
|
|
// over the public synthetic artifact and future deadlines; no funds, no
|
|
// deployment, no wallet. This contract records settlement states only: it is
|
|
// not an escrow, never receives or holds anything, and is deliberately
|
|
// not payable. The lab explains every requested signature; it never requests
|
|
// one. Nothing signs or deploys merely because this file exists.
|
|
pragma solidity ^0.8.24;
|
|
|
|
/// @title FairSettlement — a readable fair-settlement state machine.
|
|
/// @notice Mirrors the accepted settlement design note: the agreement comes
|
|
/// before the work and before any funds move. The happy path is
|
|
/// funded → delivered → accepted → released; either party may open a
|
|
/// dispute (delivered → disputed → resolved → released or refunded),
|
|
/// and cancellation plus every timeout end in a DEFINED outcome. The
|
|
/// real budget sits with a SEPARATELY AUTHORIZED holder — an escrow
|
|
/// instance or named human authority — that reads these records and
|
|
/// executes transfers in its own audited system. This contract never
|
|
/// holds, receives, or moves value of any kind. The Commons never
|
|
/// holds funds and never gains custody.
|
|
contract FairSettlement {
|
|
/// The settlement states, named exactly as the design note names them.
|
|
enum State {
|
|
none, // agreement recorded, funding not yet declared
|
|
funded, // the budget sits with the separately authorized holder
|
|
delivered, // a delivery receipt links the digest to the work
|
|
accepted, // the coordinator accepted the newest revision (release CONDITION)
|
|
disputed, // one party named the dispute path
|
|
resolved, // the holder recorded the dispute resolution
|
|
released, // terminal: settled to the contributor
|
|
refunded // terminal: settled back to the coordinator
|
|
}
|
|
|
|
// ---- Roles from the agreement (synthetic placeholders) ----
|
|
address public immutable contributor = 0x0000000000000000000000000000000000000001;
|
|
address public immutable coordinator = 0x0000000000000000000000000000000000000002;
|
|
// Placeholder for the separately authorized holder of the real budget.
|
|
// This contract records what the holder declares; custody itself is a
|
|
// separate, audited system with its own threat model.
|
|
address public immutable holder = 0x0000000000000000000000000000000000000003;
|
|
|
|
// ---- Delivery reference: sha256 over the delivered bytes ----
|
|
// The manifest digest of the synthetic artifact this example stands for.
|
|
bytes32 public constant DECLARED_DIGEST = 0x74aebc353c86c6269f80cc8509528ab19b3a1612ac3291bab40f32701454f69b;
|
|
|
|
// ---- Named deadlines: every waiting state has one ----
|
|
uint256 public constant REVIEW_DEADLINE = 1830000000; // delivery not reviewed → dispute path, never silent release
|
|
uint256 public constant DISPUTE_DEADLINE = 1832592000; // dispute unresolved → the agreement's fallback
|
|
uint256 public constant OUTER_DEADLINE = 1835184000; // from it (inclusive) the refund path has EXCLUSIVE priority
|
|
|
|
// ---- The fallback outcome the agreement fixed for a deadlocked dispute ----
|
|
bool public constant DISPUTE_FALLBACK_RELEASES = false; // refund
|
|
|
|
// ---- Settlement state: one path at a time, terminal states are final ----
|
|
State public state = State.none;
|
|
uint16 public currentRevision; // last delivered revision; 0 = none yet
|
|
uint16 public acceptedRevision; // 0 = nothing accepted yet
|
|
bool public resolutionReleases; // the holder's recorded dispute decision
|
|
|
|
event FundingRecorded(address indexed who);
|
|
event DeliveryRecorded(uint16 indexed revision, bytes32 digest, address indexed who);
|
|
event Accepted(uint16 indexed revision, address indexed who);
|
|
event DisputeOpened(string reason, address indexed who);
|
|
event DisputeResolved(bool releases, address indexed who);
|
|
event Released(address indexed who);
|
|
event Refunded(string reason, address indexed who);
|
|
|
|
error NotContributor();
|
|
error NotCoordinator();
|
|
error NotHolder();
|
|
error NotParty(address sender);
|
|
error WrongState(State current);
|
|
error DuplicateRevision(uint16 revision);
|
|
error NotNewestRevision(uint16 newest);
|
|
error DeadlinePassed(uint256 now_, uint256 deadline);
|
|
error DisputeWindowStillOpen(uint256 now_, uint256 deadline);
|
|
error OuterDeadlineNotPassed(uint256 now_, uint256 deadline);
|
|
error OuterDeadlinePassed(uint256 now_, uint256 deadline);
|
|
|
|
modifier onlyContributor() {
|
|
if (msg.sender != contributor) revert NotContributor();
|
|
_;
|
|
}
|
|
modifier onlyCoordinator() {
|
|
if (msg.sender != coordinator) revert NotCoordinator();
|
|
_;
|
|
}
|
|
modifier onlyHolder() {
|
|
if (msg.sender != holder) revert NotHolder();
|
|
_;
|
|
}
|
|
modifier onlyParty() {
|
|
if (msg.sender != contributor && msg.sender != coordinator) revert NotParty(msg.sender);
|
|
_;
|
|
}
|
|
|
|
/// @notice The holder records that the agreed budget sits with the
|
|
/// separately authorized holder. This declares funding; it never
|
|
/// receives anything.
|
|
/// @dev Signature (): no arguments. Reverts for anyone but the holder
|
|
/// or when funding was already declared.
|
|
function recordFunding() external onlyHolder {
|
|
if (state != State.none) revert WrongState(state);
|
|
state = State.funded;
|
|
emit FundingRecorded(msg.sender);
|
|
}
|
|
|
|
/// @notice The contributor records the next immutable delivery revision,
|
|
/// referencing the declared digest — the same delivery shape as
|
|
/// the coordination protocol.
|
|
/// @dev Signature (uint16): the new revision number, one above the
|
|
/// current. Reverts for anyone but the contributor, for a repeat
|
|
/// or skip, or when the settlement is not waiting for delivery.
|
|
function recordDelivery(uint16 revision) external onlyContributor {
|
|
if (state != State.funded) revert WrongState(state);
|
|
if (revision != currentRevision + 1) revert DuplicateRevision(revision);
|
|
currentRevision = revision;
|
|
state = State.delivered;
|
|
emit DeliveryRecorded(revision, DECLARED_DIGEST, msg.sender);
|
|
}
|
|
|
|
/// @notice The coordinator accepts exactly the newest revision before the
|
|
/// review deadline. Acceptance is the release CONDITION, never the
|
|
/// release itself. After the deadline the dispute path is the only
|
|
/// way forward — never a silent release.
|
|
/// @dev Signature (uint16): the revision being accepted. Reverts for
|
|
/// non-coordinators, stale revisions, a passed review deadline, or
|
|
/// when the settlement is not in the delivered state.
|
|
function accept(uint16 revision) external onlyCoordinator {
|
|
if (state != State.delivered) revert WrongState(state);
|
|
if (revision != currentRevision) revert NotNewestRevision(currentRevision);
|
|
if (block.timestamp > REVIEW_DEADLINE) revert DeadlinePassed(block.timestamp, REVIEW_DEADLINE);
|
|
acceptedRevision = revision;
|
|
state = State.accepted;
|
|
emit Accepted(revision, msg.sender);
|
|
}
|
|
|
|
/// @notice The holder executes the release its custody rules require once
|
|
/// acceptance has satisfied the release condition — before the
|
|
/// outer deadline. From the deadline instant itself on (the
|
|
/// moment the refund path opens), no new release can succeed:
|
|
/// refund has priority. The transfer itself happens in the
|
|
/// holder's separate, audited system; this record only marks
|
|
/// the outcome.
|
|
/// @dev Signature (): no arguments. Reverts for anyone but the holder,
|
|
/// when the settlement has not been accepted, or from the outer
|
|
/// deadline instant on (OuterDeadlinePassed — the refund window
|
|
/// and the release window are disjoint).
|
|
function release() external onlyHolder {
|
|
if (state != State.accepted) revert WrongState(state);
|
|
if (block.timestamp >= OUTER_DEADLINE) revert OuterDeadlinePassed(block.timestamp, OUTER_DEADLINE);
|
|
state = State.released;
|
|
emit Released(msg.sender);
|
|
}
|
|
|
|
/// @notice Either party opens the dispute path named in the agreement,
|
|
/// stating why. The holder then applies the agreed rules; the
|
|
/// service records outcomes only.
|
|
/// @dev Signature (string calldata): the reason the dispute was opened.
|
|
/// Reverts for anyone but the two parties or when nothing is
|
|
/// awaiting review.
|
|
function openDispute(string calldata reason) external onlyParty {
|
|
if (state != State.delivered) revert WrongState(state);
|
|
state = State.disputed;
|
|
emit DisputeOpened(reason, msg.sender);
|
|
}
|
|
|
|
/// @notice The holder records the resolution decided under the agreed
|
|
/// dispute rules, inside the dispute window. releases=true means
|
|
/// the work settles as delivered; false means refund.
|
|
/// @dev Signature (bool): which way the resolution goes. Reverts for
|
|
/// anyone but the holder, outside the disputed state, or after the
|
|
/// dispute deadline.
|
|
function resolveDispute(bool releases) external onlyHolder {
|
|
if (state != State.disputed) revert WrongState(state);
|
|
if (block.timestamp > DISPUTE_DEADLINE) revert DeadlinePassed(block.timestamp, DISPUTE_DEADLINE);
|
|
resolutionReleases = releases;
|
|
state = State.resolved;
|
|
emit DisputeResolved(releases, msg.sender);
|
|
}
|
|
|
|
/// @notice The holder executes the recorded resolution: released or
|
|
/// refunded, exactly as resolved. A releasing execution is
|
|
/// refused from the outer deadline instant on — the refund path
|
|
/// has priority from then on; a refunding execution still
|
|
/// reaches its (already refund) outcome. The transfer itself
|
|
/// stays in the holder's custody system.
|
|
/// @dev Signature (): no arguments. Reverts for anyone but the holder,
|
|
/// when no resolution has been recorded, or when the resolution
|
|
/// releases from the outer deadline instant on.
|
|
function executeResolution() external onlyHolder {
|
|
if (state != State.resolved) revert WrongState(state);
|
|
if (resolutionReleases && block.timestamp >= OUTER_DEADLINE) revert OuterDeadlinePassed(block.timestamp, OUTER_DEADLINE);
|
|
if (resolutionReleases) {
|
|
state = State.released;
|
|
emit Released(msg.sender);
|
|
} else {
|
|
state = State.refunded;
|
|
emit Refunded("dispute resolution", msg.sender);
|
|
}
|
|
}
|
|
|
|
// ---- Timeouts: every waiting state names a deadline and a default ----
|
|
// A timeout produces a DEFINED outcome; it never mints authority. The two
|
|
// paths below are therefore deliberately permissionless: they can only
|
|
// ever reach the outcome the agreement already fixed. From the outer
|
|
// deadline instant (inclusive) the refund path has EXCLUSIVE priority:
|
|
// every release-minting transition is refused exactly when the refund
|
|
// becomes available, so a release can never race the refund for the
|
|
// same state.
|
|
|
|
/// @notice Past the dispute window the agreed fallback replaces a
|
|
/// silent holder: the settlement takes the outcome fixed in the
|
|
/// agreement (refund) without needing anyone's cooperation —
|
|
/// unless that outcome is a release and the outer deadline has
|
|
/// been reached: the refund priority outranks even the agreed
|
|
/// fallback.
|
|
/// @dev Signature (): no arguments. Reverts while the settlement is not
|
|
/// disputed, while the dispute window is still open, or when the
|
|
/// fallback releases from the outer deadline instant on.
|
|
function applyDisputeFallback() external {
|
|
if (state != State.disputed) revert WrongState(state);
|
|
if (block.timestamp <= DISPUTE_DEADLINE) revert DisputeWindowStillOpen(block.timestamp, DISPUTE_DEADLINE);
|
|
if (DISPUTE_FALLBACK_RELEASES && block.timestamp >= OUTER_DEADLINE) revert OuterDeadlinePassed(block.timestamp, OUTER_DEADLINE);
|
|
if (DISPUTE_FALLBACK_RELEASES) {
|
|
state = State.released;
|
|
emit Released(msg.sender);
|
|
} else {
|
|
state = State.refunded;
|
|
emit Refunded("dispute fallback", msg.sender);
|
|
}
|
|
}
|
|
|
|
/// @notice From the outer deadline instant on (inclusive), funds
|
|
/// unreleased take the refund path from every non-terminal
|
|
/// state: the budget flows back rather than sitting withheld
|
|
/// indefinitely. From that moment on this is the only outcome —
|
|
/// every release path is closed.
|
|
/// @dev Signature (): no arguments. Reverts before the outer deadline
|
|
/// and on terminal states (nothing left to refund).
|
|
function refundAfterOuterDeadline() external {
|
|
if (state == State.none || state == State.released || state == State.refunded) revert WrongState(state);
|
|
if (block.timestamp < OUTER_DEADLINE) revert OuterDeadlineNotPassed(block.timestamp, OUTER_DEADLINE);
|
|
state = State.refunded;
|
|
emit Refunded("outer deadline", msg.sender);
|
|
}
|
|
|
|
/// @notice Cancellation before delivery: the agreement named cancellation,
|
|
/// and the holder records the budget flowing back unused.
|
|
/// @dev Signature (): no arguments. Reverts for anyone but the holder
|
|
/// or when delivery has already begun.
|
|
function refundOnCancellation() external onlyHolder {
|
|
if (state != State.funded) revert WrongState(state);
|
|
state = State.refunded;
|
|
emit Refunded("cancelled before delivery", msg.sender);
|
|
}
|
|
}
|