Skip to content

Deployment Validation Files aim to simplify Deployment Validation of Smart Contracts

License

Notifications You must be signed in to change notification settings

Uniblake/deployment_validation

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Deployment Validation Deployment Validation

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.

Content

  1. Prerequisites

  2. Installation

  3. Configuration

  4. Basic Usage

  5. Advanced Usage

  6. Common Problems

  7. Getting Help

  8. Examples

  9. Known Limitations and Bugs

  10. Supported Networks

Prerequisites

Depending on your use case, dv has different requirements.

  1. If you only want to validate a DVF received by a trusted signer, go to DVF validation.
  2. If you want to create DVF files, go to DVF creation.

DVF Validation

To successfully validate DVFs, you need access to the following APIs:

  1. An RPC node for the given chain ID.
  2. (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.

DVF Creation

To successfully create DVFs for on-chain smart contracts, you need access to the following APIs:

  1. An RPC archive node for the desired chain ID.
  2. (Optional) A Blockscout API key.
  3. (Optional) An Etherscan API key.

Please note the following restrictions/requirements:

  1. 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.
  2. A Blockscout API key allows for faster execution.
  3. Your RPC node must support either debug_traceTransaction or trace_transaction.
  4. Your RPC node should support debug_traceTransaction with opcode logger enabled. Otherwise, dv won't be able to decode mapping keys.
  5. 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:

  1. Rust
  2. Foundry
  3. (Optional) NodeJS

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.

Installation

Building From Source

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.

Using Docker

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.

Configuration

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.

Basic Usage

Create DVF

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.

Step 1 - Initialize a DVF

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.

Step 2 - Validate data and select constraints

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:

  1. Verify that all immutable variables (and possibly constructor arguments) have been set to the correct values depending on the project's security requirements.

  2. Select all storage variables that are critical to the security of the project and delete the rest.

  3. If necessary, update the comparison_operator and value (e.g., if the balance of a token should be at least a certain amount, you can set the GreaterThan comparison operator and the specified amount). The available operators are:

    • Equal.
    • GreaterThan.
    • LessThan.
    • GreaterThanOrEqual.
    • LessThanOrEqual.
  4. Select the events that are critical to the security of the project and delete the rest.

  5. If the deployment is not secure in its current state, set the insecure flag to true.

  6. (Optional) Fill in the unvalidated_metadata.

  7. (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.

Step 3 - Finalize the DVF

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

Step 4 - Test your DVF

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

Validate DVF

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

Update DVF

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.

Check Bytecode

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.

Handle Errors

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.

Use Build Cache

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:

  1. Circumvent the internal project compilation in case of issues.
  2. 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>

Advanced Usage

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.

Proxies and Delegated Calls

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.

Factories

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

References

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.

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:

  1. Allow dv validate to validate References. All referenced DVFs must therefore reside in the DVF storage.
  2. 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.

Locally Deployed Contracts

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.

Etherscan Verified Contracts

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 (solcversion, 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.

Common Problems

This section will be updated soon.

Getting Help

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:

  1. The project you were trying the compile (repository URI + commit hash).
  2. The full dv command.
  3. The full output of that command.
  4. The contents fo the generated DVF, if any.

For any other inquiries, this section will be updated with further contact possibilities soon.

Examples

This section will be updated soon.

Known Limitations and Bugs

  • Currently only solidity is supported.
  • Only projects with solc version starting from 0.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.

Supported Networks

  • All EVM compatible networks with the storage layout used by Ethereum.
  • Networks with a non-standard storage layout might not be supported.

About

Deployment Validation Files aim to simplify Deployment Validation of Smart Contracts

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Rust 84.3%
  • Solidity 14.0%
  • Other 1.7%