Ethereum smart contract fuzzer
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
echidna/lib/Echidna/Transaction.hs

184 lines
8.2 KiB

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}
module Echidna.Transaction where
import Prelude hiding (Word)
import Control.Lens
5 years ago
import Control.Monad (join, liftM2, liftM3, liftM5)
import Control.Monad.Catch (MonadThrow)
import Control.Monad.Random.Strict (MonadRandom, getRandomR, uniform)
import Control.Monad.Reader.Class (MonadReader)
import Control.Monad.State.Strict (MonadState, State, evalStateT, runState)
import Data.Aeson (ToJSON(..), object)
import Data.ByteString (ByteString)
import Data.Has (Has(..))
import Data.List (intercalate)
import EVM hiding (value)
import EVM.ABI (abiCalldata, abiValueType)
import EVM.Concrete (Word(..), w256)
import EVM.Types (Addr)
import qualified Control.Monad.State.Strict as S (state)
import qualified Data.ByteString.Base16 as BS16
import qualified Data.ByteString.Char8 as BSC8
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T
import qualified Data.Vector as V
import Echidna.ABI
-- | A transaction call is either a @CREATE@, a fully instrumented 'SolCall', or
-- an abstract call consisting only of calldata.
data TxCall = SolCreate ByteString
| SolCall SolCall
| SolCalldata ByteString
deriving (Show, Ord, Eq)
makePrisms ''TxCall
-- | A transaction is either a @CREATE@ or a regular call with an origin, destination, and value.
-- Note: I currently don't model nonces or signatures here.
data Tx = Tx { _call :: TxCall -- | Call
, _src :: Addr -- | Origin
, _dst :: Addr -- | Destination
, _gas' :: Word -- | Gas
, _gasprice' :: Word -- | Gas price
, _value :: Word -- | Value
, _delay :: (Word, Word) -- | (Time, # of blocks since last call)
5 years ago
} deriving (Eq, Ord, Show)
5 years ago
makeLenses ''Tx
data TxConf = TxConf { _propGas :: Word
-- ^ Gas to use evaluating echidna properties
, _txGas :: Word
-- ^ Gas to use in generated transactions
5 years ago
, _maxGasprice :: Word
5 years ago
-- ^ Maximum gasprice to be checked for a transaction
, _maxTimeDelay :: Word
-- ^ Maximum time delay between transactions (seconds)
, _maxBlockDelay :: Word
-- ^ Maximum block delay between transactions
}
makeLenses 'TxConf
-- | Pretty-print some 'AbiCall'.
5 years ago
ppSolCall :: SolCall -> String
ppSolCall (t, vs) = (if t == "" then T.unpack "*fallback*" else T.unpack t) ++ "(" ++ intercalate "," (ppAbiValue <$> vs) ++ ")"
-- | Pretty-print some 'TxCall'
ppTxCall :: TxCall -> String
ppTxCall (SolCreate _) = "<CREATE>"
ppTxCall (SolCall x) = ppSolCall x
ppTxCall (SolCalldata x) = BSC8.unpack $ "0x" <> BS16.encode x
5 years ago
instance ToJSON Tx where
toJSON (Tx c s d g gp v (t, b)) =
object
[ ("call", toJSON $ ppTxCall c)
-- from/to are Strings, since JSON doesn't support hexadecimal notation
, ("from", toJSON $ show s)
, ("to", toJSON $ show d)
, ("value", toJSON $ show v)
, ("gas", toJSON $ show g)
, ("gasprice", toJSON $ show gp)
, ("time delay", toJSON $ show t)
, ("block delay", toJSON $ show b)
]
-- | If half a tuple is zero, make both halves zero. Useful for generating delays, since block number
-- only goes up with timestamp
level :: (Num a, Eq a) => (a, a) -> (a, a)
level (elemOf each 0 -> True) = (0,0)
level x = x
-- | A contract is just an address with an ABI (for our purposes).
type ContractA = (Addr, NE.NonEmpty SolSignature)
-- | The world is made our of humans with an address, and contracts with an address + ABI.
data World = World { _senders :: NE.NonEmpty Addr
, _receivers :: NE.NonEmpty ContractA
5 years ago
}
makeLenses ''World
-- | Given generators for an origin, destination, value, and function call, generate a call
-- transaction. Note: This doesn't generate @CREATE@s because I don't know how to do that at random.
genTxWith :: (MonadRandom m, MonadState x m, Has World x, MonadThrow m)
=> (NE.NonEmpty Addr -> m Addr) -- ^ Sender generator
-> (NE.NonEmpty ContractA -> m ContractA) -- ^ Receiver generator
5 years ago
-> (Addr -> ContractA -> m SolCall) -- ^ Call generator
-> m Word -- ^ Gas generator
5 years ago
-> m Word -- ^ Gas price generator
5 years ago
-> (Addr -> ContractA -> SolCall -> m Word) -- ^ Value generator
-> m (Word, Word) -- ^ Delay generator
-> m Tx
5 years ago
genTxWith s r c g gp v t = use hasLens >>= \(World ss rs) ->
5 years ago
let s' = s ss; r' = r rs; c' = join $ liftM2 c s' r' in
((liftM5 Tx (SolCall <$> c') s' (fst <$> r') g gp <*>) =<< liftM3 v s' r' c') <*> t
-- | Synthesize a random 'Transaction', not using a dictionary.
5 years ago
genTx :: forall m x y. (MonadRandom m, MonadReader x m, Has TxConf x, MonadState y m, Has World y, MonadThrow m) => m Tx
genTx = use (hasLens :: Lens' y World) >>= evalStateT genTxM . (defaultDict,)
-- | Generate a random 'Transaction' with either synthesis or mutation of dictionary entries.
5 years ago
genTxM :: (MonadRandom m, MonadReader x m, Has TxConf x, MonadState y m, Has GenDict y, Has World y, MonadThrow m) => m Tx
5 years ago
genTxM = view hasLens >>= \(TxConf _ g maxGp t b) -> genTxWith
rElem rElem -- src and dst
5 years ago
(const $ genInteractionsM . snd) -- call itself
(pure g) (inRange maxGp) (\_ _ _ -> pure 0) -- gas, gasprice, value
(level <$> liftM2 (,) (inRange t) (inRange b)) -- delay
where inRange hi = w256 . fromIntegral <$> getRandomR (0 :: Integer, fromIntegral hi)
-- | Check if a 'Transaction' is as \"small\" (simple) as possible (using ad-hoc heuristics).
5 years ago
canShrinkTx :: Tx -> Bool
canShrinkTx (Tx (SolCreate _) _ _ _ 0 0 (0, 0)) = False
canShrinkTx (Tx (SolCall (_,l)) _ _ _ 0 0 (0, 0)) = any canShrinkAbiValue l
canShrinkTx (Tx (SolCalldata _) _ _ _ 0 0 (0, 0)) = False
canShrinkTx _ = True
-- | Given a 'Transaction', generate a random \"smaller\" 'Transaction', preserving origin,
-- destination, value, and call signature.
5 years ago
shrinkTx :: MonadRandom m => Tx -> m Tx
shrinkTx tx'@(Tx c _ _ _ gp (C _ v) (C _ t, C _ b)) = let
c' = case c of
SolCreate{} -> pure c
SolCall sc -> SolCall <$> shrinkAbiCall sc
SolCalldata{} -> pure c
lower 0 = pure $ w256 0
lower x = w256 . fromIntegral <$> getRandomR (0 :: Integer, fromIntegral x)
>>= \r -> uniform [0, r] -- try 0 quicker
possibilities =
[ set call <$> c'
, set value <$> lower v
, set gasprice' <$> lower gp
, set delay <$> fmap level (liftM2 (,) (lower t) (lower b))
]
in join (uniform possibilities) <*> pure tx'
-- | Lift an action in the context of a component of some 'MonadState' to an action in the
-- 'MonadState' itself.
liftSH :: (MonadState a m, Has b a) => State b x -> m x
liftSH = S.state . runState . zoom hasLens
-- | Given a 'Transaction', set up some 'VM' so it can be executed. Effectively, this just brings
-- 'Transaction's \"on-chain\".
5 years ago
setupTx :: (MonadState x m, Has VM x) => Tx -> m ()
5 years ago
setupTx (Tx c s r g gp v (t, b)) = liftSH . sequence_ $
5 years ago
[ result .= Nothing, state . pc .= 0, state . stack .= mempty, state . memory .= mempty, state . gas .= g
, tx . gasprice .= gp, tx . origin .= s, state . caller .= s, state . callvalue .= v
, block . timestamp += t, block . number += b, setup] where
setup = case c of
SolCreate bc -> assign (env . contracts . at r) (Just $ initialContract (RuntimeCode bc) & set balance v) >> loadContract r
SolCall cd -> loadContract r >> state . calldata .= encode cd
SolCalldata cd -> loadContract r >> state . calldata .= cd
5 years ago
encode (n, vs) = abiCalldata
(encodeSig (n, abiValueType <$> vs)) $ V.fromList vs