Module M.01

URL Rot

The Disappearing Oracle

When you hardcode a single URL in a GenLayer contract, you create a single point of failure. If that site goes down, changes its API, or adds anti-bot protection, your contract will fail to reach consensus forever. This is the most common failure mode for oracle-based contracts.

Side-by-side · Vulnerable vs. Patched

two contracts · proven by paired transactions
vulnerablecontracts/vulnerable/VulnerableOracle.py
Failed TX
> consensus failed · validators diverged
1# { "Depends": "py-genlayer:15qfivjvy80800rh998pcxmd2m8va1wq2qzqhz850n8ggcr4i9q0" }23from genlayer import *45# Module 1 (Vulnerable) -- URL Rot.6# Hardcoded single endpoint on a reserved-invalid TLD. DNS resolution will7# fail on every validator, producing a deterministic FINISHED_WITH_ERROR8# execution receipt. The point isn't that the URL is invalid -- it's that9# there is no fallback path, so any change to a real endpoint (downtime,10# anti-bot, schema change) bricks the contract forever.1112PRICE_URL = "https://oracle-rot-demo.invalid/eth.json"131415class VulnerableOracle(gl.Contract):16    last_value: str1718    def __init__(self):19        self.last_value = ""2021    @gl.public.write22    def fetch_price(self) -> None:23        def _fetch() -> str:24            resp = gl.nondet.web.request(PRICE_URL, method='GET')25            # No schema validation, no fallback, no error handling: if the26            # call raises (DNS, 5xx), the tx goes FINISHED_WITH_ERROR.27            return resp.body.decode("utf-8", errors="replace").strip()2829        self.last_value = gl.eq_principle.strict_eq(_fetch)3031    @gl.public.view32    def get_last_value(self) -> str:33        return self.last_value
patchedcontracts/patched/ResilientOracle.py
Success TX
> consensus reached · all validators agree
1# { "Depends": "py-genlayer:15qfivjvy80800rh998pcxmd2m8va1wq2qzqhz850n8ggcr4i9q0" }23from genlayer import *4import json56# Module 1 (Patched) -- Resilient oracle.7# Mutable allow-list of endpoints. Tries each in order; first success wins.8# Owner can add/remove endpoints without redeploying.910DEFAULT_ENDPOINTS = [11    # Intentionally-broken endpoint to prove fallback works.12    "https://oracle-rot-demo.invalid/eth.json",13    # GenLayer-hosted static fixture; returns the same body on every14    # validator so strict_eq converges.15    "https://test-server.genlayer.com/static/genvm/hello.html",16]1718# A substring guaranteed to appear in the static fixture's body.19STABLE_MARKER = "Hello"202122class ResilientOracle(gl.Contract):23    owner: str24    endpoints_json: str25    last_value: str2627    def __init__(self):28        self.owner = str(gl.message.sender_address)29        self.endpoints_json = json.dumps(DEFAULT_ENDPOINTS)30        self.last_value = ""3132    def _require_owner(self) -> None:33        if str(gl.message.sender_address) != self.owner:34            raise Exception("only owner")3536    @gl.public.write37    def fetch_value(self) -> None:38        endpoints = json.loads(self.endpoints_json)3940        def _fetch() -> str:41            for url in endpoints:42                try:43                    resp = gl.nondet.web.request(url, method='GET')44                    if 200 <= resp.status_code < 300:45                        body = resp.body.decode("utf-8", errors="replace")46                        if STABLE_MARKER in body:47                            return f"OK:{url}:{STABLE_MARKER}"48                except Exception:49                    continue50            raise Exception("all endpoints failed")5152        self.last_value = gl.eq_principle.strict_eq(_fetch)5354    @gl.public.write55    def add_endpoint(self, url: str) -> None:56        self._require_owner()57        if not url.startswith("https://"):58            raise ValueError("https only")59        endpoints = json.loads(self.endpoints_json)60        if url not in endpoints:61            endpoints.append(url)62            self.endpoints_json = json.dumps(endpoints)6364    @gl.public.write65    def remove_endpoint(self, url: str) -> None:66        self._require_owner()67        endpoints = json.loads(self.endpoints_json)68        endpoints = [u for u in endpoints if u != url]69        self.endpoints_json = json.dumps(endpoints)7071    @gl.public.view72    def get_last_value(self) -> str:73        return self.last_value7475    @gl.public.view76    def get_endpoints(self) -> str:77        return self.endpoints_json
Call invoked
fetch_price()

web.request hits oracle-rot-demo.invalid -> NXDOMAIN -> execution error

Call invoked
add_endpoint("https://new-oracle.example.com/price")

owner adds an endpoint on-chain -- proves the URL list is mutable, which is the fix

On-chain receipts

Knowledge check · M.01

01 / 02

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

Question 01 / 02
Why does a hardcoded URL fail in GenLayer?