Skip to content

gl.get_contract_at(...).view() dispatches to __handle_undefined_method__ despite target method being correctly registered in schema #420

Description

@Snehal707

Summary

Calling a correctly-defined, correctly-decorated @gl.public.view method on a separately deployed Intelligent Contract through gl.get_contract_at(address).view().method_name() fails with call to private method Contract.__handle_undefined_method__ / call to private method Contract.__receive__, even though gen_getContractSchema confirms that the target method is registered and visible.

Environment

  • StudioNet (https://studio.genlayer.com/api)
  • genlayer-test 0.30.0rc2
  • genlayer-py 0.19.0rc2

Reproduction

Target contract:

from genlayer import *

class ICStoreProbe(gl.Contract):
    value: str

    def __init__(self, value: str):
        self.value = value

    @gl.public.view
    def read_it(self) -> str:
        return self.value
  1. Deploy the target with value = "probe-value"; deployment finalizes before the call.
  2. Query gen_getContractSchema for the deployed target. It returns:
{
  "read_it": {
    "params": [],
    "kwparams": {},
    "readonly": true,
    "ret": "string"
  }
}
  1. Deploy a second contract and call:
other = gl.get_contract_at(target_address)
return other.view().read_it()
  1. The call fails with gen_call failed (code=-32000): execution failed.

Ruled out

  • The target method is decorated correctly and is schema-visible.
  • Deployment timing/race: the test harness waits for the deployment receipt before returning the contract handle.
  • Dynamic-proxy-only issue: a statically typed @gl.contract_interface variant also failed.
  • Read-context-only issue: a write transaction variant also failed. Example transaction 0x2a9b9962b5fa117bbefee0dea26cd73fc8a9545db8927a41b5851634eccdc041 reached FINALIZED, but its leader/validator execution_result was ERROR.

The detailed GenVM stderr from the dynamic/write attempt was:

ValueError: call to private method `<function Contract.__handle_undefined_method__ ...>`
call to private method `<function Contract.__receive__ ...>`

Conclusion

The target method is correctly defined and schema-visible, yet the calling runtime dispatches to the undefined-method fallback path. This appears to be a dispatch/resolution bug in the IC-to-IC call mechanism rather than a contract-authoring error.

Question

Is gl.get_contract_at(...).view() known to work in the current StudioNet build? Is there a required registration/configuration step beyond correct method decoration, or is this a runtime bug?

Activity

  1. ygd58 commented on Sep 17, 2026

    @ygd58

    Traced this as far as I could with static analysis of the pinned SDK (genlayer-py 0.19.0rc2, matching your report) — the client-side call construction looks correct, which narrows where the actual bug has to live.

    What I checked

    gl.get_contract_at(addr).view().read_it() resolves like this in genlayer/contract/__init__.py:

    1. .view() returns _ContractAtGetter(_ContractAtViewMethod, address, state, catch_vm_error).
    2. _ContractAtGetter.__getattr__('read_it') correctly captures the accessed name and constructs _ContractAtViewMethod('read_it', address, state, catch_vm_error) — the method name is captured correctly here, no typo/mangling.
    3. Calling that instance sends a CallContract message via gl_call_generic, with calldata: _make_calldata_obj('read_it', args, kwargs), which produces {'': 'read_it', ...} — the target method name is encoded under the empty-string key.

    I couldn't find anywhere in this package where the decision to fall through to __handle_undefined_method__ gets made — that method is defined as an abstract fallback on the base Contract class (Contract.__handle_undefined_method__.__isabstractmethod__ = True), but nothing in genlayer.contract itself resolves an incoming calldata's '' key against the target contract's registered methods. That resolution has to happen in the GenVM host (Rust), which I don't have matching source access to for this pinned build — I can't verify further without either building/running it locally (not something I could do reliably) or someone with genvm-executor/genvm-manager access checking the actual method-lookup path for cross-contract CallContract dispatch.

    What this rules in/out

    The SDK's call-construction side looks correct end-to-end — the method name isn't getting lost, mangled, or mis-encoded before it leaves the caller. If this is a real dispatch bug (not e.g. schema-cache staleness specific to Studio Next), it's most likely in how the GenVM host resolves the '' calldata key against the target contract's method table for CallContract-triggered calls specifically (as opposed to the initial external is_init/top-level call, which presumably works fine or every contract call would fail, not just cross-contract ones).

    Not a fix, but hopefully narrows where to look.

  2. Snehal707 commented on Sep 18, 2026

    @Snehal707
    Author

    Thanks for tracing this, really helpful to have independent confirmation that the client-side call construction is clean. Matches what we found from our side.
    Given it looks like GenVM host-level resolution of the CallContract dispatch for the ''-keyed calldata, we don't have access to chase this further either — happy to wait for anyone with genvm-executor/genvm-manager visibility to pick it up from here.
    One tangentially related data point, in case it's useful: while separately working through #421 (Studio Next deployment), we found that a stale/mismatched runner ID caused a related-sounding failure (invalid_contract runner malformed) — the fix there was pulling the currently-correct runner ID directly from Studio Next's live served assets rather than trusting documented examples. Not claiming that's the same root cause here, but flagging in case a runner/build mismatch is worth ruling in or out for this dispatch issue too.
    Will keep this open and update if we find anything further from our side.

  3. ygd58 commented on Sep 18, 2026

    @ygd58

    That runner-ID-mismatch lead is worth ruling in — I actually have first-party evidence of exactly that instability from my own testing today, on the same "py-genlayer:latest" tag.

    Two separate runs of the identical direct_deploy command, a few minutes apart, in the same session, resolved "latest" to two different runner releases:

    Run 1: Downloading https://github.com/genlayerlabs/genvm-manager/releases/download/v0.6.0-rc3/genvm-runners-all.tar.xz
    Run 2: Downloading https://github.com/genlayerlabs/genvm-manager/releases/download/v0.6.0-rc5/genvm-runners-all.tar.xz
    

    (Separately, both of my local runs also logged Warning: could not resolve latest GenVM version (HTTP Error 403: Forbidden); checking the local cache before falling back — so "latest" resolution is itself flaky, on top of whatever it resolves to when it does succeed.)

    So: within a single testing session, "latest" moved across an RC boundary. If ICStoreProbe and Caller in the original repro were deployed far enough apart in time (or against different environments/caches) to land on different actual runner builds, that's a very plausible way to get exactly this symptom — two contracts that look schema-compatible from the outside but were compiled/dispatched against subtly different runner internals for cross-contract calls specifically.

    Doesn't confirm the mechanism, but it's a second independent signal (yours from #421's runner-ID fix, mine from directly observing "latest" drift) pointing at the same category of cause. Might be worth re-running the original #420 repro with both contracts pinned to the exact same explicit runner hash (not "latest") to see if that alone makes the dispatch succeed — that would isolate this cleanly.

  4. Snehal707 commented on Sep 18, 2026

    @Snehal707
    Author

    Completed the three follow-ups.

    1. Original store's pin verified — retrieved deployed source directly, both original store and caller declare the identical runner pin (py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6). Runner-pin mismatch is ruled out as the explanation for the original failure — they were never mismatched.
    2. Write path reproduced — submitted capture_target against a same-pinned setup on the matching endpoint. Transaction 0xe8277ac15468c8228aab09249bb7b10f816fdb12e65042537349c4e758f8fedf: execution SUCCESS, captured() changed from empty string to the expected value.
    3. Network confirmed as a genuine match, not assumed — decoded the original failed transaction's signed chain ID directly rather than trusting the issue text alone. It's 61999, matching today's endpoint.

    Both original contracts' retrieved sources declare the same runner pin, and today's same-pin control successfully executes capture_target on the matching endpoint and chain. The failure did not reproduce in this control. We haven't established why the outcomes differ; host/runtime changes remain one possibility, not a confirmed cause.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions