Skip to content

Commit 828edc1

Browse files
authored
Merge pull request #620 from ethereum/overapproximate-staticcall
Overapproximate staticcall in case we can't resolve callee
2 parents bbb40fc + 0914a54 commit 828edc1

File tree

4 files changed

+234
-17
lines changed

4 files changed

+234
-17
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## Added
9+
- When a staticcall is made to a contract that does not exist, we overapproximate
10+
and return symbolic values
11+
- More simplification rules for Props
912
- JoinBytes simplification rule
1013
- New simplification rule to help deal with abi.encodeWithSelector
1114
- More simplification rules for Props

src/EVM.hs

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ makeVm o = do
165165
, currentFork = 0
166166
, labels = mempty
167167
, osEnv = mempty
168+
, freshVar = 0
168169
}
169170
where
170171
env = Env
@@ -287,6 +288,17 @@ getOpW8 state = case state.code of
287288
getOpName :: forall (t :: VMType) s . FrameState t s -> [Char]
288289
getOpName state = intToOpName $ fromEnum $ getOpW8 state
289290

291+
-- If the address is already in the cache, or can be obtained via API, return True
292+
-- otherwise, return False
293+
canFetchAccount :: forall (t :: VMType) s . VMOps t => Expr EAddr -> EVM t s (Bool)
294+
canFetchAccount addr = do
295+
use (#env % #contracts % at addr) >>= \case
296+
Just _ -> pure True
297+
Nothing -> case addr of
298+
LitAddr _ -> pure True
299+
SymAddr _ -> pure False
300+
GVar _ -> internalError "GVar not allowed here"
301+
290302
-- | Executes the EVM one step
291303
exec1 :: forall (t :: VMType) s. VMOps t => EVM t s ()
292304
exec1 = do
@@ -982,26 +994,38 @@ exec1 = do
982994
case stk of
983995
xGas:xTo:xInOffset:xInSize:xOutOffset:xOutSize:xs ->
984996
case wordToAddr xTo of
985-
Nothing -> do
986-
loc <- codeloc
987-
let msg = "Unable to determine a call target"
988-
partial $ UnexpectedSymbolicArg (snd loc) opName msg [SomeExpr xTo]
989-
Just xTo' ->
997+
Nothing -> fallback
998+
Just xTo' -> do
990999
case gasTryFrom xGas of
9911000
Left _ -> vmError IllegalOverflow
992-
Right gas -> do
993-
overrideC <- use $ #state % #overrideCaller
994-
delegateCall this gas xTo' xTo' (Lit 0) xInOffset xInSize xOutOffset xOutSize xs $
995-
\callee -> do
996-
zoom #state $ do
997-
assign #callvalue (Lit 0)
998-
assign #caller $ fromMaybe self overrideC
999-
assign #contract callee
1000-
assign #static True
1001-
touchAccount self
1002-
touchAccount callee
1001+
Right gas -> canFetchAccount xTo' >>= \case
1002+
False -> fallback
1003+
True -> do
1004+
overrideC <- use $ #state % #overrideCaller
1005+
delegateCall this gas xTo' xTo' (Lit 0) xInOffset xInSize xOutOffset xOutSize xs $
1006+
\callee -> do
1007+
zoom #state $ do
1008+
assign #callvalue (Lit 0)
1009+
assign #caller $ fromMaybe self overrideC
1010+
assign #contract callee
1011+
assign #static True
1012+
touchAccount self
1013+
touchAccount callee
10031014
_ ->
10041015
underrun
1016+
where
1017+
fallback = do
1018+
-- Reset caller if needed
1019+
resetCaller <- use $ #state % #resetCaller
1020+
when resetCaller $ assign (#state % #overrideCaller) Nothing
1021+
-- overapproximate by returning a symbolic value
1022+
freshVar <- use #freshVar
1023+
assign #freshVar (freshVar + 1)
1024+
let freshVarExpr = Var ("staticcall-result-stack-" <> (pack . show) freshVar)
1025+
pushSym freshVarExpr
1026+
modifying #constraints ((:) (PLEq freshVarExpr (Lit 1) ))
1027+
assign (#state % #returndata) (AbstractBuf ("staticall-result-data-" <> (pack . show) freshVar))
1028+
next
10051029

10061030
OpSelfdestruct ->
10071031
notStatic $

src/EVM/Types.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,9 @@ data VM (t :: VMType) s = VM
670670
, currentFork :: Int
671671
, labels :: Map Addr Text
672672
, osEnv :: Map String String
673+
, freshVar :: Int
674+
-- ^ used to generate fresh symbolic variable names for overapproximations
675+
-- during symbolic execution. See e.g. OpStaticcall
673676
}
674677
deriving (Generic)
675678

test/test.hs

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2060,10 +2060,197 @@ tests = testGroup "hevm"
20602060
}
20612061
}
20622062
|]
2063-
let sig = Just (Sig "checkval(uint256,uint256)" [AbiUIntType 256, AbiUIntType 256])
2063+
let sig = Just (Sig "checkval(uint8)" [AbiUIntType 8])
20642064
(res, [Qed _]) <- withDefaultSolver $ \s ->
20652065
checkAssert s defaultPanicCodes c sig [] defaultVeriOpts
20662066
putStrLnM $ "successfully explored: " <> show (Expr.numBranches res) <> " paths"
2067+
, test "staticcall-check-orig" $ do
2068+
Just c <- solcRuntime "C"
2069+
[i|
2070+
contract Target {
2071+
function add(uint256 x, uint256 y) external pure returns (uint256) {
2072+
unchecked {
2073+
return x + y;
2074+
}
2075+
}
2076+
}
2077+
2078+
contract C {
2079+
function checkval(uint256 x, uint256 y) public {
2080+
Target t = new Target();
2081+
address realAddr = address(t);
2082+
bytes memory data = abi.encodeWithSignature("add(uint256,uint256)", x, y);
2083+
(bool success, bytes memory returnData) = realAddr.staticcall(data);
2084+
assert(success);
2085+
2086+
uint result = abi.decode(returnData, (uint256));
2087+
uint expected;
2088+
unchecked {
2089+
expected = x + y;
2090+
}
2091+
assert(result == expected);
2092+
}
2093+
}
2094+
|]
2095+
let sig = Just (Sig "checkval(uint256,uint256)" [AbiAddressType, AbiUIntType 256, AbiUIntType 256])
2096+
(res, ret) <- withDefaultSolver $ \s -> checkAssert s defaultPanicCodes c sig [] defaultVeriOpts
2097+
putStrLnM $ "successfully explored: " <> show (Expr.numBranches res) <> " paths"
2098+
let numCexes = sum $ map (fromEnum . isCex) ret
2099+
let numErrs = sum $ map (fromEnum . isError) ret
2100+
let numQeds = sum $ map (fromEnum . isQed) ret
2101+
assertEqualM "number of counterexamples" 0 numCexes
2102+
assertEqualM "number of errors" 0 numErrs
2103+
assertEqualM "number of qed-s" 1 numQeds
2104+
, test "staticcall-check-orig2" $ do
2105+
Just c <- solcRuntime "C"
2106+
[i|
2107+
contract Target {
2108+
function add(uint256 x, uint256 y) external pure returns (uint256) {
2109+
assert(1 == 0);
2110+
}
2111+
}
2112+
contract C {
2113+
function checkval(uint256 x, uint256 y) public {
2114+
Target t = new Target();
2115+
address realAddr = address(t);
2116+
bytes memory data = abi.encodeWithSignature("add(uint256,uint256)", x, y);
2117+
(bool success, bytes memory returnData) = realAddr.staticcall(data);
2118+
assert(success);
2119+
}
2120+
}
2121+
|]
2122+
let sig = Just (Sig "checkval(uint256,uint256)" [AbiAddressType, AbiUIntType 256, AbiUIntType 256])
2123+
(res, ret) <- withDefaultSolver $ \s -> checkAssert s defaultPanicCodes c sig [] defaultVeriOpts
2124+
putStrLnM $ "successfully explored: " <> show (Expr.numBranches res) <> " paths"
2125+
-- let cexesExt = map (snd . fromJust . extractCex) ret
2126+
-- putStrLnM $ "Cexes: \n" <> (unlines $ map ("-> " ++) (map show cexesExt))
2127+
let numCexes = sum $ map (fromEnum . isCex) ret
2128+
let numErrs = sum $ map (fromEnum . isError) ret
2129+
let numQeds = sum $ map (fromEnum . isQed) ret
2130+
assertEqualM "number of counterexamples" 1 numCexes
2131+
assertEqualM "number of errors" 0 numErrs
2132+
assertEqualM "number of qed-s" 0 numQeds
2133+
, test "staticcall-check-symbolic1" $ do
2134+
Just c <- solcRuntime "C"
2135+
[i|
2136+
contract C {
2137+
function checkval(address inputAddr, uint256 x, uint256 y) public {
2138+
bytes memory data = abi.encodeWithSignature("add(uint256,uint256)", x, y);
2139+
(bool success, bytes memory returnData) = inputAddr.staticcall(data);
2140+
assert(success);
2141+
}
2142+
}
2143+
|]
2144+
let sig = Just (Sig "checkval(address,uint256,uint256)" [AbiAddressType, AbiUIntType 256, AbiUIntType 256])
2145+
(res, ret) <- withDefaultSolver $ \s -> checkAssert s defaultPanicCodes c sig [] defaultVeriOpts
2146+
putStrLnM $ "successfully explored: " <> show (Expr.numBranches res) <> " paths"
2147+
let numCexes = sum $ map (fromEnum . isCex) ret
2148+
let numErrs = sum $ map (fromEnum . isError) ret
2149+
let numQeds = sum $ map (fromEnum . isQed) ret
2150+
-- There are 2 CEX-es, in contrast to the above (staticcall-check-orig2).
2151+
-- This is because with one CEX, the return DATA
2152+
-- is empty, and in the other, the return data is non-empty (but symbolic)
2153+
assertEqualM "number of counterexamples" 2 numCexes
2154+
assertEqualM "number of errors" 0 numErrs
2155+
assertEqualM "number of qed-s" 0 numQeds
2156+
-- This checks that calling a symbolic address with staticcall will ALWAYS return 0/1
2157+
-- which is the semantic of the EVM. We insert a constraint over the return value
2158+
-- even when overapproximation is used, as below.
2159+
, test "staticcall-check-symbolic-yul" $ do
2160+
Just c <- solcRuntime "C"
2161+
[i|
2162+
contract C {
2163+
function checkval(address inputAddr, uint256 x, uint256 y) public {
2164+
uint success;
2165+
assembly {
2166+
// Allocate memory for the call data
2167+
let callData := mload(0x40)
2168+
2169+
// Function signature for "add(uint256,uint256)" is "0x771602f7"
2170+
mstore(callData, 0x771602f700000000000000000000000000000000000000000000000000000000)
2171+
2172+
// Store the parameters x and y
2173+
mstore(add(callData, 4), x)
2174+
mstore(add(callData, 36), y)
2175+
2176+
// Perform the static call
2177+
success := staticcall(
2178+
gas(), // Forward all available gas
2179+
inputAddr, // Address to call
2180+
callData, // Input data location
2181+
68, // Input data size (4 bytes for function signature + 32 bytes each for x and y)
2182+
0, // Output data location (0 means we don't care about the output)
2183+
0 // Output data size
2184+
)
2185+
}
2186+
assert(success <= 1);
2187+
}
2188+
}
2189+
|]
2190+
let sig = Just (Sig "checkval(address,uint256,uint256)" [AbiAddressType, AbiUIntType 256, AbiUIntType 256])
2191+
(res, ret) <- withDefaultSolver $ \s -> checkAssert s defaultPanicCodes c sig [] defaultVeriOpts
2192+
putStrLnM $ "successfully explored: " <> show (Expr.numBranches res) <> " paths"
2193+
let numCexes = sum $ map (fromEnum . isCex) ret
2194+
let numErrs = sum $ map (fromEnum . isError) ret
2195+
let numQeds = sum $ map (fromEnum . isQed) ret
2196+
assertEqualM "number of counterexamples" 0 numCexes -- no counterexamples, because it is always 0/1
2197+
assertEqualM "number of errors" 0 numErrs
2198+
assertEqualM "number of qed-s" 1 numQeds
2199+
, test "staticcall-check-symbolic2" $ do
2200+
Just c <- solcRuntime "C"
2201+
[i|
2202+
contract C {
2203+
function checkval(address inputAddr, uint256 x, uint256 y) public {
2204+
bytes memory data = abi.encodeWithSignature("add(uint256,uint256)", x, y);
2205+
(bool success, bytes memory returnData) = inputAddr.staticcall(data);
2206+
assert(success);
2207+
2208+
uint result = abi.decode(returnData, (uint256));
2209+
uint expected;
2210+
unchecked {
2211+
expected = x + y;
2212+
}
2213+
assert(result == expected);
2214+
}
2215+
}
2216+
|]
2217+
let sig = Just (Sig "checkval(address,uint256,uint256)" [AbiAddressType, AbiUIntType 256, AbiUIntType 256])
2218+
(res, ret) <- withDefaultSolver $ \s -> checkAssert s defaultPanicCodes c sig [] defaultVeriOpts
2219+
putStrLnM $ "successfully explored: " <> show (Expr.numBranches res) <> " paths"
2220+
let numCexes = sum $ map (fromEnum . isCex) ret
2221+
let numErrs = sum $ map (fromEnum . isError) ret
2222+
let numQeds = sum $ map (fromEnum . isQed) ret
2223+
assertEqualM "number of counterexamples" 2 numCexes
2224+
assertEqualM "number of errors" 1 numErrs
2225+
assertEqualM "number of qed-s" 0 numQeds
2226+
, test "jump-symbolic" $ do
2227+
Just c <- solcRuntime "C"
2228+
[i|
2229+
// Target contract with a view function
2230+
contract Target {
2231+
}
2232+
2233+
// Caller contract using staticcall
2234+
contract C {
2235+
function checkval(address inputAddr, uint256 x, uint256 y) public {
2236+
Target t = new Target();
2237+
address realAddr = address(t);
2238+
2239+
bytes memory data = abi.encodeWithSignature("add(uint256,uint256)", x, y);
2240+
(bool success, bytes memory returnData) = inputAddr.staticcall(data);
2241+
assert(success == true);
2242+
}
2243+
}
2244+
|]
2245+
let sig = Just (Sig "checkval(address,uint256,uint256)" [AbiAddressType, AbiUIntType 256, AbiUIntType 256])
2246+
(res, ret) <- withDefaultSolver $ \s -> checkAssert s defaultPanicCodes c sig [] defaultVeriOpts
2247+
putStrLnM $ "successfully explored: " <> show (Expr.numBranches res) <> " paths"
2248+
let numCexes = sum $ map (fromEnum . isCex) ret
2249+
let numErrs = sum $ map (fromEnum . isError) ret
2250+
let numQeds = sum $ map (fromEnum . isQed) ret
2251+
assertEqualM "number of counterexamples" numCexes 2
2252+
assertEqualM "number of symbolic copy errors" numErrs 0
2253+
assertEqualM "number of qed-s" numQeds 0
20672254
,
20682255
test "opcode-mul-assoc" $ do
20692256
Just c <- solcRuntime "MyContract"

0 commit comments

Comments
 (0)