Skip to content

feat: support external calls with return value handling in ContractSpec DSL #926

@Th0rgal

Description

@Th0rgal

What is needed

The ContractSpec DSL needs high-level abstractions for external contract calls with return value handling. While Expr.call and Expr.staticcall exist as low-level primitives, they require manual memory layout management (mstore for calldata encoding, returndatacopy for result extraction). A higher-level construct is needed that handles:

  1. ABI-encoded external calls: Given a function selector and typed arguments, automatically encode calldata, perform the call, and decode the return value.
  2. Return value extraction: After a call/staticcall, automatically returndatacopy and mload the result into a local variable.
  3. Error propagation: Pattern for "if call fails, forward revert data" (the common if iszero(call(...)) { returndatacopy; revert } pattern).
  4. ERC20 transfer patterns: The safeTransfer/safeTransferFrom pattern with optional bool return handling (already partially supported via Expr.returndataOptionalBoolAt).

Why it is needed (Morpho case)

Morpho Blue's core operations (supply, withdraw, borrow, repay, liquidate, flashLoan) require:

  • ERC20 transfers: token.transfer(to, amount) and token.transferFrom(from, to, amount) with the SafeERC20 optional-bool pattern
  • Oracle price calls: oracle.price() via staticcall with selector 0xa035b1fe
  • IRM borrow rate calls: irm.borrowRate(marketParams, market) with a complex calldata layout (selector + 11 words)
  • Callbacks: onMorphoSupply(assets, data), onMorphoFlashLoan(assets, data) etc.

Currently these ~1,180 lines of hand-written Yul are injected as raw strings via injectSupplyShim, completely bypassing the formal verification pipeline.

Recommended implementation

1. Add Stmt.externalCallWithReturn

| externalCallWithReturn
    (resultVar : String)         -- local var to bind the decoded result
    (target : Expr)              -- contract address
    (selector : Nat)             -- 4-byte function selector
    (args : List (ParamType × Expr))  -- typed arguments for ABI encoding
    (returnTypes : List ParamType)    -- expected return types
    (revertOnFailure : Bool := true)  -- auto-forward revert data on failure

2. Add Stmt.safeTransfer / Stmt.safeTransferFrom

Sugar for the common ERC20 pattern:

| safeTransfer (token to amount : Expr)
| safeTransferFrom (token from to amount : Expr)

These would encode the calldata, perform the call, check the optional bool return, and revert with appropriate error on failure.

3. Codegen changes

In compileStmtList, add cases that:

  • Compute calldata size from argument types
  • Emit mstore sequence for selector + ABI-encoded args
  • Emit call/staticcall
  • Emit failure check (if iszero(success) { returndatacopy; revert })
  • Emit returndatacopy + mload for return value extraction

Tests

  • Unit tests: compile a spec with externalCallWithReturn, verify generated Yul matches expected output
  • Integration test: ERC20 token contract + consumer contract that calls transfer via the DSL
  • Morpho-specific: once implemented, rewrite supplyCase to use the DSL and verify parity with the hand-written Yul via forge test

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions