Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Equivalence fixes #681

Merged
merged 13 commits into from
Mar 19, 2025
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
so we don't get too large buffers as counterexamples
- More symbolic overapproximation for Balance and ExtCodeHash opcodes, fixing
CodeHash SMT representation
- Add deployment code flag to the `equivalenceCheck` function
- PNeg + PGT/PGEq/PLeq/PLT simplification rules
- We no longer dispatch Props to SMT that can be solved by a simplification

Expand All @@ -57,6 +58,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- CopySlice rewrite rule is now less strict while still being sound
- Assumptions about reading from buffer after its size are now the same in all cases.
Previously, they were too weak in case of reading 32 bytes.
- The equivalence checker now is able to prove that an empty store is
equivalent to a store with all slots initialized to 0.
- Equivalence checking was incorrectly assuming that overapproximated values
were sequentially equivalent. We now distinguish these symbolic values with
`A-` and `B-`

## Changed
- Warnings now lead printing FAIL. This way, users don't accidentally think that
Expand Down
3 changes: 2 additions & 1 deletion cli/cli.hs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ data Command w
, maxBranch :: w ::: Int <!> "100" <?> "Max number of branches to explore when encountering a symbolic value (default: 100)"
, maxBufSize :: w ::: Int <!> "64" <?> "Maximum size of buffers such as calldata and returndata in exponents of 2 (default: 64, i.e. 2^64 bytes)"
, promiseNoReent:: w ::: Bool <!> "Promise no reentrancy is possible into the contract(s) being examined"
, create :: w ::: Bool <?> "Tx: creation"
}
| Exec -- Execute a given program with specified env & calldata
{ code :: w ::: Maybe ByteString <?> "Program bytecode"
Expand Down Expand Up @@ -312,7 +313,7 @@ equivalence cmd = do
cores <- liftIO $ unsafeInto <$> getNumProcessors
let solverCount = fromMaybe cores cmd.numSolvers
withSolvers solver solverCount (fromMaybe 1 cmd.solverThreads) cmd.smttimeout $ \s -> do
(res, e) <- equivalenceCheck s (fromJust bytecodeA) (fromJust bytecodeB) veriOpts calldata
(res, e) <- equivalenceCheck s (fromJust bytecodeA) (fromJust bytecodeB) veriOpts calldata cmd.create
liftIO $ case (any isCex res, any Expr.isPartial e || any isUnknown res) of
(False, False) -> putStrLn " \x1b[32m[PASS]\x1b[0m Contracts behave equivalently"
(True, _) -> putStrLn " \x1b[31m[FAIL]\x1b[0m Contracts do not behave equivalently"
Expand Down
8 changes: 4 additions & 4 deletions src/EVM.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1601,9 +1601,9 @@ freshBufFallback xs = do
freshVar <- use #freshVar
assign #freshVar (freshVar + 1)
let opName = pack $ show $ getOp ?op
let freshVarExpr = Var (opName <> "-result-stack-" <> (pack . show) freshVar)
let freshVarExpr = Var (opName <> "-result-stack-fresh-" <> (pack . show) freshVar)
modifying #constraints ((:) (PLEq freshVarExpr (Lit 1) ))
let freshReturndataExpr = AbstractBuf (opName <> "-result-data-" <> (pack . show) freshVar)
let freshReturndataExpr = AbstractBuf (opName <> "-result-data-fresh-" <> (pack . show) freshVar)
modifying #constraints ((:) (PLEq (bufLength freshReturndataExpr) (Lit (2 ^ ?conf.maxBufSize))))
assign (#state % #returndata) freshReturndataExpr
next >> assign (#state % #stack) (freshVarExpr:xs)
Expand All @@ -1617,7 +1617,7 @@ freshVarFallback xs _ = do
freshVar <- use #freshVar
assign #freshVar (freshVar + 1)
let opName = pack $ show $ getOp ?op
let freshVarExpr = Var (opName <> "-result-stack-" <> (pack . show) freshVar)
let freshVarExpr = Var (opName <> "-result-stack-fresh-" <> (pack . show) freshVar)
next >> assign (#state % #stack) (freshVarExpr:xs)

forceConcrete :: VMOps t => Expr EWord -> String -> (W256 -> EVM t s ()) -> EVM t s ()
Expand Down Expand Up @@ -2962,7 +2962,7 @@ instance VMOps Symbolic where
pushGas = do
modifying (#env % #freshGasVals) (+ 1)
n <- use (#env % #freshGasVals)
pushSym $ Expr.Gas n
pushSym $ Expr.Gas "" n
enoughGas _ _ = True
subGas _ _ = ()
toGas _ = ()
Expand Down
4 changes: 0 additions & 4 deletions src/EVM/Format.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ module EVM.Format
, strip0x'
, hexByteString
, hexText
, bsToHex
, showVal
) where

Expand Down Expand Up @@ -859,9 +858,6 @@ hexText t =
Right x -> x
_ -> internalError $ "invalid hex bytestring " ++ show t

bsToHex :: ByteString -> String
bsToHex bs = concatMap (paddedShowHex 2) (BS.unpack bs)

showVal :: AbiValue -> Text
showVal (AbiBytes _ bs) = formatBytes bs
showVal (AbiAddress addr) = T.pack . show $ addr
Expand Down
37 changes: 22 additions & 15 deletions src/EVM/SMT.hs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ instance Monoid CexVars where
data BufModel
= Comp CompressedBuf
| Flat ByteString
deriving (Eq, Show)
deriving (Eq)
instance Show BufModel where
show (Comp c) = "Comp " <> show c
show (Flat b) = "Flat 0x" <> bsToHex b

-- | This representation lets us store buffers of arbitrary length without
-- exhausting the available memory, it closely matches the format used by
Expand Down Expand Up @@ -322,10 +325,10 @@ referencedFrameContext expr = nubOrd $ foldTerm go [] expr
where
go :: Expr a -> [(Builder, [Prop])]
go = \case
TxValue -> [(fromString "txvalue", [])]
v@(Balance a) -> [(fromString "balance_" <> formatEAddr a, [PLT v (Lit $ 2 ^ (96 :: Int))])]
Gas freshVar -> [(fromString ("gas_" <> show freshVar), [])]
CodeHash a@(LitAddr _) -> [(fromString "codehash_" <> formatEAddr a, [])]
o@TxValue -> [(fromRight' $ exprToSMT o, [])]
o@(Balance _) -> [(fromRight' $ exprToSMT o, [PLT o (Lit $ 2 ^ (96 :: Int))])]
o@(Gas _ _) -> [(fromRight' $ exprToSMT o, [])]
o@(CodeHash (LitAddr _)) -> [(fromRight' $ exprToSMT o, [])]
_ -> []

referencedBlockContext :: TraversableTerm a => a -> [(Builder, [Prop])]
Expand Down Expand Up @@ -443,20 +446,26 @@ declareConstrainAddrs names = SMT2 (["; concrete and symbolic addresses"] <> fma
assume n = "(assert (bvugt " <> n <> " (_ bv9 160)))"
cexvars = (mempty :: CexVars){ addrs = fmap toLazyText names }

-- The gas is a tuple of (prefix, index). Within each prefix, the gas is strictly decreasing as the
-- index increases. This function gets a map of Prefix -> [Int], and for each prefix,
-- enforces the order
enforceGasOrder :: [Prop] -> SMT2
enforceGasOrder ps = SMT2 (["; gas ordering"] <> order indices) mempty mempty
enforceGasOrder ps = SMT2 (["; gas ordering"] <> (concatMap (uncurry order) indices)) mempty mempty
where
order :: [Int] -> [Builder]
order n = consecutivePairs n >>= \(x, y)->
order :: TS.Text -> [Int] -> [Builder]
order prefix n = consecutivePairs (nubInt n) >>= \(x, y)->
-- The GAS instruction itself costs gas, so it's strictly decreasing
["(assert (bvugt gas_" <> (fromString . show $ x) <> " gas_" <> (fromString . show $ y) <> "))"]
["(assert (bvugt " <> fromRight' (exprToSMT (Gas prefix x)) <> " " <>
fromRight' ((exprToSMT (Gas prefix y))) <> "))"]
consecutivePairs :: [Int] -> [(Int, Int)]
consecutivePairs [] = []
consecutivePairs l = zip l (tail l)
indices :: [Int] = nubInt $ concatMap (foldProp go mempty) ps
go :: Expr a -> [Int]
indices = Map.toList $ toMapOfLists $ concatMap (foldProp go mempty) ps
toMapOfLists :: [(TS.Text, Int)] -> Map.Map TS.Text [Int]
toMapOfLists = foldr (\(k, v) acc -> Map.insertWith (++) k [v] acc) Map.empty
go :: Expr a -> [(TS.Text, Int)]
go e = case e of
Gas freshVar -> [freshVar]
Gas prefix v -> [(prefix, v)]
_ -> []

declareFrameContext :: [(Builder, [Prop])] -> Err SMT2
Expand Down Expand Up @@ -872,8 +881,8 @@ exprToSMT = \case
pure $ "(store" `sp` encPrev `sp` encIdx `sp` encVal <> ")"
SLoad idx store -> op2 "select" store idx
LitAddr n -> pure $ fromLazyText $ "(_ bv" <> T.pack (show (into n :: Integer)) <> " 160)"
Gas freshVar -> pure $ fromLazyText $ "gas_" <> (T.pack $ show freshVar)
CodeHash a@(LitAddr _) -> pure $ fromLazyText "codehash_" <> formatEAddr a
Gas prefix var -> pure $ fromLazyText $ "gas_" <> T.pack (TS.unpack prefix) <> T.pack (show var)

a -> internalError $ "TODO: implement: " <> show a
where
Expand Down Expand Up @@ -1060,14 +1069,12 @@ parseBlockCtx "prevrandao" = PrevRandao
parseBlockCtx "gaslimit" = GasLimit
parseBlockCtx "chainid" = ChainId
parseBlockCtx "basefee" = BaseFee
parseBlockCtx gas | TS.isPrefixOf (TS.pack "gas_") gas = Gas (textToInt $ TS.drop 4 gas)
parseBlockCtx val = internalError $ "cannot parse '" <> (TS.unpack val) <> "' into an Expr"

parseTxCtx :: TS.Text -> Expr EWord
parseTxCtx name
| name == "txvalue" = TxValue
| Just a <- TS.stripPrefix "balance_" name = Balance (parseEAddr a)
| Just a <- TS.stripPrefix "gas_" name = Gas (textToInt a)
| Just a <- TS.stripPrefix "codehash_" name = CodeHash (parseEAddr a)
| otherwise = internalError $ "cannot parse " <> (TS.unpack name) <> " into an Expr"

Expand Down
1 change: 1 addition & 0 deletions src/EVM/Solidity.hs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
import Control.Applicative
import Control.Monad
import Control.Monad.IO.Unlift
import Data.Aeson hiding (json)

Check warning on line 53 in src/EVM/Solidity.hs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Module ‘Data.Aeson’ does not export ‘json’
import Data.Aeson.Types
import Data.Aeson.Optics
import Data.Aeson.Key qualified as Key
Expand Down Expand Up @@ -394,6 +394,7 @@
Just (Contracts sol, _, _) -> pure $ Map.lookup ("hevm.sol:" <> contract) sol <&> (.runtimeCode)
Nothing -> internalError $ "unable to parse solidity output:\n" <> (T.unpack json)


functionAbi :: Text -> IO Method
functionAbi f = do
json <- solc Solidity ("contract ABI { function " <> f <> " public {}}")
Expand Down
2 changes: 1 addition & 1 deletion src/EVM/Solvers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ checkMulti (SolverGroup taskQueue) smt2 multiSol = do
-- collect result
readChan resChan

checkSatWithProps :: App m => SolverGroup -> [Prop] ->m (CheckSatResult, Err SMT2)
checkSatWithProps :: App m => SolverGroup -> [Prop] -> m (CheckSatResult, Err SMT2)
checkSatWithProps (SolverGroup taskQueue) props = do
conf <- readConfig
let psSimp = simplifyProps props
Expand Down
Loading
Loading