// ========================================================================================= // VestingToken — a ZK13 fungible token with a built-in per-beneficiary vesting schedule. // // WHAT IT IS FOR // A fungible asset whose ledger lives in this contract's own storage, with one extra power // the base token does not have: the mint authority can GRANT a beneficiary a total that // comes into existence gradually, linearly, over a window the grant fixes. It is the // `+vesting` profile of docs/ZK13_TOKEN_STANDARD.md — every view, every mutating function, // every event and every `require` message the base standard spells is here, spelled exactly, // plus `grant` and `release` and the five views that read a schedule back. This is the token // a project uses to pay a team or an investor on a cliffless linear release without a // separate escrow contract: the schedule is enforced by the code holders can read, not by a // spreadsheet somebody promises to honour. // // WHAT IT GUARANTEES // * Supply moves only through `mint`, `release` and `burn`. `transfer` moves it by exactly // zero. A grant does NOT create tokens: it records a commitment, and the tokens are minted // by `release` as they vest. So the circulating supply of a granted-but-unreleased schedule // is zero until the beneficiary pulls it. // * `maxSupply` bounds LIFETIME ISSUANCE and is immutable. The one counter it is checked // against is `committedEver` = everything ever MINTED plus everything ever GRANTED, and // `lifetimeIssued()` returns exactly that. A vesting grant is a promise to issue, so it // spends cap room the moment it is made, not when it is released — otherwise a schedule // could be granted against headroom that a direct mint had already taken, and `release` // would mint past the cap one claim at a time with nothing complaining. `committedEver` // only ever goes up; `burn` lowers circulating `totalSupply` and never touches it. // `maxSupply() == 0` means UNCAPPED, which is what this template ships as. // * The sum of everything a schedule ever pays is exactly its `total`, never more, however // many times `release` is called and in whatever order. Each `release` pays // `vestedNow - alreadyReleased`, never `vestedNow` — paying the vested total on every call // is the classic vesting bug and it pays the whole schedule repeatedly. The final release // trues up to exactly `total` because at or after the end the vested amount is // `total * duration / duration`, which is `total` with no remainder. // * A schedule is IMMUTABLE once granted. There is no `revoke`, no `cancel`, no `accelerate`, // no `setSchedule` and no second `grant` for the same beneficiary — the mint authority // keeps NO power over a grant after making it. Read the ABI: there is no such function to // call. The one thing the authority can still do is appoint a new authority or close // minting, neither of which reaches an existing schedule. // * `release` pays the CALLER's own schedule and mints into the caller's own balance. Unlike // the native `vesting.ares`, nobody can trigger it on a beneficiary's behalf, because a // mint credits `caller` and a contract cannot be `caller`. A beneficiary with no gas is a // beneficiary who has not yet released; the schedule waits, it does not expire. // * The owner can pause and can hand over ownership in two steps. The owner CANNOT move, // grant away, or release anybody else's balance or schedule: there is no `setBalance`, no // `adjust`, no `transferFrom`, no `releaseFor`. // * A revert leaves the ledger byte-identical. Storage commits only on success, so a refused // release rolls the `released` counter back with the mint that did not happen. // * A self-transfer is refused, so the classic debit-then-credit inflation bug cannot be // reached at all. // // THE CLOCK, AND WHAT THE SCHEDULE'S HONESTY DOES AND DOES NOT REST ON // `release` reads the BLOCK's timestamp (ms) and nothing else for its clock — there is no // time argument on `release`, so there is no unlock moment the beneficiary can supply. The // grantor DOES supply `startMs` and `durationMs` to `grant`, which is correct: defining the // schedule is the grantor's job and is done once, visibly, at grant time. A `startMs` in the // past vests immediately from that point; a `startMs` in the future is a cliff until then. // Either way it is fixed when the grant is made and no function changes it afterwards. // // What the vesting enforces is the RELEASE RATE: the contract will not mint faster than the // schedule. What it does not and cannot enforce is the BACKING — a grant is the mint // authority's commitment that the total it promised is a total it means to have issuable, and // `committedEver` is what holds the authority to that against the cap. There is no collateral // here and none is claimed. // // HOW IT IS TYPICALLY MISUSED // * Reading `granted(b)` as "already received". It is the WHOLE schedule; `released(b)` is // what has been minted so far and `vested(b)` is what could be released right now. A // beneficiary owed 1,000,000 over a year has `granted` 1,000,000 on day one and `balanceOf` // zero until they release. // * Expecting the grantor to be able to claw a grant back. 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. // * Granting to a contract address. A contract cannot be `caller`, so it can never call // `release`, so a grant to a contract address is a schedule nothing can ever draw — // PERMANENT and unrecoverable. The one case it can detect, its own address, it refuses. // * Treating "holds a balance" as "is a verified user". Receiving a ZK13 token is not gated // by STR.domain and cannot be — there is no domain accessor in AresLang. // * Deploying and forgetting to `closeMinting`, while telling holders the supply is fixed. // `minter()` is in the ABI and a holder will read it; so is every outstanding grant's // committed total, through `granted()` and `lifetimeIssued()`. // * Under-declaring the gas limit on the call record. Post-mining covers only // min(gasUsed, gasLimit / 10), so the network silently absorbs the rest, forever, on every // call. MEASURED, on the v2 gas table, with 53-character addresses: mint 1,144, // transfer 835, burn 774, grant 1,368, release 1,220, proposeOwner 412, // acceptOwnership 612, setPaused 363, setMinter 471, closeMinting 470. The most expensive // entry point is `grant` at 1,368. Declare a gas limit of at least 14,000. // // WHAT IT DELIBERATELY DOES NOT HAVE // No `approve` and no `transferFrom`: an allowance exists so another CONTRACT can spend on a // holder's behalf, and AresLang has no cross-contract calls. See ZK13_TOKEN_STANDARD.md §4.2. // No `revoke`, `cancel`, `accelerate` or `releaseFor`, for the reasons above. // No upgrade path and no proxy. What you deploy is what holders get, forever. // // A NOTE ON THE LINEAR ARITHMETIC // `vestedNow = total * min(now - start, duration) / duration`. The multiply happens before // the divide, so the integer division truncates exactly once, at the end. AresLang integers // are arbitrary precision (thousands of bits), so `total * elapsed` cannot wrap for any total // and duration a token will ever carry — a total below 2^128 and a duration below 2^64 give a // product below 2^192, orders of magnitude inside the limit. Intermediate releases may each // be one unit short of the true continuous value because of the truncation; the FINAL release // is exact, so the schedule always pays out to the last unit and the dust is never lost. // // TO MAKE THIS YOUR TOKEN // Edit the four constants below and nothing else. Changing them changes the bytecode and // therefore the deployed address, which is correct: a different token is a different contract. // Supply starts at zero; the deployer is owner and minter, so the first call after deployment // is `mint(founder, amount)` for the circulating part and `grant(beneficiary, total, startMs, // durationMs)` for each schedule. // ========================================================================================= contract VestingToken { // ---- edit these four ---------------------------------------------------------------- state tokenName: string = "Example Vesting Token"; state tokenSymbol: string = "EVEST"; // 13, like all seven native ledgers. A wallet showing two assets at different precision is // where somebody sends 10^5 times what they meant to. state tokenDecimals: uint = 13; // A ceiling on LIFETIME ISSUANCE, not on circulating supply — see the header. 0 means // uncapped, and must be declared as 0 rather than as a very large number that looks like a // cap. Immutable once deployed. SHIPPED AT 0, which is uncapped: a template cannot invent a // cap for a token it knows nothing about. If your holders are to be told the supply is // bounded, set the real number before you deploy — the address changes with it, which is // correct — or `closeMinting()` once the whole supply is minted or granted. state supplyCap: uint = 0; // ------------------------------------------------------------------------------------- state supply: uint = 0; // The ONE counter the cap is enforced against: everything ever MINTED plus everything ever // GRANTED, and it never goes down. `supply` cannot be the cap counter — `burn` lowers it — and // neither can a mint-only counter, because a grant is a commitment to mint later and a cap // that ignored commitments could be over-granted and then released past itself one claim at a // time. `release` does NOT add to this: the grant already counted its whole total here. state committedEver: uint = 0; // `caller` inside a state initializer is the DEPLOYER: Sindri runs the compiler-generated // initializer with the authenticated deployer as caller. That is why this token needs no // `claimOwnership` call and has no window in which a stranger could claim it. state contractOwner: address = caller; state pendingOwner: address = ""; state mintAuthority: address = caller; state frozen: bool = false; state balances: map; // The vesting layer, as four FLAT per-beneficiary maps rather than one map of a record: the // whole schedule is `grantTotal[b]`, `startMsOf[b]`, `durationMsOf[b]` and `releasedOf[b]`. // `grantTotal[b] == 0` is the single "no schedule" sentinel the whole layer keys off, so a // grant refuses a zero total and `release`/`vested` treat a zero total as nothing to pay. state grantTotal: map; state startMsOf: map; state durationMsOf: map; state releasedOf: map; // Transfer is the SINGLE record of every balance movement and there is no second one: a mint // and a release are both Transfer("", to, amount), a burn is Transfer(from, "", amount), and a // transfer is Transfer(from, to, amount). An indexer summing Transfer must arrive at exactly // the balances in storage, which is why a release emits one too. event Transfer(from: address, to: address, amount: uint); // Emitted by `grant` when a schedule is recorded. It is evidence of the commitment, not a // movement — no tokens exist yet — so it is NOT a Transfer. `release` emits the Transfer. event Granted(beneficiary: address, total: uint, startMs: uint, durationMs: uint); event OwnerProposed(current: address, pending: address); event OwnerChanged(previous: address, next: address); event PausedChanged(paused: bool, by: address); event MinterChanged(previous: address, next: address); // ---- views ---------------------------------------------------------------------------- // Which standard this contract was written against, and the profile of it. The `+vesting` // suffix is how a wallet learns, in one cheap read, that this token has a schedule layer with // a `grant`/`release` path. A self-declaration that proves nothing; the check is // `catalog(address)` against the standard's required surface. view fn standard() -> string { return "zk13-fungible/1+vesting"; } view fn name() -> string { return tokenName; } view fn symbol() -> string { return tokenSymbol; } view fn decimals() -> uint { return tokenDecimals; } view fn totalSupply() -> uint { return supply; } view fn maxSupply() -> uint { return supplyCap; } // Everything ever minted or granted, and it never goes down. This is the number the cap is // checked against — mint and grant both move it, release and burn do not — so a wallet reads // remaining mintable as `maxSupply() - lifetimeIssued()` and it is right whether or not any // grant has been released yet. view fn lifetimeIssued() -> uint { return committedEver; } view fn balanceOf(account: address) -> uint { return balances[account]; } // The WHOLE schedule for a beneficiary — the committed total, not what has been released. 0 // means no schedule. Read `released` and `vested` alongside it. view fn granted(account: address) -> uint { return grantTotal[account]; } // How much of a schedule has been minted to the beneficiary so far. view fn released(account: address) -> uint { return releasedOf[account]; } // The schedule's start, in ms, exactly as the grantor fixed it. view fn vestingStart(account: address) -> uint { return startMsOf[account]; } // The schedule's duration, in ms, exactly as the grantor fixed it. view fn vestingDuration(account: address) -> uint { return durationMsOf[account]; } // How much of a schedule has vested as of THIS block's timestamp: the amount a `release` // called now would bring the beneficiary's released total up to. `vested - released` is what // the next release would pay. Returns 0 for an address with no schedule and before the start. view fn vested(account: address) -> uint { let total = grantTotal[account]; if (total == 0) { return 0; } // `grant` guarantees a non-zero total has a non-zero duration, so the divide below is safe. let duration = durationMsOf[account]; let start = startMsOf[account]; let elapsed = 0; // Nothing before the start; `timestamp - start` would be a checked subtract that reverts on // a negative, so it is only taken when the clock has passed the start. if (timestamp > start) { elapsed = timestamp - start; } if (elapsed > duration) { elapsed = duration; } // Multiply before divide: the truncation happens once, at the end, and at or past the end // `elapsed == duration` makes this exactly `total`. return total * elapsed / duration; } view fn owner() -> address { return contractOwner; } // "" means minting is closed permanently. Required even though this token does mint: a holder // must be able to ask "who can mint this?" and get an answer from the ABI. Closing minting // also closes granting — a grant is a mint authority power — so an existing schedule can still // be released but no new one can be made. view fn minter() -> address { return mintAuthority; } view fn paused() -> bool { return frozen; } view fn pendingOwnerIs() -> address { return pendingOwner; } // ---- movement ------------------------------------------------------------------------- fn transfer(to: address, amount: uint) -> bool { require(frozen == false, "paused"); // Unreachable through a call record — the ABI check refuses a malformed `address` argument // before the first opcode runs — and kept because it is the standard's rule. require(len(to) > 0, "empty recipient"); // The guard costs 8 gas and makes a future caching edit harmless; see zk13-token.ares §6.6. require(to != caller, "self transfer"); // A balance recorded at a contract address can never move, per standard section 7.3. This is // the one case of it the token can cheaply refuse outright. require(to != self, "self is not payable"); require(balances[caller] >= amount, "insufficient balance"); // uint - uint compiles to the CHECKED subtract, which reverts rather than producing a // negative that every later comparison would read as "plenty". balances[caller] = balances[caller] - amount; balances[to] = balances[to] + amount; // A zero-amount transfer is permitted and still emits: refusing it breaks batch payers that // send a list including zeros. emit Transfer(caller, to, amount); return true; } // The caller burns its OWN balance and no other. With no allowance mechanism there is no // authority by which anyone could burn somebody else's, and inventing one is confiscation. fn burn(amount: uint) -> bool { require(frozen == false, "paused"); require(balances[caller] >= amount, "insufficient balance"); balances[caller] = balances[caller] - amount; supply = supply - amount; emit Transfer(caller, "", amount); return true; } fn mint(to: address, amount: uint) -> bool { require(frozen == false, "paused"); require(len(mintAuthority) > 0, "mint closed"); require(caller == mintAuthority, "not minter"); require(len(to) > 0, "empty recipient"); // A position at this contract's own address could never move again: a contract cannot be // `caller`, and the value ledger refuses a contract that pays itself. require(to != self, "self is not payable"); // Refuses at the cap rather than truncating. The cap is tested against `committedEver`, // which a grant also moves, so a direct mint and a vesting commitment share one ceiling. if (supplyCap > 0) { require(committedEver + amount <= supplyCap, "supply cap"); } committedEver = committedEver + amount; supply = supply + amount; balances[to] = balances[to] + amount; emit Transfer("", to, amount); return true; } // ---- the vesting layer ---------------------------------------------------------------- // Records an IMMUTABLE schedule for a beneficiary. The mint authority commits `total` to be // released linearly from `startMs` over `durationMs`; the tokens are NOT minted here — they // come into existence as the beneficiary releases them. The commitment spends cap room at // once, through `committedEver`, because a grant that did not would let the cap be beaten by // granting and minting against the same headroom. There is no revoke, cancel or accelerate, // and a beneficiary already holding a schedule cannot be granted a second one — "grant exists" // protects the first grant's terms from being overwritten. fn grant(beneficiary: address, total: uint, startMs: uint, durationMs: uint) { require(frozen == false, "paused"); require(len(mintAuthority) > 0, "mint closed"); require(caller == mintAuthority, "not minter"); require(len(beneficiary) > 0, "empty recipient"); // A schedule to this contract's own address could never be released — a contract is never // `caller` — so it would be a permanent, unrecoverable commitment. Refused at the door. require(beneficiary != self, "self is not payable"); require(total > 0, "nothing to grant"); require(durationMs > 0, "duration must be positive"); // Immutable: a second grant would silently rewrite the first one's total, start and clock. require(grantTotal[beneficiary] == 0, "grant exists"); // The commitment respects the same cap a mint does, counted the same way. if (supplyCap > 0) { require(committedEver + total <= supplyCap, "supply cap"); } committedEver = committedEver + total; grantTotal[beneficiary] = total; startMsOf[beneficiary] = startMs; durationMsOf[beneficiary] = durationMs; emit Granted(beneficiary, total, startMs, durationMs); } // The BENEFICIARY pulls their newly-vested portion. It mints `vested - released` into the // caller's own balance and nowhere else, so nobody can release on another's behalf and the // mint authority has no `releaseFor`. Pays the DIFFERENCE and never the vested total — paying // the total on every call is the classic bug that pays the whole schedule repeatedly. The sum // of everything a schedule ever pays is exactly its `total`, because the final release lands // when `elapsed == duration` and vests `total` with no remainder. fn release() -> uint { require(frozen == false, "paused"); let total = grantTotal[caller]; require(total > 0, "no grant"); let duration = durationMsOf[caller]; let start = startMsOf[caller]; let elapsed = 0; // Nothing before the start; the checked subtract is only taken once the clock has passed it. if (timestamp > start) { elapsed = timestamp - start; } if (elapsed > duration) { elapsed = duration; } // Multiply before divide — one truncation, at the end — and `total > 0` guarantees // `duration > 0`, so the divide cannot be by zero. let vestedNow = total * elapsed / duration; let alreadyReleased = releasedOf[caller]; let claimable = vestedNow - alreadyReleased; require(claimable > 0, "nothing to release"); // Effects before the mint, and the mint is an ordinary credit of this token. A revert rolls // the released counter back with the supply that was not created. releasedOf[caller] = alreadyReleased + claimable; supply = supply + claimable; balances[caller] = balances[caller] + claimable; emit Transfer("", caller, claimable); return claimable; } // ---- powers the deployer keeps, all of them visible in the ABI -------------------------- fn proposeOwner(next: address) { require(caller == contractOwner, "not owner"); pendingOwner = next; emit OwnerProposed(contractOwner, next); } // Two steps, because a one-step handover to a mistyped address is permanent: there is no // administrator anywhere in SourceLess who can reach into a contract's slots. fn acceptOwnership() { require(caller == pendingOwner, "not the pending owner"); emit OwnerChanged(contractOwner, caller); contractOwner = caller; pendingOwner = ""; } fn setPaused(next: bool) { require(caller == contractOwner, "not owner"); frozen = next; emit PausedChanged(next, caller); } fn setMinter(next: address) { require(caller == contractOwner, "not owner"); // Minting, once closed, stays closed. A fixed supply that can be un-fixed is not one. require(len(mintAuthority) > 0, "mint closed"); emit MinterChanged(mintAuthority, next); mintAuthority = next; } // One-way. This closes both minting and granting permanently. There is no argument because an // `address` argument cannot be "": the ABI check refuses it before the first opcode. fn closeMinting() { require(caller == contractOwner, "not owner"); require(len(mintAuthority) > 0, "mint closed"); emit MinterChanged(mintAuthority, ""); mintAuthority = ""; } }