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:
- ABI-encoded external calls: Given a function selector and typed arguments, automatically encode calldata, perform the call, and decode the return value.
- Return value extraction: After a
call/staticcall, automatically returndatacopy and mload the result into a local variable.
- Error propagation: Pattern for "if call fails, forward revert data" (the common
if iszero(call(...)) { returndatacopy; revert } pattern).
- 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
What is needed
The ContractSpec DSL needs high-level abstractions for external contract calls with return value handling. While
Expr.callandExpr.staticcallexist as low-level primitives, they require manual memory layout management (mstorefor calldata encoding,returndatacopyfor result extraction). A higher-level construct is needed that handles:call/staticcall, automaticallyreturndatacopyandmloadthe result into a local variable.if iszero(call(...)) { returndatacopy; revert }pattern).safeTransfer/safeTransferFrompattern with optional bool return handling (already partially supported viaExpr.returndataOptionalBoolAt).Why it is needed (Morpho case)
Morpho Blue's core operations (
supply,withdraw,borrow,repay,liquidate,flashLoan) require:token.transfer(to, amount)andtoken.transferFrom(from, to, amount)with the SafeERC20 optional-bool patternoracle.price()viastaticcallwith selector0xa035b1feirm.borrowRate(marketParams, market)with a complex calldata layout (selector + 11 words)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 failure2. Add
Stmt.safeTransfer/Stmt.safeTransferFromSugar 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:mstoresequence for selector + ABI-encoded argscall/staticcallif iszero(success) { returndatacopy; revert })returndatacopy+mloadfor return value extractionTests
externalCallWithReturn, verify generated Yul matches expected outputtransfervia the DSLsupplyCaseto use the DSL and verify parity with the hand-written Yul viaforge test