Please note: This project is currently in BETA status
After a complex smart deployment it is very cumbersome to identify any potential (malicious) mistakes during deployment which later could put security at risk. Even harder, smart contract upgrades and configuration changes can result in security accidents. For example, audited smart contracts that are updated with unverified changes on-chain or deployed with a configuration that was not originally reflected in the audit might expose users to unforeseen consequences. These users are not immediately made aware of such changes and therefore still maintain high confidence towards the contract due to a published audit report.
Deployment Validation ensures that smart contracts have been deployed with the expected bytecode and configuration. Trusted entities can sign and publish Deployment Validation Files (DVF) which can subsequently be checked against the on-chain smart contracts to ensure a correct deployment at any given block number. Each DVF describes the correct state of exactly one smart contract and may reference other DVFs on whose correctness it depends. Thereby, Deployment Validation checks security not just during development, but also during deployment and later updates.
During DVF initialization, dv
compiles a given project (Foundry- or Hardhat-based) and compares the generated bytecode with the on-chain bytecode of a given address. This ensures that a deployed smart contract corresponds to a certain repository/commit combination (e.g., an audited version of the code).
dv
also automatically retrieves a smart contract's full decoded state and all events emitted since deployment until the end of a given block. DVF creators can choose which state and events are important and define appropriate constraints (e.g., equivalence to a certain value).
Once a DVF is published, any user can choose to trust the signer of that DVF and validate the contained bytecode and constraints against the on-chain smart contract at any given block number. As long as the DVF has been carefully crafted to ensure security of the smart contract, a successful validation indicates that the smart contract has been deployed correctly and, since then, not changed in any way that compromises security.
Depending on your use case, dv
has different requirements.
- If you only want to validate a DVF received by a trusted signer, go to DVF validation.
- If you want to create DVF files, go to DVF creation.
To successfully validate DVFs, you need access to the following APIs:
- An RPC node for the given chain ID.
- (Optional) An Etherscan API key.
To run dv
, you can either build from source it or use the pre-configured Docker image.
If you choose to install dv
, Rust has to be installed on your system.
Once you have it installed you can continue to validate.
To successfully create DVFs for on-chain smart contracts, you need access to the following APIs:
- An RPC archive node for the desired chain ID.
- (Optional) A Blockscout API key.
- (Optional) An Etherscan API key.
Please note the following restrictions/requirements:
- While Blockscout and Etherscan API keys are optional, at least one of them is required to determine the deployment transaction of a contract. If you provide neither, you are limited to local RPC nodes with less than 100 blocks.
- A Blockscout API key allows for faster execution.
- Your RPC node must support either
debug_traceTransaction
ortrace_transaction
. - Your RPC node should support
debug_traceTransaction
with opcode logger enabled. Otherwise,dv
won't be able to decode mapping keys. - For faster execution, your RPC node may support
debug_storageRangeAt
.
The RPC provider QuickNode supports all aforementioned requirements. A full list of supported RPC providers may be added here at a later point in time.
To run dv
, you can either build from source it or use the pre-configured Docker image.
If you choose to install dv
, the following dependencies have to be installed on your system:
NodeJS is only required if you are running dv
in a Hardhat project. Foundry is always required even when you are not interacting with any Foundry projects.
To install dv
, clone this repository and build:
git clone TODO: add repo URI
cd deployment-validation
cargo install --path .
This creates a binary at ~/.cargo/bin
. You can add the location to your PATH
with the following command:
echo "export PATH=$PATH:$HOME/.cargo/bin" >> ~/.profile
Depending on your system's configuration, this command might have to be adapted.
To run dv
with the pre-configured Docker image, clone this repository and run:
git clone TODO: add repo URI
cd deployment-validation
docker build -t dv .
The docker image can then be started from the directory containing all required files (DVFs and/or project folders):
docker run --rm -v $PWD:/home/dv/shared -it dv
The folder shared
in your Docker home directory now contains all files of the directory you executed the command in.
Before dv
can be used for validation and/or DVF creation, a configuration file has to be created. dv
searches for the file in ~/.dvf_config.json
by default.
If you want to store the file at a different location, the --config
parameter has to be used any time you run dv
:
dv --config <PATH> <COMMAND>
The config file can be generated interactively with the following command:
dv generate-config
To be able to sign DVFs, a "signer" configuration can be added during the interactive command. It should be noted that the address you use for signing should also be added to the "trusted signers" so that you are able to validate your own DVFs.
If you wish to create the file manually or change it at a later point in time, please refer to the configuration specification.
This section describes how to create a DVF for a simple smart contract. If the desired smart contract is a factory, proxy or requires any other special handling, please refer to Advanced Usage.
To create a DVF for a simple smart contract, run the following command:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> new.dvf.json
Replace the placeholders with:
<PROJECT_PATH>
: The root directory of the project on your local system.<ADDRESS>
: The on-chain address of the contract.<NAME>
: The name of the contract.
dv
compiles the Foundry project in <PROJECT_PATH>
, compares the compiled bytecode of <NAME>
with the bytecode of <ADDRESS>
deployed on the Ethereum Mainnet and decodes the storage as well as gathers all emitted events in the deployment block.
To check a contract on another EVM chain, you can pass the respective chain ID with --chainid
:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --chainid <CHAIN_ID> new.dvf.json
An RPC endpoint for the given <CHAIN_ID>
must be present in your configuration file.
If the project uses Hardhat instead of Foundry, you can pass the Hardhat environment with --env
:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --env hardhat new.dvf.json
If the Hardhat project does not store its compilation artifacts in the default directory, you can pass the correct directory with --artifacts
:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --env hardhat --artifacts <ARTIFACTS> new.dvf.json
If you have to use an external build-info (i.e., you don't want dv
to build the project), you can specify the path to the build-info directory with --buildcache
:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --buildcache <PATH_TO_BUILD_INFO> new.dvf.json
In many cases, deployments are not completed in one block as parameters may be set in subsequent transactions. To receive the storage at a later block (and emitted events up to that block), you can pass the desired block number with --initblock
:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --initblock <B> new.dvf.json
Please note that <B>
must be equal to or larger than the deployment block of the contract. Additionally, it is recommended to use only block numbers of finalized blocks in order to prevent the DVF containing wrong data due to possible re-orgs in the future.
After Step 1, a new JSON file has been created that contains the following data:
- Immutable variables.
- Constructor arguments.
- Critical storage variables.
- Critical events.
insecure
flag.
You must now perform the following tasks:
-
Verify that all immutable variables (and possibly constructor arguments) have been set to the correct values depending on the project's security requirements.
-
Select all storage variables that are critical to the security of the project and delete the rest.
-
If necessary, update the
comparison_operator
andvalue
(e.g., if thebalance
of a token should be at least a certain amount, you can set theGreaterThan
comparison operator and the specified amount). The available operators are:Equal
.GreaterThan
.LessThan
.GreaterThanOrEqual
.LessThanOrEqual
.
-
Select the events that are critical to the security of the project and delete the rest.
-
If the deployment is not secure in its current state, set the
insecure
flag totrue
. -
(Optional) Fill in the
unvalidated_metadata
. -
(Optional) Set an expiry timestamp in the
expiry_in_epoch_seconds
field.
For a detailed description of all fields contained in a DVF, please refer to the technical specification.
Once the DVF is validated against an on-chain smart contract, changes that do not satisfy the given constraints anymore result in the validation to fail. It is therefore important that the DVF only contains constraints that are not violated during normal activity (e.g., a constraint that requires the totalSupply
of a token to be a specific value does not work here). Additionally, the constraints should only be related to security. This means, any storage variables / events that would not compromise security in any way if changed / emitted should be deleted.
When the DVF is finished, you can sign it using the following command:
dv sign new.dvf.json
If you do not wish to sign the DVF, you can instead finalize it by generating an ID:
dv id new.dvf.json
Once your DVF is signed, it is ready to be shipped. However, you should first check that it validates correctly:
dv validate new.dvf.json
If you want to validate DVFs, you first have to decide which DVF publishers you can trust. This can include auditors or any other entities who you consider capable of understanding the intricacies of the smart contracts you want to validate.
The addresses of these signers have to be set in your configuration file, see Configuration for details.
After you have added the appropriate addresses, you can start validating any DVFs signed by them:
dv validate new.dvf.json
This will validate the DVF against any security-relevant changes the respective smart contract has undergone from its deployment to the end of the latest block on its chain. If you would like to perform the same task for a different end block, use the following command:
dv validate --validationblock <B> new.dvf.json
<B>
must be greater than the deployment block of the contract and smaller than or equal to the current block of the smart contract's chain.
If you wish to validate DVFs that have not been signed, you can add the --allowuntrusted
flag:
dv validate --allowuntrusted new.dvf.json
Please note: The update
command is currently only updating existing storage variables in a DVF and might not be suitable for fully updating a DVF to the current state of a smart contract. This behavior will be changed in future releases.
To update the values of storage variables in a DVF to the state of the latest block and gather all events up to this block, run the following command:
dv update new.dvf.json
If you want to update storage variables and events to a certain block number, you can pass the desired block number with --validationblock
:
dv update --validationblock <B> new.dvf.json
<B>
must be greater than the deployment block of the contract and smaller than or equal to the current block of the smart contract's chain.
For a simple check that the compiled bytecode of a certain project is equal to the on-chain bytecode of an address, you can use the following command:
dv bytecode-check --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> new.dvf.json
Replace the placeholders with:
<PROJECT_PATH>
: The root directory of the project on your local system.<ADDRESS>
: The on-chain address of the contract.<NAME>
: The name of the contract.
dv
compiles the Foundry project in <PROJECT_PATH>
and compares the generated bytecode of <NAME>
with the bytecode of <ADDRESS>
on the Ethereum Mainnet.
To check a contract on another EVM chain, you can pass the respective chain ID with --chainid
:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --chainid <CHAIN_ID> new.dvf.json
An RPC endpoint for the given <CHAIN_ID>
must be present in your configuration file.
If the project uses Hardhat instead of Foundry, you can pass the Hardhat environment with --env
:
dv bytecode-check --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --env hardhat new.dvf.json
If the Hardhat project does not store its compilation artifacts in the default directory, you can pass the correct directory with --artifacts
:
dv bytecode-check --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --env hardhat --artifacts <ARTIFACTS> new.dvf.json
To check the bytecode at a specific block, you can pass the desired block number with --initblock
:
dv bytecode-check --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --initblock <B> new.dvf.json
Please note that <B>
must be equal to or larger than the deployment block of the contract.
If something goes wrong during a run of dv
, you can add the --verbose
option to get additional information:
dv --verbose <COMMAND> new.dvf.json
To get even more information, you can add it a second time:
dv --verbose --verbose <COMMAND> new.dvf.json
Please refer to section Common Problems for help with understanding the output.
If your problem cannot be solved by yourself or if you have found a bug (e.g., dv
crashed), please refer to section Getting Help.
By default dv
compiles the full project every time you run the init
or bytecode-check
commands.
You can pass an external build-info path (containing the compiler output) to dv
using the --buildcache
flag. This can be used to:
- Circumvent the internal project compilation in case of issues.
- Skip subsequent compilations if you want to create DVFs for multiple contracts in a large project.
For the second case, you can use the generate-build-cache
command to generate a persisted build-info path that can be passed to --buildcache
:
dv generate-build-cache --project <PROJECT_PATH>
You can also use the command for hardhat projects using --env
:
dv generate-build-cache --project <PROJECT_PATH> --env hardhat
If the Hardhat project does not store its compilation artifacts in the default directory, you can pass the correct directory with --artifacts
:
dv generate-build-cache --project <PROJECT_PATH> --env hardhat --artifacts <ARTIFACTS>
Not all projects can be easily validated by validating single contracts. If the smart contracts in the project you are validating have dependencies to other contracts that are security relevant, please refer to this section.
Contracts calling other contracts with delegatecall
inherit their storage layout and event ABI. For this reason, validating such contracts requires to validate their on-chain state against the storage layout and event ABI of both the contract that performs the delegatecall
as well as the contract that is called. This is, however, only necessary if the called contract contains any storage variables and events.
To initialize a DVF for such contracts, run the following command:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --implementation <IMPL_NAME> new.dvf.json
Compared to the basic usage in Create DVF, the name of the implementation contract <IMPL_NAME>
is additionally passed with --implementation
.
As it is possible that the implementation contract resides in another project, you can additionally pass this project's directory with --implementationproject
:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --implementation <IMPL_NAME> --implementationproject <IMPL_PROJECT_PATH> new.dvf.json
If your implementation project uses Hardhat, you can pass the Hardhat environment with --implementationenv
:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --implementation <IMPL_NAME> --implementationproject <IMPL_PROJECT_PATH> --implementationenv hardhat new.dvf.json
If the Hardhat project does not store its compilation artifacts in the default directory, you can pass the correct directory with --implementationartifacts
:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --implementation <IMPL_NAME> --implementationproject <IMPL_PROJECT_PATH> --implementationenv hardhat --implementationartifacts <IMPL_ARTIFACTS> new.dvf.json
If you have to use an external build-info (i.e., you don't want dv
to build the implementation project), you can specify the path to the implementation project's build-info directory with --implementationbuildcache
:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --implementation <IMPL_NAME> --implementationproject <IMPL_PROJECT_PATH> --implementationbuildcache <IMPL_BUILD_CACHE> new.dvf.json
Please note that this does not validate the implementation contract itself. If there are any security risks associated with the implementation contracts, you should create another DVF for it and then create a reference (see References) from the original DVF to the implementation contract's DVF.
Certain factory contracts contain the bytecode of the contracts they are deploying inside their own bytecode. If this is the case, bytecode validation of the factory can be problematic due to the metadata of the child contract: During the local re-compilation, the metadata can potentially differ from the metadata generated by the original compilation that was deployed on-chain. In this case, a bytecode check can fail even though the relevant bytecode is, in fact, identical. For this reason, you can use the flag --factory
to exclude such internal metadata from bytecode checks:
dv init --project <PROJECT_PATH> --address <ADDRESS> --contractname <NAME> --factory new.dvf.json
In some cases, the security of a contract depends on the security of another contract. Examples include:
- A proxy depends on the security of its implementation.
- A contract with privileged functions depends on the correct configuration of a multi-sig smart contract wallet that holds the respective privileges.
- A contract calling functions on another proxied contract depends on the fact that the other contract does not change its implementation (e.g., because this could introduce reentrancy vectors).
With references, dependencies between multiple DVFs can be created. A DVF containing a reference to another DVF only validates, if the other DVF also validates.
To create a reference to another DVF, run the following command:
dv add-reference --id <REF_DVF_ID> --contractname <REF_CONTRACT_NAME> new.dvf.json
<REF_DVF_ID>
must be the generated ID of the referenced DVF. This means that the other DVF has to be already finalized (either via dvf sign
or dvf id
). <REF_CONTRACT_NAME>
is the name of the contract the other DVF describes.
Validating DVFs with references requires all associated DVFs to be included in the Registry.
The registry is an internal representation of all DVFs in your chosen DVF storage (i.e., the directory path in your config's dvf_storage
setting). dv
automatically loads all DVFs from the DVF storage for two purposes:
- Allow
dv validate
to validate References. All referenced DVFs must therefore reside in the DVF storage. - Resolve the contract names of known addresses in
dv init
. All addresses in the storage, the immutable variables, or events of a smart contract that match with the address of an existing DVF in the storage are automatically decoded to the respective contract name.
If you deployed contracts in a local testnet, e.g. anvil, those can also be validated as long as those use chain ID 1337 or 31337. Simply specify the endpoint for the network, e.g. "http://127.0.0.1:8545" for 31337, and run all the commands as you normally would.
If you do not wish to initialize a DVF with a specific project directory, you can use fetch-from-etherscan
to instead create a project from a verified Etherscan contract automatically:
fetch-from-etherscan --project <PROJECT_PATH> --address <ADDRESS>
Replace the placeholders with:
<PROJECT_PATH>
: The directory where the Foundry project should be created.<ADDRESS>
: The on-chain address of the contract.
fetch-from-etherscan
creates a new Foundry project with all necessary parameters (solc
version, evm version, etc.) and adds all verified Etherscan contracts. The resulting project should now produce the same bytecode as the on-chain version. It can thus be used with dv init
flawlessly.
To check a contract on another EVM chain, you can pass the respective chain ID with --chainid
:
fetch-from-etherscan --project <PROJECT_PATH> --address <ADDRESS> --chainid <CHAIN_ID>
An RPC endpoint for the given <CHAIN_ID>
must be present in your configuration file.
Please note that Foundry's forge clone
provides similar functionality but is currently not suitable for this task due to a bug.
This section will be updated soon.
If you have found a bug or have a feature request that is not covered in Known Limitations and Bugs, please add an issue to this GitHub repository.
Make sure to add the following contents:
- The project you were trying the compile (repository URI + commit hash).
- The full
dv
command. - The full output of that command.
- The contents fo the generated DVF, if any.
For any other inquiries, this section will be updated with further contact possibilities soon.
This section will be updated soon.
- Currently only solidity is supported.
- Only projects with
solc
version starting from0.5.13
are supported due to the lack of generated storage layout in older versions (see solc release 0.5.13). - The RPC endpoints automatically parsed in
dv generate-config
are not guaranteed to be compatible. - As detailed above, many public RPCs are not or only partially supported for DVF creation.
- Finding the deployment transaction of a contract currently requires either Blockscout or Etherscan API keys to collect all relevant information.
- Contracts performing
delegatecall
to more than one other contract are currently not supported. dv update
currently only updates values of existing storage variables in the DVF and does not add newly added storage values.- Multiple contracts with the same name compiled with different compiler versions in one project are not supported.
- Static mapping keys (e.g.,
mapping[0]
) can currently not be decoded. - Empty-string mapping keys can currently not be decoded correctly.
- Big transaction traces (
debug_traceTransaction
with opcode logger) of multiple GB may cause a crash. - Proxy Contracts without events when changing the implementation cannot be accurately secured, as implementation changes could be missed.
- Successfully running validation against an non-finalized block at height H does not guarantee, validity at height H.
- Missing optimizations can cause longer waiting times than necessary.
- Celoscan.io is currently not supported.
- All EVM compatible networks with the storage layout used by Ethereum.
- Networks with a non-standard storage layout might not be supported.