caller vs tx_origin: the authorization bug
Two functions that look interchangeable. Using the wrong one hands your contract to anyone who can get you to click something.
The VM gives you two ways to ask "who is calling?", and they answer different questions:
abi.caller()— the immediate caller. If a contract called you, this is that contract.abi.tx_origin()— the wallet that signed the transaction, at the very start of the chain of calls. Always an externally-owned account.
For a plain wallet-to-contract transaction they are identical, which is what makes this so easy to get wrong: every test you write by hand passes.
The vulnerability. Suppose a vault authorizes with tx_origin:
abi.require(abi.tx_origin() == owner, b"not_owner") # WRONG
Now an attacker publishes some unrelated-looking contract and persuades the owner to call it — an airdrop, a game, a "claim your rewards" button. That contract calls your vault. The chain is:
owner's wallet → attacker's contract → your vault
Inside the vault, tx_origin() is still the owner — they did sign the transaction. The check passes. The attacker just drained the vault using the owner's own authority, and the owner only ever agreed to call an unrelated contract.
With abi.caller() the check compares against the attacker's contract address and fails. The attack does not work.
The rule: authorize with caller(). tx_origin() is for auditing and analytics — knowing which human ultimately initiated something. Never for permission. The one classic use, "reject all contract callers", is both rarely what you want and breaks every smart-contract wallet.
Your turn
The vault below authorizes with tx_origin and is exploitable. Fix withdraw so it authorizes the immediate caller. The checks simulate an attacker contract calling on the owner's behalf — caller is the attacker, origin is the owner.
Hints
One way to do it
from stdlib import abi, storage
K_OWNER = b"vault:owner"
K_FUNDS = b"vault:funds"
def init(owner_addr: bytes, funds: int) -> None:
storage.set(K_OWNER, bytes(owner_addr))
storage.set(K_FUNDS, int(funds).to_bytes(8, "big"))
def funds() -> int:
raw = storage.get(K_FUNDS, b"")
return 0 if raw == b"" else int.from_bytes(raw, "big")
def owner() -> bytes:
return storage.get(K_OWNER, b"")
def withdraw(amount: int) -> int:
# Authorize the IMMEDIATE caller. An intermediary contract now fails this
# check even when the owner is the one who signed the transaction.
abi.require(abi.caller() == storage.get(K_OWNER, b""), b"not_owner")
amt = int(amount)
have = funds()
abi.require(have >= amt, b"insufficient")
storage.set(K_FUNDS, (have - amt).to_bytes(8, "big"))
return funds()
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.