// ========================================================================================= // Vesting — releases native STR to one beneficiary over time, with a cliff. // // WHAT IT IS FOR // A grantor funds this contract, then starts a schedule. From the cliff onwards the // beneficiary can draw the linearly vested share; after the end date they can draw the // rest. Nobody can draw it earlier and nobody can redirect it. // // WHAT IT GUARANTEES // * Nothing is payable before the cliff. The clock is the BLOCK's timestamp; there is no // time argument in the ABI, so there is no unlock date a caller can supply. // * Every release pays `vested - alreadyReleased`, never `vested`. The classic vesting bug // is paying the vested total on every call, which pays the whole schedule repeatedly. // * The sum of everything ever paid is exactly `total`, and never more, however many times // `release` is called and in whatever order. // * `release` pays the STORED beneficiary. Any caller may trigger it — a beneficiary // without gas is not a beneficiary who cannot be paid — but the money goes to one // address and there is no function that changes it. // * The schedule cannot be restarted, revoked, cancelled or accelerated. The grantor keeps // NO power after `start`. Read the ABI: there is no such function to call. // // THE IDENTITY RULE — CORRECTED 2026-09-08 // This block described a rule that no longer exists. `ValueJournal.receiveRefusal` used to // require a payment recipient to hold an ALLOCATED STR.domain or be a deployed contract; it // now refuses only Hnitbjorg reserved accounts. The gate became MORE permissive, so nothing // built against the old text is unsafe — but it instructed issuers to pre-flight a check that // no longer applies, and told them a payout would revert when it will not. // // Today: any well-formed, non-reserved address, or a deployed contract, may be paid. What a // recipient with no proved domain still cannot do is SPEND what it receives — a fact about // that holder, not about this contract. // // HOW IT IS FUNDED, AND THE LIMIT THAT FOLLOWS // A call record carries no value, so the grantor funds this contract with a PLAIN STR // TRANSFER first, and `start` then snapshots `balance(self)` as the granted total. The // contract cannot see who paid it or when, which is why the total has to be taken as a // snapshot rather than accumulated per deposit. // ANYTHING TRANSFERRED IN AFTER `start` IS STRANDED. It is not part of `total`, it is not // vested, and this contract has no sweep — deliberately, because a grantor sweep is a // revocation power wearing a support tool's clothes. Fund fully, check `grantTotal()`, // then start. // // HOW IT IS TYPICALLY MISUSED // * Starting before funding. `start` refuses a zero balance, but it cannot refuse a // PARTIAL one: it snapshots whatever is there. Check `grantTotal()` after starting and // before the beneficiary relies on it. // * Assuming the grantor can cancel. It cannot, and that is the point. If your agreement // needs revocation, this is the wrong template and the beneficiary must be able to see // that in the ABI before accepting the grant. // * Reading `height` and `timestamp` as interchangeable. This template uses time because a // vesting date is a date. For anything adversarial prefer height: a producer has some // latitude over the timestamp it writes and none over the height. // * Under-declaring the gas limit. MEASURED: start 1,971, release 3,105 during the linear // period and 2,890 after the end date. Declare at least 32,000. // ========================================================================================= contract Vesting { // The deployer. `caller` in a state initializer is the authenticated deployer, so there is // no claim race between deployment and setup. state grantor: address = caller; state beneficiary: address = ""; state startedAtMs: uint = 0; state cliffAtMs: uint = 0; state endsAtMs: uint = 0; state total: uint = 0; state released: uint = 0; state started: bool = false; event Started(beneficiary: address, total: uint, cliffAtMs: uint, endsAtMs: uint); event Released(to: address, amount: uint, releasedToDate: uint); // Fund the contract FIRST, then call this. The balance at this instant is the grant. fn start(who: address, cliffAfterMs: uint, durationMs: uint) -> uint { require(caller == grantor, "not the grantor"); require(started == false, "already started"); require(durationMs > 0, "duration must be positive"); require(cliffAfterMs <= durationMs, "cliff must be inside the schedule"); // THE GRANT IS LOST IF THIS LINE IS REMOVED. The value ledger refuses a contract that // pays itself, so a schedule whose beneficiary is this contract reverts on every // `release` forever — and there is no sweep, no restart and no revocation here by // design. MEASURED 2026-10-07: `start` accepted it, reported the grant total, emitted // `Started`, and every later release failed with `a contract cannot transfer to itself`. // Refused at the door, where the grantor can still fix it. require(who != self, "self is not payable"); let held = balance(self); require(held > 0, "fund the contract before starting"); beneficiary = who; startedAtMs = timestamp; cliffAtMs = timestamp + cliffAfterMs; endsAtMs = timestamp + durationMs; total = held; started = true; emit Started(who, held, cliffAtMs, endsAtMs); return held; } fn release() -> uint { require(started == true, "not started"); require(timestamp >= cliffAtMs, "cliff not reached"); // Vested-to-date. Multiplication happens before division so the integer division // truncates once, at the end; AresLang integers are arbitrary precision to 4096 bits, so // `total * elapsed` cannot wrap. let vested = total; if (timestamp < endsAtMs) { let elapsed = timestamp - startedAtMs; let span = endsAtMs - startedAtMs; vested = total * elapsed / span; } // The line the whole template turns on: pay the DIFFERENCE, not the vested total. let due = vested - released; require(due > 0, "nothing to release"); // Effects before the payment. released = released + due; transfer(beneficiary, due); emit Released(beneficiary, due, released); return due; } view fn beneficiaryIs() -> address { return beneficiary; } view fn grantTotal() -> uint { return total; } view fn releasedToDate() -> uint { return released; } view fn cliff() -> uint { return cliffAtMs; } view fn endsAt() -> uint { return endsAtMs; } view fn held() -> uint { return balance(self); } view fn hasStarted() -> bool { return started; } }