diff --git a/flake.lock b/flake.lock index 6f81b13f4..58b3c3409 100644 --- a/flake.lock +++ b/flake.lock @@ -34,6 +34,41 @@ "type": "github" } }, + "flake-utils_2": { + "locked": { + "lastModified": 1644229661, + "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "foundry": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1704964255, + "narHash": "sha256-ycOn/YR6cFgddtHlWj6xtDZuh4wpe6YAQxrVZjOA9lg=", + "owner": "shazow", + "repo": "foundry.nix", + "rev": "d5172f514d1cfa5e01e13ce31f624b990c9f53f4", + "type": "github" + }, + "original": { + "owner": "shazow", + "ref": "monthly", + "repo": "foundry.nix", + "type": "github" + } + }, "nix-bundle-exe": { "flake": false, "locked": { @@ -51,6 +86,20 @@ } }, "nixpkgs": { + "locked": { + "lastModified": 1666753130, + "narHash": "sha256-Wff1dGPFSneXJLI2c0kkdWTgxnQ416KE6X4KnFkgPYQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f540aeda6f677354f1e7144ab04352f61aaa0118", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs_2": { "locked": { "lastModified": 1703499205, "narHash": "sha256-lF9rK5mSUfIZJgZxC3ge40tp1gmyyOXZ+lRY3P8bfbg=", @@ -70,8 +119,9 @@ "inputs": { "flake-compat": "flake-compat", "flake-utils": "flake-utils", + "foundry": "foundry", "nix-bundle-exe": "nix-bundle-exe", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs_2" } }, "systems": { diff --git a/flake.nix b/flake.nix index 9574993b1..63ef7d967 100644 --- a/flake.nix +++ b/flake.nix @@ -2,6 +2,7 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-utils.url = "github:numtide/flake-utils"; + foundry.url = "github:shazow/foundry.nix/monthly"; flake-compat = { url = "github:edolstra/flake-compat"; flake = false; @@ -12,7 +13,7 @@ }; }; - outputs = { self, nixpkgs, flake-utils, nix-bundle-exe, ... }: + outputs = { self, nixpkgs, flake-utils, foundry, nix-bundle-exe, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; @@ -149,6 +150,7 @@ buildInputs = [ solc slither-analyzer + foundry.defaultPackage.${system} haskellPackages.hlint haskellPackages.cabal-install haskellPackages.haskell-language-server diff --git a/lib/Echidna/Solidity.hs b/lib/Echidna/Solidity.hs index 1300730e3..2d29b3f33 100644 --- a/lib/Echidna/Solidity.hs +++ b/lib/Echidna/Solidity.hs @@ -21,11 +21,11 @@ import Data.Text (Text, isPrefixOf, isSuffixOf, append) import Data.Text qualified as T import System.Directory (doesDirectoryExist, doesFileExist, findExecutable, listDirectory, removeFile) -import System.Process (StdStream(..), readCreateProcessWithExitCode, proc, std_err) import System.Exit (ExitCode(..)) import System.FilePath (joinPath, splitDirectories, ()) import System.IO (openFile, IOMode(..)) import System.Info (os) +import System.Process import EVM (initialContract, currentContract) import EVM.ABI @@ -85,14 +85,47 @@ readSolcBatch d = do -- | Given a list of files, use its extenstion to check if it is a precompiled -- contract or try to compile it and get a list of its contracts and a list of source -- cache, throwing exceptions if necessary. -compileContracts - :: SolConf - -> NonEmpty FilePath - -> IO [BuildOutput] -compileContracts solConf fp = do - path <- findExecutable "crytic-compile" >>= \case +compileContracts :: SolConf -> NonEmpty FilePath -> IO [BuildOutput] +compileContracts solConf targetPaths = do + case targetPaths of + targetPath :| [] -> + doesFileExist (targetPath "foundry.toml") >>= \case + True -> do + buildWithFoundry solConf targetPath >>= \case + Nothing -> buildWithCryticCompile solConf targetPaths + Just buildOutput -> pure [buildOutput] + False -> buildWithCryticCompile solConf targetPaths + _ -> buildWithCryticCompile solConf targetPaths + +buildWithFoundry :: SolConf -> FilePath -> IO (Maybe BuildOutput) +buildWithFoundry solConf projectPath = do + findExecutable "forge" >>= \case + Nothing -> pure Nothing + Just forge -> do + unless solConf.quiet $ putStrLn "Foundry project detected, running forge." + stream <- if solConf.quiet + then UseHandle <$> openFile nullFilePath WriteMode + else pure Inherit + let processParams = (proc forge ["build"]) + { cwd = Just projectPath + , std_out = stream + , std_err = stream + } + (_, _, _, processHandle) <- createProcess processParams + waitForProcess processHandle >>= \case + ExitFailure code -> + throwM $ CompileFailure ("forge failed with error code: " <> show code) "" + ExitSuccess -> + readBuildOutput projectPath Foundry >>= \case + Right buildOutput -> pure (Just buildOutput) + Left err -> + throwM $ CompileFailure ("reading forge build output failed with error:\n" <> err) "" + +buildWithCryticCompile :: SolConf -> NonEmpty FilePath -> IO [BuildOutput] +buildWithCryticCompile solConf targetPaths = do + cryticCompile <- findExecutable "crytic-compile" >>= \case Nothing -> throwM NoCryticCompile - Just path -> pure path + Just cryticCompile -> pure cryticCompile let usual = ["--solc-disable-warnings", "--export-format", "solc"] @@ -100,27 +133,25 @@ compileContracts solConf fp = do (\sa -> if null sa then [] else ["--solc-args", sa]) compileOne :: FilePath -> IO [BuildOutput] compileOne x = do - stderr <- if solConf.quiet - then UseHandle <$> openFile nullFilePath WriteMode - else pure Inherit (ec, out, err) <- measureIO solConf.quiet ("Compiling " <> x) $ do readCreateProcessWithExitCode - (proc path $ (solConf.cryticArgs ++ solargs) |> x) {std_err = stderr} "" + (proc cryticCompile $ (solConf.cryticArgs ++ solargs) |> x) "" case ec of ExitSuccess -> readSolcBatch "crytic-export" ExitFailure _ -> throwM $ CompileFailure out err - -- | OS-specific path to the "null" file, which accepts writes without storing them - nullFilePath :: String - nullFilePath = if os == "mingw32" then "\\\\.\\NUL" else "/dev/null" -- clean up previous artifacts removeJsonFiles "crytic-export" - buildOutputs <- mapM compileOne fp + buildOutputs <- mapM compileOne targetPaths when (length buildOutputs > 1) $ putStrLn "WARNING: more than one SourceCaches was found after compile. \ \Only the first one will be used." pure $ NE.head buildOutputs +-- | OS-specific path to the "null" file, which accepts writes without storing them +nullFilePath :: String +nullFilePath = if os == "mingw32" then "\\\\.\\NUL" else "/dev/null" + removeJsonFiles :: FilePath -> IO () removeJsonFiles dir = whenM (doesDirectoryExist dir) $ do