height 23745 of 23745 on the network. 9 peer(s) connected, 77 in its peer directory; 1041 block(s) followed and 865 pulled in by sync since this follower started. Caught up: this follower holds every block any peer has claimed, so every panel below is the whole chain. Genesis 1cf8655125cc1bbb…, producers genesis.
What this follower is, locally
Following 7 seed(s) — their addresses are not published here, for the reason the peer table gives. Its store is held under its own Cerberus lease — no other node’s store is opened, touched or stopped.
ZK13Vault for your fork
- Status
- a draft, not deployed nothing here is on any chain; never send value to a draft
- Profile
- ZK13 vault, the reference
- Holds
- an asset ledger and a share ledger
- The property that matters
- every rounding goes to the pool; redeem is not pausable
- On Asgard
- the same template in the catalogue
Edit the draft
Set the constants the template marks for editing, and name a deployer to see where it would land.
The draft as shipped
- Contract
ZK13Vault- Compiler
areslang-1.3.0- Bytecode SHA-256
4a17724cb676e19a69569ad229cfb8d25bc750efa3f8d966bba0845a6f1b3c58- Bytecode
- 5,742 bytes
- Source SHA-256
1272e405c119fe84f13105c070620cba655a715c5b0fc6a0a6fa9de43e958ea3- The catalogue text?
- Yes, byte for byte: matches the published vector
- Ready to deploy?
- not yet a deployment refuses while these remain: tokenName = "Example ZK13 Vault Asset", tokenSymbol = "EZV"
- Would land at
- name a deployer to see it
Deploy it to your fork
Only the last step signs, and only it reaches a node.
- Save the draft. Download it above, or start from the catalogue text with
strblk contract template zk13-vault > zk13-vault.aresand edit the marked constants yourself. - Dry-run it on your machine.
strblk wallet deploy zk13-vault.ares --dry-run --address <your address on the fork>compiles it locally and prints its digest and address. They must match this page; if they do not, the file is not this draft. - Find your fork's RPC. It is the
rpc (Iris, wallets)line of the fork's banner, and therpcUrlof each node in<data root>/fork.json. - Deploy it.
strblk wallet deploy zk13-vault.ares --node <your fork's RPC>, from a wallet whose account holds a proved STR.domain on your fork. Before it signs, the wallet prints the network: the realm reads unregistered and the genesis is your fork's hash. Check that hash; it is what the signature binds.
Measured gas
Charged by the STARW VM under areslang-1.3.0, the same on your fork. Measured, not estimated.
| Entry point | Gas |
|---|---|
| mintShares | 1,582 |
| withdraw | 1,531 |
| redeem | 1,488 |
| deposit | 1,484 |
| mint | 1,144 |
| fundYield | 836 |
| transferShares | 835 |
- Declare at least
- 15,820 ten times the most expensive call
Functions
| Kind | Name | Inputs | Outputs | Mutability |
|---|---|---|---|---|
| function | standard | string | view | |
| function | name | string | view | |
| function | symbol | string | view | |
| function | decimals | uint | view | |
| function | totalSupply | uint | view | |
| function | maxSupply | uint | view | |
| function | lifetimeIssued | uint | view | |
| function | balanceOf | account: address | uint | view |
| function | totalAssets | uint | view | |
| function | totalShares | uint | view | |
| function | shareBalanceOf | account: address | uint | view |
| function | shareDecimals | uint | view | |
| function | convertToShares | assets: uint | uint | view |
| function | convertToAssets | shareAmount: uint | uint | view |
| function | previewDeposit | assets: uint | uint | view |
| function | previewRedeem | shareAmount: uint | uint | view |
| function | previewMintShares | shareAmount: uint | uint | view |
| function | previewWithdraw | assets: uint | uint | view |
| function | maxRedeem | account: address | uint | view |
| function | maxWithdraw | account: address | uint | view |
| function | owner | address | view | |
| function | minter | address | view | |
| function | paused | bool | view | |
| function | pendingOwnerIs | address | view | |
| function | deposit | assets: uint | uint | nonpayable |
| function | mintShares | shareAmount: uint | uint | nonpayable |
| function | redeem | shareAmount: uint | uint | nonpayable |
| function | withdraw | assets: uint | uint | nonpayable |
| function | fundYield | amount: uint | bool | nonpayable |
| function | transferShares | to: address, amount: uint | bool | nonpayable |
| function | transfer | to: address, amount: uint | bool | nonpayable |
| function | burn | amount: uint | bool | nonpayable |
| function | mint | to: address, amount: uint | bool | nonpayable |
| function | proposeOwner | next: address | nonpayable | |
| function | acceptOwnership | nonpayable | ||
| function | setPaused | next: bool | nonpayable | |
| function | setMinter | next: address | nonpayable | |
| function | closeMinting | nonpayable | ||
| event | Transfer | from: address, to: address, amount: uint | ||
| event | ShareTransfer | from: address, to: address, amount: uint | ||
| event | Deposit | owner: address, assets: uint, sharesMinted: uint | ||
| event | Withdraw | owner: address, assets: uint, sharesBurned: uint | ||
| event | YieldFunded | from: address, assets: 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 |
Your draft's source
templates/zk13-vault.ares, as shipped
// =========================================================================================
// ZK13Vault — the reference share-accounted vault for SourceLess.
//
// WHAT IT IS FOR
// A pool of one asset, owned pro rata by holders of a SHARE. You deposit assets and receive
// shares; the pool grows when anyone funds yield into it; you redeem shares for assets at
// whatever the pool is worth then. This is the reference implementation of
// docs/ZK13_VAULT_STANDARD.md — the ERC-4626 shape, written for a chain that has no
// cross-contract calls.
//
// WHY THE ASSET AND THE VAULT ARE ONE CONTRACT, AND NOT TWO
// Because they have to be. ERC-4626 is a wrapper: the vault holds a BALANCE IN ANOTHER
// CONTRACT and calls `transferFrom` on it. AresLang has no cross-contract calls, so a
// separate vault could never move a holder's token balance — and a balance recorded at a
// vault contract's address could never move again at all, because a contract cannot be
// `caller`. A vault over a ZK13 token is therefore only writable INSIDE the token contract.
// This is that: the asset ledger and the share ledger are two maps in one contract.
//
// The same reasoning is why this does not vault native STR. A depositor would have to send
// STR with a plain transfer, and THE CONTRACT CANNOT SEE WHO PAID IT — a call record carries
// no value and there is no deposit hook. Every per-depositor native pattern is unwritable
// for that one reason, and no amount of contract code fixes it.
//
// THE TWO LEDGERS, AND WHICH VIEW ANSWERS FOR WHICH
// asset — `balanceOf`, `totalSupply`, `transfer`, `mint`, `burn`, `decimals`. The ZK13
// fungible token this contract issues. Deposited assets LEAVE the depositor's
// balance and are held in the pool, which is `totalAssets()` and NOT a balance
// entry: the invariant is
// totalSupply() == (the sum of every balanceOf) + totalAssets()
// and there is no address at which the pooled assets appear.
// share — `shareBalanceOf`, `totalShares`, `transferShares`, `shareDecimals`. A claim on
// the pool. Shares are transferable, so a share is a bearer claim: whoever holds
// it can redeem it.
//
// WHAT IT GUARANTEES
// * Every rounding goes to the pool, never to the caller. `deposit` and `redeem` round DOWN
// what the caller gets; `withdraw` rounds UP the shares it burns and `mintShares` rounds UP
// the assets it takes. A caller can therefore lose at most one minor unit to rounding per
// call and can never gain one, which is the property that keeps a vault from being drained a
// unit at a time by a loop.
// * Redeeming is NOT gated by the pause, deliberately, and neither is withdrawing. The owner
// can stop deposits, funding, asset transfers and minting; the one thing the owner cannot
// do is stand between a holder and their own share of the pool.
// * A share can only be created by `deposit` or `mintShares`, both of which take assets for
// it, and only destroyed by `redeem`/`withdraw`. No function gives a share to anybody,
// including the owner. There is no `setShares`.
// * The pool can only grow by `deposit`, `mintShares` and `fundYield`, and can only shrink by
// `redeem` and `withdraw`. The owner has no path to the pooled assets at all — no fee, no
// skim, no sweep. Read the ABI: nothing moves `vaultAssets` anywhere but to a redeemer.
// * `fundYield` refuses while no shares are outstanding, so a donation cannot land where
// nobody can ever claim it.
// * A revert leaves both ledgers byte-identical. Storage commits only on success.
//
// THE INFLATION ATTACK, AND WHAT THE OFFSET ACTUALLY BUYS
// The classic attack on a share vault: be the first depositor with 1 unit, donate a large
// amount straight into the pool, and every later depositor's share count rounds down to
// zero or near it — their assets become yours. The defence here is the virtual offset:
// pricing runs against `totalShares + virtualShares` over `totalAssets + 1`, so the pool
// behaves as if it already held ten million phantom shares against one phantom unit.
//
// MEASURED on this contract. The attacker deposits 1 unit (10,000,000 shares), donates 1,000
// into the pool, and the victim then deposits 1,000 and receives 19,960,079 shares. Pool
// 2,001 assets against 29,960,079 real shares. `maxWithdraw` afterwards: victim 999 against
// the 1,000 deposited, attacker 501 against the 1,001 they put in. **The attack costs the
// attacker 500 to deprive the victim of 1.** The missing 501 is not the attacker's either —
// it backs the phantom shares, and no holder present at that moment can withdraw it.
//
// THE PRICE OF THE DEFENCE, MEASURED RATHER THAN GUESSED AT. The phantom shares claim a
// fraction of every increase in the pool, and only of the increase:
// * NO YIELD FUNDED — a full exit returns the whole pool. Deposit 1,000, redeem every
// share, receive 1,000, `totalAssets()` 0. The offset cancels exactly, because it sits
// on both sides of the ratio.
// * YIELD FUNDED — the phantom shares keep `virtualShares / (totalShares + virtualShares)`
// of it. Deposit 1,000, fund 500 of yield, redeem every share: receive 1,499 and ONE
// UNIT stays in a pool with no shares outstanding. That is the whole of the leakage at
// this offset — a thousandth of the yield, not of the principal.
// * DONATION-INFLATED — whatever the attack above bought. 501 of 2,001 in that run, which
// is the attacker's money and is exactly why the attack does not pay.
// The residue is not destroyed: it prices into the pool for whoever deposits next. What it
// is not is claimable by the holders who were there when it appeared. If your accounting
// needs the pool to reach exactly zero after any yield, this is not the contract for it.
//
// The claim above is a measurement of this code, not a proof: it says the cheap version of
// this attack is self-defeating here, not that no pricing attack exists. An attacker willing
// to donate ten million times a victim's deposit can still round that victim down, and would
// be paying ten million times the victim's deposit to do it.
//
// HOW IT IS TYPICALLY MISUSED
// * Reading a share count as an asset count. They are different units at different scales —
// `shareDecimals()` is `decimals()` plus seven, because the opening price is ten million
// shares to one asset unit. Quote a position with `convertToAssets`, never with the raw
// share number.
// * Expecting yield. Nothing in this contract generates any. `fundYield` is the ONLY way the
// pool grows other than deposits, and it is a voluntary payment by whoever calls it. A
// vault whose yield nobody funds is a vault that pays exactly what was put in, minus
// rounding. Do not deploy this and call it an interest-bearing account.
// * Sending assets or shares to a contract address. A contract cannot be `caller`, so either
// is a PERMANENT LOSS. This template refuses the one case an address check can detect —
// its own address — and cannot detect any other.
// * Forgetting that shares are transferable. Whoever holds a share can redeem it; there is
// no lock, no vesting and no recovery. If you need a non-transferable claim, remove
// `transferShares` before you deploy — the address changes with it, which is correct.
// * 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: deposit 1,484,
// mintShares 1,582, withdraw 1,531, redeem 1,488, fundYield 836, transferShares 835,
// mint 1,144, transfer 835, burn 774, proposeOwner 412, acceptOwnership 612,
// setPaused 363, setMinter 471, closeMinting 470. Declare a gas limit of at least 16,000.
//
// TO MAKE THIS YOUR VAULT
// Edit the constants below and nothing else. Supply starts at zero and the deployer is owner
// and minter, so the sequence after deployment is `mint(holder, amount)` and then the holder
// calls `deposit(amount)`.
// =========================================================================================
contract ZK13Vault {
// ---- edit these ----------------------------------------------------------------------
state tokenName: string = "Example ZK13 Vault Asset";
state tokenSymbol: string = "EZV";
// 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 of the ASSET, not on circulating supply, and not on the
// pool. 0 means uncapped, which is what this ships as.
state supplyCap: uint = 0;
// -------------------------------------------------------------------------------------
// THESE TWO MUST AGREE: virtualShares is ten to the power of shareOffsetDigits. AresLang has
// no exponentiation, so the value and its exponent are two constants, and nothing in the
// machine checks that they match. If you change one, change the other — the first is what
// the pricing uses and the second is only ever reported to a wallet, so a mismatch does not
// break the arithmetic. It misprices every display of every position, which is worse,
// because it is invisible from inside the contract.
state virtualShares: uint = 10000000;
state shareOffsetDigits: uint = 7;
state supply: uint = 0;
// Everything `mint` has ever created, and it never goes down. This — not `supply`, which
// `burn` lowers — is what `supplyCap` bounds: a cap tested against a burnable number is
// defeated by mint, burn, mint, which issues without limit against a cap that never appears
// to be breached.
state issuedEver: uint = 0;
// `caller` inside a state initializer is the DEPLOYER: Sindri runs the compiler-generated
// initializer with the authenticated deployer as caller, so there is no claim race.
state contractOwner: address = caller;
state pendingOwner: address = "";
state mintAuthority: address = caller;
state frozen: bool = false;
// The pool. Not a balance entry at any address — see the header's invariant.
state vaultAssets: uint = 0;
state shareSupply: uint = 0;
state balances: map<address, uint>;
state shares: map<address, uint>;
// Transfer is the single record of every ASSET movement between addresses: a mint is
// Transfer("", to, amount) and a burn is Transfer(from, "", amount). Movements into and out
// of the POOL are Deposit and Withdraw instead, because they are not movements between two
// addresses and an indexer summing Transfer must not see them twice.
event Transfer(from: address, to: address, amount: uint);
event ShareTransfer(from: address, to: address, amount: uint);
event Deposit(owner: address, assets: uint, sharesMinted: uint);
event Withdraw(owner: address, assets: uint, sharesBurned: uint);
event YieldFunded(from: address, assets: 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);
// ---- asset views: the ZK13 fungible interface, answering for the asset ledger ----------
// Which standards this contract was written against — both of them, because it is a
// conforming ZK13 fungible token AND a conforming vault over itself. A SELF-DECLARATION that
// proves nothing; a client that needs certainty checks `catalog(address)` against each
// standard's required surface.
view fn standard() -> string {
return "zk13-vault/1+zk13-fungible/1";
}
view fn name() -> string {
return tokenName;
}
view fn symbol() -> string {
return tokenSymbol;
}
view fn decimals() -> uint {
return tokenDecimals;
}
// Every asset unit in existence, INCLUDING the ones held in the pool. The pool is not an
// address, so `balanceOf` cannot see them; `totalAssets()` is where they are.
view fn totalSupply() -> uint {
return supply;
}
view fn maxSupply() -> uint {
return supplyCap;
}
view fn lifetimeIssued() -> uint {
return issuedEver;
}
view fn balanceOf(account: address) -> uint {
return balances[account];
}
// ---- vault views ------------------------------------------------------------------------
// What the pool holds. The denominator of every price in this contract.
view fn totalAssets() -> uint {
return vaultAssets;
}
view fn totalShares() -> uint {
return shareSupply;
}
view fn shareBalanceOf(account: address) -> uint {
return shares[account];
}
// Asset decimals plus the offset, because one asset unit opens at ten million shares. A
// wallet that renders a share count with `decimals()` is out by seven orders of magnitude.
view fn shareDecimals() -> uint {
return tokenDecimals + shareOffsetDigits;
}
// The quotes. Every one of them is the same arithmetic the mutating function performs,
// so a caller can price a move before making it — and they are `view`, so the compiler has
// refused them the ability to write anything, which is why it is safe to call one from a
// wallet against live state.
//
// They quote the CURRENT block's state. Between the quote and the record landing, anyone may
// deposit, redeem or fund yield, and the price moves. Treat a quote as an estimate and the
// return value of the mutating call as the fact.
view fn convertToShares(assets: uint) -> uint {
return assets * (shareSupply + virtualShares) / (vaultAssets + 1);
}
view fn convertToAssets(shareAmount: uint) -> uint {
return shareAmount * (vaultAssets + 1) / (shareSupply + virtualShares);
}
view fn previewDeposit(assets: uint) -> uint {
return assets * (shareSupply + virtualShares) / (vaultAssets + 1);
}
view fn previewRedeem(shareAmount: uint) -> uint {
return shareAmount * (vaultAssets + 1) / (shareSupply + virtualShares);
}
// What an exact number of shares COSTS, rounded UP. The counterpart of `previewWithdraw` on
// the way in: the caller pays the remainder of the division rather than the pool absorbing it.
view fn previewMintShares(shareAmount: uint) -> uint {
return (shareAmount * (vaultAssets + 1) + shareSupply + virtualShares - 1)
/ (shareSupply + virtualShares);
}
// Rounds UP: taking an exact number of assets out costs at least the shares that many assets
// are worth, and the remainder of the division stays with the pool rather than with the
// caller. `(x + d - 1) / d` with `d = vaultAssets + 1`, which is why the numerator adds
// `vaultAssets` and not `vaultAssets + 1`.
view fn previewWithdraw(assets: uint) -> uint {
return (assets * (shareSupply + virtualShares) + vaultAssets) / (vaultAssets + 1);
}
view fn maxRedeem(account: address) -> uint {
return shares[account];
}
view fn maxWithdraw(account: address) -> uint {
return shares[account] * (vaultAssets + 1) / (shareSupply + virtualShares);
}
view fn owner() -> address {
return contractOwner;
}
// "" means minting is closed permanently.
view fn minter() -> address {
return mintAuthority;
}
view fn paused() -> bool {
return frozen;
}
view fn pendingOwnerIs() -> address {
return pendingOwner;
}
// ---- the vault ---------------------------------------------------------------------------
fn deposit(assets: uint) -> uint {
require(frozen == false, "paused");
require(assets > 0, "amount must be positive");
require(balances[caller] >= assets, "insufficient balance");
// Priced BEFORE any state changes, off the pool as it stands. Computing the price after
// crediting the pool is the classic vault bug: the depositor is priced against their own
// deposit and gets fewer shares than the quote said.
let minted = assets * (shareSupply + virtualShares) / (vaultAssets + 1);
// A deposit that buys nothing is a donation the caller did not intend. Refuse it rather
// than take the assets — this is the line that makes a rounded-to-zero deposit a revert
// instead of a loss.
require(minted > 0, "no shares");
balances[caller] = balances[caller] - assets;
vaultAssets = vaultAssets + assets;
shares[caller] = shares[caller] + minted;
shareSupply = shareSupply + minted;
emit Deposit(caller, assets, minted);
return minted;
}
// Assets in for an EXACT number of shares — the other direction of `deposit`, for a caller who
// needs a share count rather than an asset amount. It is a separate function rather than an
// argument to `deposit` because the rounding goes the other way: the assets it takes are
// rounded UP, so the pool never sells a share for less than the share is worth.
//
// Named `mintShares` and not `mint`, because `mint` on this contract is the ASSET's minter
// function and the two are not remotely the same power: one issues the underlying token by
// authority, the other buys a claim on the pool with tokens the caller already holds.
fn mintShares(shareAmount: uint) -> uint {
require(frozen == false, "paused");
require(shareAmount > 0, "amount must be positive");
let cost = (shareAmount * (vaultAssets + 1) + shareSupply + virtualShares - 1)
/ (shareSupply + virtualShares);
require(balances[caller] >= cost, "insufficient balance");
balances[caller] = balances[caller] - cost;
vaultAssets = vaultAssets + cost;
shares[caller] = shares[caller] + shareAmount;
shareSupply = shareSupply + shareAmount;
// The same event as `deposit`, because it is the same movement: assets into the pool,
// shares to the caller. An indexer must not have to know which entry point was used.
emit Deposit(caller, cost, shareAmount);
return cost;
}
// Shares in, assets out. NOT gated by the pause: see the header — the owner does not stand
// between a holder and their own share of the pool.
fn redeem(shareAmount: uint) -> uint {
require(shareAmount > 0, "amount must be positive");
require(shares[caller] >= shareAmount, "insufficient shares");
// Rounds down, so the remainder of the division stays in the pool and is shared by
// everyone still in it.
let paid = shareAmount * (vaultAssets + 1) / (shareSupply + virtualShares);
// The mirror of `no shares` in `deposit`, and just as load-bearing: without it, redeeming
// a small enough parcel of shares destroys them and pays nothing, which is a loss the
// caller asked for by accident. It also means a holder cannot exit an EMPTY pool — there
// is nothing there to pay them, and burning the shares would not change that.
require(paid > 0, "no assets");
// Unreachable as the arithmetic stands: `shareAmount` is at most `shareSupply`, and
// `shareSupply * (vaultAssets + 1) / (shareSupply + virtualShares)` is strictly below
// `vaultAssets + 1` whenever `virtualShares` is positive. It is kept because it is the
// floor under the pool, and because anyone editing the formula above will want the revert
// rather than a `SUBU` underflow with no name on it.
require(paid <= vaultAssets, "vault short");
shares[caller] = shares[caller] - shareAmount;
shareSupply = shareSupply - shareAmount;
vaultAssets = vaultAssets - paid;
balances[caller] = balances[caller] + paid;
emit Withdraw(caller, paid, shareAmount);
return paid;
}
// Assets out, shares in — the other direction of the same move, for a caller who needs an
// exact number of assets rather than an exact number of shares. Also not pause-gated.
fn withdraw(assets: uint) -> uint {
require(assets > 0, "amount must be positive");
require(assets <= vaultAssets, "vault short");
// Rounds UP. The caller pays the remainder of the division rather than the pool paying it,
// which is the only direction that cannot be looped into a drain.
let burned = (assets * (shareSupply + virtualShares) + vaultAssets) / (vaultAssets + 1);
require(shares[caller] >= burned, "insufficient shares");
shares[caller] = shares[caller] - burned;
shareSupply = shareSupply - burned;
vaultAssets = vaultAssets - assets;
balances[caller] = balances[caller] + assets;
emit Withdraw(caller, assets, burned);
return burned;
}
// Anyone may pay assets into the pool for the benefit of every share outstanding. This is
// the ONLY way the pool grows other than deposits, and nothing in this contract obliges
// anybody to call it.
fn fundYield(amount: uint) -> bool {
require(frozen == false, "paused");
require(amount > 0, "amount must be positive");
// Into an empty vault this would be a gift to whoever deposits next, priced against a pool
// that already holds it — and with no shares outstanding, nobody can ever claim it back.
// Refuse rather than accept assets that cannot be attributed.
require(shareSupply > 0, "no shares outstanding");
require(balances[caller] >= amount, "insufficient balance");
balances[caller] = balances[caller] - amount;
vaultAssets = vaultAssets + amount;
emit YieldFunded(caller, amount);
return true;
}
// A share is a bearer claim on the pool and moves like one. Gated by the pause, unlike
// redeeming: stopping the market in shares is not the same as standing between a holder and
// the pool, and the holder can still exit through `redeem` while stopped.
fn transferShares(to: address, amount: uint) -> bool {
require(frozen == false, "paused");
require(len(to) > 0, "empty recipient");
require(to != caller, "self transfer");
require(to != self, "self is not payable");
require(shares[caller] >= amount, "insufficient shares");
shares[caller] = shares[caller] - amount;
shares[to] = shares[to] + amount;
emit ShareTransfer(caller, to, amount);
return true;
}
// ---- the asset ledger ---------------------------------------------------------------------
fn transfer(to: address, amount: uint) -> bool {
require(frozen == false, "paused");
require(len(to) > 0, "empty recipient");
require(to != caller, "self transfer");
require(to != self, "self is not payable");
require(balances[caller] >= amount, "insufficient balance");
balances[caller] = balances[caller] - amount;
balances[to] = balances[to] + amount;
emit Transfer(caller, to, amount);
return true;
}
// The caller burns its OWN balance and no other. Burning does not touch the pool: assets in
// the pool are not in anyone's balance, so they cannot be burned by anyone.
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. Every `transfer` in
// this catalogue has refused this since it was written; the ISSUANCE path did not, so the
// guard sat on the call a holder makes and was absent from the call an operator makes.
// MEASURED 2026-10-07: minting here succeeded and produced a position nothing can spend.
require(to != self, "self is not payable");
if (supplyCap > 0) {
require(issuedEver + amount <= supplyCap, "supply cap");
}
issuedEver = issuedEver + amount;
supply = supply + amount;
balances[to] = balances[to] + amount;
emit Transfer("", to, amount);
return true;
}
// ---- 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 = "";
}
// Covers deposit, fundYield, transferShares, transfer, burn and mint. It does NOT cover
// redeem or withdraw, and that asymmetry is the most important line in this file.
fn setPaused(next: bool) {
require(caller == contractOwner, "not owner");
frozen = next;
emit PausedChanged(next, caller);
}
fn setMinter(next: address) {
require(caller == contractOwner, "not owner");
require(len(mintAuthority) > 0, "mint closed");
emit MinterChanged(mintAuthority, next);
mintAuthority = next;
}
// One-way. Lets the asset PROVE a fixed supply rather than promise one.
fn closeMinting() {
require(caller == contractOwner, "not owner");
require(len(mintAuthority) > 0, "mint closed");
emit MinterChanged(mintAuthority, "");
mintAuthority = "";
}
}