Skip to content

feat: add Stmt.externalCallWithReturn for ABI-encoded external calls#935

Merged
Th0rgal merged 3 commits into
mainfrom
feat/stmt-external-call-with-return
Feb 24, 2026
Merged

feat: add Stmt.externalCallWithReturn for ABI-encoded external calls#935
Th0rgal merged 3 commits into
mainfrom
feat/stmt-external-call-with-return

Conversation

@Th0rgal
Copy link
Copy Markdown
Member

@Th0rgal Th0rgal commented Feb 24, 2026

Summary

Adds Stmt.externalCallWithReturn — a high-level statement that compiles the common ABI-encoded external call pattern used throughout Morpho Blue (oracle price feeds, IRM borrow rate queries, ERC20 balance checks) into correct Yul.

  • New Stmt.externalCallWithReturn variant takes a target address, 4-byte selector, argument list, and isStatic flag
  • Compiles to: mstore(shl(224, selector)) + arg encoding + call/staticcall + revert forwarding + returndatasize validation + returndatacopy+mload binding
  • All 11 pattern-match functions updated (validation, compilation, interpreter)
  • 3 test groups with 15 assertions covering staticcall, single-arg call, and multi-arg call patterns

Partial implementation of #926 — covers the single-return-value case. Multi-return and SafeTransfer sugar are planned as follow-up.

Test plan

  • lake build Compiler.ContractSpecFeatureTest passes all existing + 3 new test groups
  • lake build Compiler.ContractSpec builds cleanly (no warnings)
  • lake build Verity.Proofs.Stdlib.SpecInterpreter builds cleanly
  • CI passes

🤖 Generated with Claude Code


Note

Medium Risk
Touches core compiler codegen and validation logic for external calls; incorrect encoding, revert forwarding, or scoping could produce wrong or unsafe EVM behavior despite added tests.

Overview
Adds a new statement variant, Stmt.externalCallWithReturn, to express a common ABI-encoded external call that binds a single uint256 return value.

The compiler now lowers this statement to Yul that ABI-encodes the selector/args in memory, performs call/staticcall, forwards reverts with returned data, checks returndatasize() >= 32, and binds the result via mload(0). All relevant analyses/validators (name collection, scoping, mutability/state-effects, interop checks, external target validation) and the SpecInterpreter are updated to recognize the new statement (interpreter treats it as unsupported like other linked-Yul features).

Adds feature tests covering staticcall vs call, arg encoding offsets/calldata sizing, revert forwarding/size checks, and compile-time rejection of result-var shadowing/redeclaration and multiple uses in one function.

Written by Cursor Bugbot for commit 353319d. This will update automatically on new commits. Configure here.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dumbcontracts Ready Ready Preview, Comment Feb 24, 2026 9:56pm

Request Review

Comment thread Compiler/ContractSpec.lean Outdated
Comment thread Compiler/ContractSpec.lean Outdated
Comment thread Compiler/ContractSpec.lean
Comment thread Compiler/ContractSpec.lean Outdated
@Th0rgal
Copy link
Copy Markdown
Member Author

Th0rgal commented Feb 24, 2026

All 4 bugbot findings addressed:

  1. Redundant returndatacopy (Low) — Removed. call/staticcall with retOffset=0, retSize=32 already copies returndata to memory[0..32].

  2. Result variable scoped inside block (High) — Fixed. Statements now emitted flat instead of wrapped in YulStmt.block, so resultVar is visible to subsequent statements.

  3. Missing validation for resultVar shadowing (Medium) — Added parameter shadow and local redeclaration checks, matching Stmt.letVar behavior.

  4. staticcall incorrectly marked as writing state (Medium) — Fixed. stmtWritesState now returns !isStatic, so view functions can use staticcall external calls.

Added 3 new tests: parameter shadow rejection, local redeclaration rejection, and view+staticcall acceptance.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

Comment thread Compiler/ContractSpec.lean Outdated
Comment thread Compiler/ContractSpec.lean Outdated
@Th0rgal
Copy link
Copy Markdown
Member Author

Th0rgal commented Feb 24, 2026

Both bugbot findings from the second review addressed:

  1. Duplicate __ecwr_success declaration (High) — Fixed by wrapping the call + revert + size-check in a YulStmt.block. The __ecwr_success and __ecwr_rds temporaries are now block-scoped, so multiple externalCallWithReturn statements in the same function don't collide. The resultVar binding remains flat-scoped (outside the block) so subsequent statements can reference it.

  2. collectStmtNames omits target expression (Low) — Fixed. The target expression is now included in collectStmtNames, matching all other handler functions.

Added test: multiple externalCallWithReturn in same function compiles without collision.

@Th0rgal
Copy link
Copy Markdown
Member Author

Th0rgal commented Feb 24, 2026

Re: bugbot findings on externalCallWithReturn

Thanks for the thorough review. Addressing each finding:

  1. Redundant returndatacopy (LOW): Valid optimization opportunity. The call already writes return data, so the explicit returndatacopy is redundant when outOffset is 0. Will note for future optimization but doesn't affect correctness.

  2. Result variable scoped inside block (HIGH): Valid bug. The resultVar is declared inside a YulStmt.block, making it block-scoped and inaccessible outside. This needs to be fixed by emitting the let binding outside the block.

  3. Missing resultVar shadowing validation (MEDIUM): Valid concern. Will add validation to reject duplicate result variable names.

  4. staticcall marked as writing state (MEDIUM): Valid bug. stmtWritesState should check isStatic and return false for static calls. Currently returns true unconditionally.

  5. Duplicate __ecwr_success temp variable (HIGH): Valid bug. Using a hardcoded temp name means two externalCallWithReturn statements in the same function would produce invalid Yul. Need to generate unique names (e.g., with a counter suffix).

  6. collectStmtNames omits target (LOW): Valid. The target expression should be collected for completeness.

Will fix findings #2 and #5 (the HIGH severity ones) in a follow-up commit.

Th0rgal and others added 3 commits February 24, 2026 22:41
…926)

Add a high-level statement that compiles to the common
mstore(selector+args) → call/staticcall → revert forwarding →
returndatacopy+mload pattern used throughout Morpho Blue for oracle
price feeds, IRM borrow rate queries, and ERC20 balance checks.

The generated Yul:
1. Stores the left-shifted selector at memory[0]
2. Stores each argument at sequential 32-byte offsets from memory[4]
3. Performs call() or staticcall() based on isStatic flag
4. Forwards revert data on call failure
5. Validates returndatasize ≥ 32
6. Copies and binds the return value to a local variable

Includes 3 test groups (15 assertions) covering:
- staticcall with no args (oracle pattern)
- call with single arg (ERC20 balanceOf pattern)
- call with multiple args (IRM borrowRate pattern)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Remove redundant returndatacopy — call/staticcall already copies
   returndata to memory[0..32] via retOffset/retSize params
2. Emit statements flat instead of in a YulStmt.block so the resultVar
   binding is accessible to subsequent statements
3. Add resultVar shadowing/redeclaration validation matching Stmt.letVar
4. staticcall (isStatic=true) no longer incorrectly marked as writing
   state, allowing view functions to use staticcall external calls

Adds 3 new tests: parameter shadow rejection, local redeclaration
rejection, and view+staticcall acceptance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address bugbot findings:

1. Wrap call/revert/sizeCheck in a YulStmt.block so __ecwr_success is
   block-scoped. This prevents duplicate let declarations when multiple
   externalCallWithReturn statements appear in the same function body.
   The resultVar binding remains flat-scoped (outside the block) so
   subsequent statements can reference it.

2. Include the target expression in collectStmtNames for
   externalCallWithReturn, matching all other handler functions.

Adds test: multiple externalCallWithReturn in same function compiles
without collision.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Th0rgal Th0rgal force-pushed the feat/stmt-external-call-with-return branch from 3ea5b85 to 353319d Compare February 24, 2026 21:55
@Th0rgal Th0rgal merged commit 9019910 into main Feb 24, 2026
23 checks passed
@Th0rgal Th0rgal deleted the feat/stmt-external-call-with-return branch March 3, 2026 12:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant