height 23776 of 23776 on the network. 9 peer(s) connected, 77 in its peer directory; 1072 block(s) followed and 894 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
- Status
- template — not deployed A template is a draft, not a deployed contract. Nothing here is on the chain: it has no address until somebody deploys it, and where it lands depends on who deploys it and with which salt. Never send value to a template.
- File
templates/zk13-vault.ares- 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
- Compiler in force
areslang-1.3.0- Bytecode
- 5,742 bytes
- Bytecode SHA-256
4a17724cb676e19a69569ad229cfb8d25bc750efa3f8d966bba0845a6f1b3c58- Source SHA-256
1272e405c119fe84f13105c070620cba655a715c5b0fc6a0a6fa9de43e958ea3- Published vector
- matches the published vector spec/conformance-vectors.json pins 4a17724cb676e19a… under areslang-1.3.0
- Catalogue
- every template
Before you deploy
The header marks these for editing before you deploy. The values below are the examples it ships with; deployed unedited, they are what every holder sees for the life of the contract.
| Constant | Type | Shipped as |
|---|---|---|
tokenName | string | "Example ZK13 Vault Asset" |
tokenSymbol | string | "EZV" |
tokenDecimals | uint | 13 |
supplyCap | uint | 0 |
Where your deployment would land
Name a deployer to see where its deployment of this template, unedited, would land.
The chain derives a contract’s address from its name, its bytecode, the deployer and the salt. This is that function over the bytecode above, so it holds only for the template exactly as shown and only while the chain runs the same compiler. Edit one constant and the address moves, which is correct: different code is a different contract. Nothing is at this address until somebody deploys there.
Measured gas
What the STARW VM charged for each call on a real chain, under areslang-1.3.0, and asserted exactly by the catalogue test: a figure that moved would fail the build. Measured, not estimated. A call that stores a string costs more for a longer one; those were measured with ipfs://QmExampleTokenMetadataDocument01.
| 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; post-mining covers only a tenth of the declared limit
ABI
| 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 |
From the command line
Print the source to a file, edit the marked constants, dry-run it to see the digest and your address, then deploy. Only the last command signs, and it is the only one that needs your key and a node.
strblk contract template zk13-vault > zk13-vault.ares strblk contract template zk13-vault --info strblk wallet deploy zk13-vault.ares --dry-run --address <your address> strblk wallet deploy zk13-vault.ares --node <your node's RPC>
Source
templates/zk13-vault.ares, as embedded in this build
// =========================================================================================
// 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 = "";
}
}