Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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; })
([
Expand Down
67 changes: 67 additions & 0 deletions lib/Echidna/Libraries.hs
Original file line number Diff line number Diff line change
@@ -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
8 changes: 6 additions & 2 deletions lib/Echidna/Solidity.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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(..))
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/test/Tests/Integration.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
64 changes: 64 additions & 0 deletions tests/solidity/basic/autolink.sol
Original file line number Diff line number Diff line change
@@ -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"));
}
}
1 change: 1 addition & 0 deletions tests/solidity/basic/autolink.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cryticArgs: ["--compile-autolink"]
Loading