A token, part 2: allowances
Letting a third party spend on your behalf — the mechanism every exchange and marketplace depends on, and its famous footgun.
transfer moves your own tokens. But a marketplace contract needs to move tokens *for* you, at a moment you are not the caller. That is what an allowance is: a standing permission, from an owner to a spender, for an amount.
def _k_allow(owner: bytes, spender: bytes) -> bytes:
return b"tok:allow:" + owner + b":" + spender
Three functions:
approve(spender, amount)— the caller grants a spender an allowanceallowance(owner, spender)— read ittransfer_from(owner, to, amount)— the spender moves the owner's tokens, spending down their allowance
The approve race. Suppose you approved a spender for 100 and want to reduce it to 50. You send approve(spender, 50). A spender watching the mempool can try to spend the original 100 before your change lands, and then spend 50 more afterwards — 150 total against an allowance you never intended.
The mitigation is social and procedural, not clever code: set an allowance to zero first, confirm it, then set the new value. Some tokens add increase_allowance/decrease_allowance for this reason. This is a real, long-standing hazard in fungible-token design and it is worth knowing that the simple approve you are about to write has it.
transfer_from where caller == owner should still work without an allowance — you never need permission to move your own tokens. The Animica token standard handles exactly this case with a branch before the allowance check.
Your turn
Add approve, allowance and transfer_from. A spender must have enough allowance AND the owner enough balance; the allowance is spent down. An owner moving their own tokens needs no allowance.
Hints
if abi.caller() != owner: — inside that branch check and reduce the allowance.One way to do it
from stdlib import abi, events, storage
def _k_bal(addr: bytes) -> bytes:
return b"tok:bal:" + addr
def _uget(key: bytes) -> int:
raw = storage.get(key, b"")
return 0 if raw == b"" else int.from_bytes(raw, "big")
def _uset(key: bytes, value: int) -> None:
v = int(value)
abi.require(v >= 0, b"negative")
if v == 0:
storage.delete(key)
return
storage.set(key, v.to_bytes(max(1, (v.bit_length() + 7) // 8), "big"))
def _k_allow(owner: bytes, spender: bytes) -> bytes:
return b"tok:allow:" + owner + b":" + spender
def mint(to: bytes, amount: int) -> None:
_uset(_k_bal(bytes(to)), _uget(_k_bal(bytes(to))) + int(amount))
def balance_of(addr: bytes) -> int:
return _uget(_k_bal(bytes(addr)))
def approve(spender: bytes, amount: int) -> int:
amt = int(amount)
abi.require(amt >= 0, b"negative")
key = _k_allow(abi.caller(), bytes(spender))
_uset(key, amt)
events.emit(b"Approval", {"owner": abi.caller(), "spender": bytes(spender), "value": amt})
return _uget(key)
def allowance(owner: bytes, spender: bytes) -> int:
return _uget(_k_allow(bytes(owner), bytes(spender)))
def transfer_from(owner: bytes, to: bytes, amount: int) -> int:
amt = int(amount)
abi.require(amt > 0, b"must_be_positive")
src = bytes(owner)
dest = bytes(to)
caller = abi.caller()
# Moving your own tokens never requires an allowance.
if caller != src:
key = _k_allow(src, caller)
allowed = _uget(key)
abi.require(allowed >= amt, b"allowance_exceeded")
_uset(key, allowed - amt)
have = _uget(_k_bal(src))
abi.require(have >= amt, b"insufficient")
_uset(_k_bal(src), have - amt)
_uset(_k_bal(dest), _uget(_k_bal(dest)) + amt)
events.emit(b"Transfer", {"from": src, "to": dest, "value": amt})
return _uget(_k_bal(src))
Claim your 10 ANM
Finish this lesson and claim 10 ANM, once per address. Paid from the Animica treasury in batches — allow a few minutes.
Stuck? Ask
Answered by Animica's own free inference network. It is donated GPU capacity, so give it 20-30 seconds.