Module M.05

Missing Access Control

The Open Door

A public write method without access control is like a bank vault with no door. Anyone on the network can call it and modify state. In GenLayer, always validate the sender address against an owner or authorized roles before allowing state mutations.

Side-by-side · Vulnerable vs. Patched

two contracts · proven by paired transactions
vulnerablecontracts/vulnerable/VulnerableVault.py
Failed TX
> consensus failed · validators diverged
1# { "Depends": "py-genlayer:15qfivjvy80800rh998pcxmd2m8va1wq2qzqhz850n8ggcr4i9q0" }23from genlayer import *4import json56# Module 5 (Vulnerable) -- Missing access control.7# `mint` is annotated @gl.public.write but performs no sender check.8# Any address on the network can mint tokens to themselves indefinitely.9# Deployment succeeds; the bug is observable at call-time.101112class VulnerableVault(gl.Contract):13    balances_json: str14    total_supply: str1516    def __init__(self):17        self.balances_json = "{}"18        self.total_supply = "0"1920    @gl.public.write21    def mint(self, amount: int) -> None:22        # BUG: no auth.23        addr = str(gl.message.sender_address)24        balances = json.loads(self.balances_json)25        balances[addr] = int(balances.get(addr, 0)) + int(amount)26        self.balances_json = json.dumps(balances)27        self.total_supply = str(int(self.total_supply) + int(amount))2829    @gl.public.view30    def balance_of(self, addr: str) -> str:31        balances = json.loads(self.balances_json)32        return str(int(balances.get(addr, 0)))3334    @gl.public.view35    def get_total_supply(self) -> str:36        return self.total_supply
patchedcontracts/patched/SecureVault.py
Success TX
> consensus reached · all validators agree
1# { "Depends": "py-genlayer:15qfivjvy80800rh998pcxmd2m8va1wq2qzqhz850n8ggcr4i9q0" }23from genlayer import *4import json56# Module 5 (Patched) -- Role-based access control.7# Owner is set in the constructor from the deployer's address; only the8# owner can grant/revoke minter rights; only minters can mint.91011class SecureVault(gl.Contract):12    owner: str13    minters_json: str       # {"0xabc...": true, ...}14    balances_json: str15    total_supply: str1617    def __init__(self):18        deployer = str(gl.message.sender_address)19        self.owner = deployer20        self.minters_json = json.dumps({deployer: True})21        self.balances_json = "{}"22        self.total_supply = "0"2324    def _require_owner(self) -> None:25        if str(gl.message.sender_address) != self.owner:26            raise Exception("only owner")2728    def _require_minter(self) -> None:29        minters = json.loads(self.minters_json)30        if not minters.get(str(gl.message.sender_address), False):31            raise Exception("only minter")3233    @gl.public.write34    def add_minter(self, addr: str) -> None:35        self._require_owner()36        minters = json.loads(self.minters_json)37        minters[addr] = True38        self.minters_json = json.dumps(minters)3940    @gl.public.write41    def remove_minter(self, addr: str) -> None:42        self._require_owner()43        minters = json.loads(self.minters_json)44        minters[addr] = False45        self.minters_json = json.dumps(minters)4647    @gl.public.write48    def mint(self, amount: int) -> None:49        self._require_minter()50        addr = str(gl.message.sender_address)51        balances = json.loads(self.balances_json)52        balances[addr] = int(balances.get(addr, 0)) + int(amount)53        self.balances_json = json.dumps(balances)54        self.total_supply = str(int(self.total_supply) + int(amount))5556    @gl.public.view57    def balance_of(self, addr: str) -> str:58        balances = json.loads(self.balances_json)59        return str(int(balances.get(addr, 0)))6061    @gl.public.view62    def is_minter(self, addr: str) -> str:63        minters = json.loads(self.minters_json)64        return "true" if minters.get(addr, False) else "false"6566    @gl.public.view67    def get_total_supply(self) -> str:68        return self.total_supply
Call invoked
mint(1000)

no sender check -> any wallet can mint freely

Call invoked
mint(1000)

deployer is owner+minter -> succeeds for the right caller

On-chain receipts

Knowledge check · M.05

01 / 02

Two questions on this incident. Pick the best answer; the question locks once committed.

Question 01 / 02
What happens if a @gl.public.write method has no access control?