diff --git a/flake.lock b/flake.lock index 70472f725..2e6f11de0 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,22 @@ { "nodes": { + "crytic-compile": { + "flake": false, + "locked": { + "lastModified": 1755965041, + "narHash": "sha256-hMgHWuC1IdfPcRD9DXD9j38AJZNh4WuTsco8GGte3MU=", + "owner": "crytic", + "repo": "crytic-compile", + "rev": "4183fee8a0671b1a4228900779556e6dba795915", + "type": "github" + }, + "original": { + "owner": "crytic", + "ref": "dev-autolink", + "repo": "crytic-compile", + "type": "github" + } + }, "flake-compat": { "flake": false, "locked": { @@ -119,6 +136,7 @@ }, "root": { "inputs": { + "crytic-compile": "crytic-compile", "flake-compat": "flake-compat", "flake-utils": "flake-utils", "foundry": "foundry", diff --git a/flake.nix b/flake.nix index f3f7b35b7..452cc4b6f 100644 --- a/flake.nix +++ b/flake.nix @@ -11,9 +11,13 @@ url = "github:hellwolf/solc.nix"; inputs.nixpkgs.follows = "nixpkgs"; }; + crytic-compile = { + url = "github:crytic/crytic-compile/dev-autolink"; + flake = false; + }; }; - outputs = { self, nixpkgs, flake-utils, solc-pkgs, foundry, ... }: + outputs = { self, nixpkgs, flake-utils, solc-pkgs, foundry, crytic-compile, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { @@ -58,6 +62,16 @@ pkgs.haskell.lib.compose.dontCheck ]); + slither-analyzer = let + python3 = pkgs.python3.override { + packageOverrides = final: prev: { + crytic-compile = prev.crytic-compile.overrideAttrs { + src = crytic-compile; + }; + }; + }; + in python3.pkgs.slither-analyzer; + echidna = pkgs: with pkgs; lib.pipe (haskellPackages.callCabal2nix "echidna" ./. { hevm = hevm pkgs; }) ([ diff --git a/lib/Echidna/Libraries.hs b/lib/Echidna/Libraries.hs new file mode 100644 index 000000000..1ffb03380 --- /dev/null +++ b/lib/Echidna/Libraries.hs @@ -0,0 +1,67 @@ +module Echidna.Libraries where + +import Control.Monad.Catch (MonadThrow) +import Control.Monad.Reader (liftIO, MonadReader, MonadIO) +import Control.Monad.ST (RealWorld) +import Data.Aeson (FromJSON(..), withObject, (.:), eitherDecode) +import Data.ByteString.Lazy qualified as LBS +import Data.List (find, isSuffixOf) +import Data.Map (Map) +import Data.Map qualified as Map +import Data.Maybe (mapMaybe) +import Data.Text (Text) +import Data.Text qualified as T +import System.Directory (listDirectory) +import System.FilePath (()) + +import EVM.Solidity +import EVM.Types hiding (Env) + +import Echidna.Deploy (deployContracts) +import Echidna.Types.Config (Env(..)) + +-- | Library linking information from crytic-compile --compile-autolink +data LibraryLinking = LibraryLinking + { deploymentOrder :: [Text] + , libraryAddresses :: Map Text Addr + } deriving (Show) + +instance FromJSON LibraryLinking where + parseJSON = withObject "LibraryLinking" $ \o -> do + deploymentOrder <- o .: "deployment_order" + libraryAddresses <- o .: "library_addresses" + pure LibraryLinking{deploymentOrder, libraryAddresses} + +-- | Try to read library linking information +readLibraryLinking :: FilePath -> IO (Maybe LibraryLinking) +readLibraryLinking d = do + fs <- filter (".link" `Data.List.isSuffixOf`) <$> listDirectory d + case fs of + [] -> pure Nothing + [linkingFile] -> do + content <- LBS.readFile $ d linkingFile + case eitherDecode content of + Left err -> do + putStrLn $ "Warning: Failed to parse " <> linkingFile <> ": " <> err + pure Nothing + Right linking -> pure (Just linking) + _ -> error $ "Multiple link files found: " <> show fs <> "\n" + +-- | Deploy libraries using autolink information if available +deployAutolinkLibraries + :: (MonadIO m, MonadReader Env m, MonadThrow m) + => [SolcContract] + -> Addr + -> VM Concrete RealWorld + -> m (VM Concrete RealWorld) +deployAutolinkLibraries cs deployer vm = do + linking <- liftIO $ readLibraryLinking "crytic-export" + case linking of + Nothing -> pure vm + Just (LibraryLinking deploymentOrder libraryAddresses) -> do + -- Deploy libraries in the specified order at the specified addresses + let orderedLibs = mapMaybe (\libName -> do + addr <- Map.lookup libName libraryAddresses + contract <- find (\c -> T.isSuffixOf (":" <> libName) c.contractName) cs + pure (addr, contract)) deploymentOrder + deployContracts orderedLibs deployer vm diff --git a/lib/Echidna/Solidity.hs b/lib/Echidna/Solidity.hs index 5b5f779f2..1687d77ae 100644 --- a/lib/Echidna/Solidity.hs +++ b/lib/Echidna/Solidity.hs @@ -41,6 +41,7 @@ import Echidna.ABI import Echidna.Deploy (deployContracts, deployBytecodes) import Echidna.Events (extractEvents) import Echidna.Exec (execTx, execTxWithCov, initialVM) +import Echidna.Libraries (deployAutolinkLibraries) import Echidna.SourceAnalysis.Slither import Echidna.Test (createTests, isAssertionMode, isPropertyMode, isDapptestMode) import Echidna.Types.Campaign (CampaignConf(..)) @@ -189,12 +190,15 @@ loadSpecified env mainContract cs = do ls <- mapM (chooseContract cs . Just . T.pack) solConf.solcLibs flip runReaderT env $ do - -- library deployment + -- library deployment (solcLibs) vm0 <- deployContracts (zip [addrLibrary ..] ls) solConf.deployer blank + -- library deployment (from autolink, if link file exists) + vm0' <- deployAutolinkLibraries cs solConf.deployer vm0 + -- additional contract deployment (by name) cs' <- mapM ((chooseContract cs . Just) . T.pack . snd) solConf.deployContracts - vm1 <- deployContracts (zip (map fst solConf.deployContracts) cs') solConf.deployer vm0 + vm1 <- deployContracts (zip (map fst solConf.deployContracts) cs') solConf.deployer vm0' -- additional contract deployment (bytecode) vm2 <- deployBytecodes solConf.deployBytecodes solConf.deployer vm1 diff --git a/src/test/Tests/Integration.hs b/src/test/Tests/Integration.hs index c37c88482..e5b9f8d78 100644 --- a/src/test/Tests/Integration.hs +++ b/src/test/Tests/Integration.hs @@ -60,6 +60,8 @@ integrationTests = testGroup "Solidity Integration Testing" [ ("echidna_library_call failed", solved "echidna_library_call") , ("echidna_valid_timestamp failed", passed "echidna_valid_timestamp") ] + , testContract' "basic/autolink.sol" (Just "TestExternalLibrary") Nothing (Just "basic/autolink.yaml") True FuzzWorker + [ ("echidna_library_call_works failed", passed "echidna_library_call_works") ] , testContractV "basic/fallback.sol" (Just (< solcV (0,6,0))) Nothing [ ("echidna_fallback failed", solved "echidna_fallback") ] , testContract "basic/push_long.sol" (Just "basic/push_long.yaml") diff --git a/tests/solidity/basic/autolink.sol b/tests/solidity/basic/autolink.sol new file mode 100644 index 000000000..bc9ae1e32 --- /dev/null +++ b/tests/solidity/basic/autolink.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title Library1 + * @dev A basic library that demonstrates a simple public function returning a string. + */ +library Library1 { + /** + * @dev Returns a static string "Library" + * @return A static string value + */ + function getLibrary() public pure returns(string memory) { + return "Library"; + } +} + +/** + * @title Library2 + * @dev A library that depends on Library1, demonstrating direct library dependencies. + * This library calls Library1's function and returns its result. + */ +library Library2 { + /** + * @dev Calls and returns the result from Library1.getLibrary() + * @return A string value from the dependent library + */ + function getLibrary() public pure returns(string memory) { + return Library1.getLibrary(); + } +} + +/** + * @title Library3 + * @dev A library with multiple dependencies, demonstrating complex dependency chains. + * This library calls both Library1 and Library2, creating a transitive dependency. + */ +library Library3 { + /** + * @dev Calls Library2.getLibrary() and then returns the result from Library1.getLibrary() + * @return A string value from Library1 + */ + function getLibrary() public pure returns(string memory) { + Library2.getLibrary(); + return Library1.getLibrary(); + } +} + +/** + * @title TestExternalLibrary + * @dev A contract that uses the external libraries defined above. + * This contract serves as a test case for the Medusa fuzzer to verify proper + * library resolution, deployment ordering, and ABI handling. + */ +contract TestExternalLibrary { + /** + * @dev Echidna property test to verify library functionality + * Returns true if Library3 returns "Library", false otherwise + * @return true if library calls work correctly + */ + function echidna_library_call_works() public view returns(bool){ + return keccak256(abi.encodePacked(Library3.getLibrary())) == keccak256(abi.encodePacked("Library")); + } +} diff --git a/tests/solidity/basic/autolink.yaml b/tests/solidity/basic/autolink.yaml new file mode 100644 index 000000000..2ebafe97a --- /dev/null +++ b/tests/solidity/basic/autolink.yaml @@ -0,0 +1 @@ +cryticArgs: ["--compile-autolink"]