From c99f1a99e52d67ed2befdb3c89d7d484e3ad25a5 Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Tue, 9 Jun 2026 22:13:52 +0200 Subject: [PATCH 1/4] Rewrite sha256 without assembly (and add Error) --- std/std.solc | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/std/std.solc b/std/std.solc index 4686cf2c..d8df3b1d 100644 --- a/std/std.solc +++ b/std/std.solc @@ -1,3 +1,5 @@ +import std.opcodes.{gas, staticcall}; + pragma no-patterson-condition ABIEncode, Num; pragma no-coverage-condition ABIDecode, MemoryType; @@ -2182,17 +2184,10 @@ forall a . a:MemorySize, a:MemoryPointer => function keccak256_(input: a) -> byt forall a . a:MemorySize, a:MemoryPointer => function sha256(input: a) -> bytes32 { let len : word = MemorySize.len(input); let ptr : word = MemoryPointer.ptr(input); - let res : word; // We assume the [0, 32] scratch space is reserved. - // TODO: add specific error code - assembly { - let ret := staticcall(gas(), 2, ptr, len, 0, 32) - if iszero(ret) { - revert(0, 0) - } - res := mload(0) - } - return bytes32(res); + let ret = staticcall(gas(), 2, ptr, len, 0, 32); + require(ret != 0, Error(0x68c071bb)); // SHA256CallFailed() + return bytes32(mload(0)); } forall a . a:MemorySize, a:MemoryPointer => function ripemd160(input: a) -> bytes32 { From 11105594f1ce2a1ba40c061729267e7e20849c16 Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Tue, 16 Jun 2026 15:21:51 +0200 Subject: [PATCH 2/4] Rewrite ripemd160 without assembly (and add Error) --- std/std.solc | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/std/std.solc b/std/std.solc index d8df3b1d..c4e96db4 100644 --- a/std/std.solc +++ b/std/std.solc @@ -2193,17 +2193,10 @@ forall a . a:MemorySize, a:MemoryPointer => function sha256(input: a) -> bytes32 forall a . a:MemorySize, a:MemoryPointer => function ripemd160(input: a) -> bytes32 { let len : word = MemorySize.len(input); let ptr : word = MemoryPointer.ptr(input); - let res : word; // We assume the [0, 32] scratch space is reserved. - // TODO: add specific error code - assembly { - let ret := staticcall(gas(), 3, ptr, len, 0, 32) - if iszero(ret) { - revert(0, 0) - } - res := mload(0) - } - return bytes32(res); + let ret = staticcall(gas(), 3, ptr, len, 0, 32); + require(ret != 0, Error(0x31a72d92)); // RIPEMD160CallFailed() + return bytes32(mload(0)); } // --- Precompiles --- From 563d5f6535622f917d6dbcab3ab4eecf8077e5e8 Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Tue, 16 Jun 2026 15:24:08 +0200 Subject: [PATCH 3/4] Rewrite ECRecover without assembly (and add Errors) --- std/std.solc | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/std/std.solc b/std/std.solc index c4e96db4..0803f86c 100644 --- a/std/std.solc +++ b/std/std.solc @@ -2218,23 +2218,14 @@ function ecrecover(hash: bytes32, v: uint256, r: bytes32, s: bytes32) -> address let r_ = Typedef.rep(r); let s_ = Typedef.rep(s); let ptr = get_free_memory(); - let res: word; // We assume the [0, 32] scratch space is reserved. - // TODO: add specific error code - assembly { - mstore(ptr, hash_) - mstore(add(ptr, 32), v_) - mstore(add(ptr, 64), r_) - mstore(add(ptr, 96), s_) - - let ret := staticcall(gas(), 1, ptr, 128, 0, 32) - if iszero(ret) { - revert(0, 0) - } - res := mload(0) - if iszero(res) { - revert(0, 0) - } - } + mstore(ptr, hash_); + mstore(ptr + 32, v_); + mstore(ptr + 64, r_); + mstore(ptr + 96, s_); + let ret = staticcall(gas(), 1, ptr, 128, 0, 32); + require(ret != 0, Error(0x578763f7)); // ECRecoverCallFailed() + let res = mload(0); + require(res != 0, Error(0x4fbfae63)); // ECRecoverFailed() return address(res); } From 5414f26c86f7e24d760bee4045aecca721d359b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 13:45:00 +0000 Subject: [PATCH 4/4] Cover ECRecoverFailed revert path Add a recoverFail() case that passes an invalid r = 0 signature component. The ecrecover precompile succeeds but recovers nothing, returning empty output, so ecrecover() yields 0 and the std helper reverts with ECRecoverFailed() (0x4fbfae63). v and s are kept well-formed so the malleability and call-failed guards don't fire. Co-authored-by: Alex Beregszaszi --- test/examples/dispatch/ecrecover.json | 12 ++++++++++++ test/examples/dispatch/ecrecover.solc | 12 ++++++++++++ test/testrunner/EVMHost.cpp | 12 ++++++++++++ 3 files changed, 36 insertions(+) diff --git a/test/examples/dispatch/ecrecover.json b/test/examples/dispatch/ecrecover.json index 3e4a2b13..8d39fff0 100644 --- a/test/examples/dispatch/ecrecover.json +++ b/test/examples/dispatch/ecrecover.json @@ -21,6 +21,18 @@ "returndata": "0000000000000000000000007e5f4552091a69125d5dfcb7b8c2659029395bdf", "status": "success" } + }, + { + "input": { + "text-calldata": "recoverFail()(address)", + "calldata": "75ebe7ac", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "4fbfae63", + "status": "failure" + } } ] } diff --git a/test/examples/dispatch/ecrecover.solc b/test/examples/dispatch/ecrecover.solc index 49dbfd98..9cb8c790 100644 --- a/test/examples/dispatch/ecrecover.solc +++ b/test/examples/dispatch/ecrecover.solc @@ -9,4 +9,16 @@ contract EcrecoverTest { let s: bytes32 = bytes32(0x3523e7d34da277c59af090e44cebddb10b73be11780f028d02cf5ae5f24109fc); return ecrecover(h, v, r, s); } + + // r = 0 is an invalid signature component: the precompile succeeds (ret != 0) + // but recovers nothing, so it returns empty output and `res` stays 0. This + // exercises the `ECRecoverFailed()` (0x4fbfae63) revert path. `v` and `s` + // are kept well-formed so neither the malleability nor call-failed guards fire. + public function recoverFail() -> address { + let h: bytes32 = bytes32(0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899); + let v: uint256 = uint256(27); + let r: bytes32 = bytes32(0x0); + let s: bytes32 = bytes32(0x3523e7d34da277c59af090e44cebddb10b73be11780f028d02cf5ae5f24109fc); + return ecrecover(h, v, r, s); + } } diff --git a/test/testrunner/EVMHost.cpp b/test/testrunner/EVMHost.cpp index 5d15b9d2..eb788df7 100644 --- a/test/testrunner/EVMHost.cpp +++ b/test/testrunner/EVMHost.cpp @@ -541,6 +541,18 @@ evmc::Result EVMHost::precompileECRecover(evmc_message const& _message) noexcept fromHex("0000000000000000000000007e5f4552091a69125d5dfcb7b8c2659029395bdf"), gas_cost } + }, + { + fromHex( + "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899" + "000000000000000000000000000000000000000000000000000000000000001b" + "0000000000000000000000000000000000000000000000000000000000000000" + "3523e7d34da277c59af090e44cebddb10b73be11780f028d02cf5ae5f24109fc" + ), + { + fromHex(""), + gas_cost + } } }; evmc::Result result = precompileGeneric(_message, inputOutput, true /* _ignoresTrailingInput */);