dapp
is a tool for building, testing and deploying smart contracts from the comfort of the command line.
As opposed to other tools, it does not use rpc
to execute transactions. Instead,
it invokes the hevm
cli directly. This is faster, and allows for a lot of flexibility
that isn't available in rpc
, such as fuzz testing, symbolic execution, or cheat codes to modify mainnet state.
Table of Contents
dapp is distributed as part of the Dapp tools suite.
Let's create a new dapp
project. We make a new directory and initialize the dapp
skeleton structure:
mkdir dapptutorial
cd dapptutorial
dapp init
This creates two contracts, Dapptutorial.sol
and Dapptutorial.t.sol
in the src
subdirectory and installs our testing library ds-test
in the lib
subdirectory.
Dapptutorial.t.sol
is a testing contract with two trivial tests, which we can run with dapp test
.
For the sake of this tutorial, let's change Dapptutorial.sol
to a simple vault with an eth bounty that can be accessed by giving the password 42:
pragma solidity ^0.8.6;
contract Dapptutorial {
receive() external payable {
}
function withdraw(uint password) public {
require(password == 42, "Access denied!");
payable(msg.sender).transfer(address(this).balance);
}
}
Compile the contract by running dapp build
. If you didn't make any mistakes, you should simply see:
+ dapp clean
+ rm -rf out
Let's write some tests for our vault. Change Dapptutorial.t.sol
to the following. We'll go over whats going on in the next paragraph.
import {DSTest} from "ds-test/test.sol";
import {Dapptutorial} from "./Dapptutorial.sol";
contract DapptutorialTest is DSTest {
Dapptutorial dapptutorial;
function setUp() public {
dapptutorial = new Dapptutorial();
}
function test_withdraw() public {
payable(address(dapptutorial)).transfer(1 ether);
uint preBalance = address(this).balance;
dapptutorial.withdraw(42);
uint postBalance = address(this).balance;
assertEq(preBalance + 1 ether, postBalance);
}
function testFail_withdraw_wrong_pass() public {
payable(address(dapptutorial)).transfer(1 ether);
uint preBalance = address(this).balance;
dapptutorial.withdraw(1);
uint postBalance = address(this).balance;
assertEq(preBalance + 1 ether, postBalance);
}
// allow sending eth to the test contract
receive() external payable {}
}
In the setUp()
function, we are deploying the Dapptutorial
contract.
All following tests are run against the poststate of the setUp()
function.
The test_withdraw
function first deposits 1 eth and then withdraws it, by giving the correct password.
We check that the call was successful by comparing the pre and post balance of the testing account using
assertEq
. You can try changing the right hand side to postBalance + 1
and see what happens.
Finally, we are testing the case where the wrong password is given in testFail_withdraw_wrong_pass
.
Any function prefixed with testFail
is expected to fail, either with a revert
or by violating an
assertion.
Finally, since a successful call to withdraw
sends eth to the testing contract, we have to remember to
implement a receive
function in it.
For more debugging information, run dapp test
with the -v
flag to print the calltrace for failing tests,
or enter the interactive debugger by running dapp debug
.
Now let's try something more interesting - property based testing and symbolically executed tests.
We can generailize our test_withdraw
function to not use the hardcoded 1 ether
, but instead take
the value as a parameter:
function test_withdraw(uint amount) public {
payable(address(dapptutorial)).transfer(amount);
uint preBalance = address(this).balance;
dapptutorial.withdraw(42);
uint postBalance = address(this).balance;
assertEq(preBalance + amount, postBalance);
}
A test that takes at least one parameters is interpreted as a "property based test", or "fuzz test", and will
be run multiple times with different values given to the parameters. The number of times each test is run can be
configured by the --fuzz-runs
flag and defaults to 100.
Running this test with dapp test -v
, we see that this test actually fails with error BalanceTooLow
for very high values of amount
.
By default, the testing contract is given a balance of 2**96
wei, so we have to restrict the type of amount
to uint96
to make sure we don't try to transfer more than we have:
function test_withdraw(uint96 amount) public {
payable(address(dapptutorial)).transfer(amount);
uint preBalance = address(this).balance;
dapptutorial.withdraw(42);
uint postBalance = address(this).balance;
assertEq(preBalance + amount, postBalance);
}
If a counterexample is found, it can be replayed or analyzed in the debugger using the --replay
flag.
While property based testing runs each function repeatedly with new input values, symbolic execution leaves these values symbolic and tries to explore each possible execution path. This gives a stronger guarantee and is more powerful than property based testing, but is also more difficult, especially for complicated functions.
Continuing with our vault example, imagine that we forgot the password and did not have the source available.
We can symbolically explore all possibilities to find the one that lets us withdraw by writing a proveFail
test:
function proveFail_withdraw(uint guess) public {
payable(address(dapptutorial)).transfer(1 ether);
uint preBalance = address(this).balance;
dapptutorial.withdraw(guess);
uint postBalance = address(this).balance;
assertEq(preBalance + 1 ether, postBalance);
}
When we run this with dapp test
, we are given a counterexample:
Failure: proveFail_withdraw(uint256)
Counterexample:
result: Successful execution
calldata: proveFail_withdraw(42)
which demonstrates that if we give the password 42
, it is possible to withdraw from the vault.
The symbolic execution engine is backed by an SMT solver. When symbolically executing more complex tests you may encounter test failures with an SMT Query Timeout
message. In this case, consider increasing the smt timeout using the --smttimeout
flag or DAPP_TEST_SMTTIMEOUT
environment variable (the default timeout is 60000 ms). Note that this timeout is per smt query not per test, and that each test may execute multiple queries (at least one query for each potential path through the test method).
For more reading on property based testing and symbolic execution, see this tutorial on the Ethereum Foundation blog.
While other forms of tests are always run against the post state of the setUp()
function in the testing contract,
it can be also be useful to check whether a property is satisfied at every possible contract state. This can be done with
the invariant*
testing type. When running an invariant test, hevm will invoke any state mutating function from all addresses returned
by a call to targetContracts()
, if such a function exists in the testing contracts. If no such method exists, it will invoke methods from
any non-testing contract available after the setUp()
function has been run, checking the invariant*
after each run.
The --depth
parameter determines how many transactions deep each test will run, while the --fuzz-runs
parameter
determines how many times the whole process is repeated.
Note that a revert in any of the randomly generated call will not trigger a test failure. The goal of invariant tests is to find a state change that results in a violation of the assertions defined in the body of the test method, and since reverts do not result in a state change, they can be safely ignored. Reverts within the body of the invariant*
test method will however still cause a test failure.
Example:
function invariant_totalSupply() public {
assertEq(token.totalSupply(), initialTotalSupply);
}
If a counterexample is found, it can be replayed or analyzed in the debugger using the --replay
flag.
If you are using the standard JSON input mode and its field settings.modelChecker.engine
is all
, bmc
or chc
, Solidity's SMTChecker will be invoked when you run dapp build
.
If you wish to use that mode, these steps are recommended:
- Run the usual compilation
- Generate a separate input JSON with the SMTChecker enabled:
export DAPP_SMTCHECKER=1 && dapp mk-standard-json &> dapp_smtchecker.json
- Modify
settings.modelChecker
in the new JSON input accordingly. It is recommended that you use the contracts fieldsettings.modelChecker.contracts
to specify the main contracts you want to verify. - Tell
dapp
to use the new JSON as input:export DAPP_STANDARD_JSON=./dapp_smtchecker.json
- Run
dapp build
You may also want to change the settings.modelChecker.timeout
and/or other fields in different runs.
You can test how your contract interacts with already deployed contracts by
letting the testing state be fetched from rpc with the --rpc
flag.
Running dapp test
with the --rpc
flag enabled will cause every state fetching operation
(such as SLOAD, EXTCODESIZE, CALL*, etc.) to request the state from $ETH_RPC_URL
.
For example, if you want to try out wrapping ETH you could define WETH in the setUp()
function:
import "ds-test/test.sol";
interface WETH {
function balanceOf(address) external returns (uint);
function deposit() external payable;
}
contract WethTest is DSTest {
WETH weth;
function setUp() public {
weth = WETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
}
function testWrap() public {
assertEq(weth.balanceOf(address(this)), 0);
weth.deposit{value :1 ether}();
assertEq(weth.balanceOf(address(this)), 1 ether);
}
}
With ETH_RPC_URL
set, you can run dapp test --rpc
on this test or dapp debug --rpc
to step through
the testWrap
function in the interactive debugger.
It is often useful to modify the state for testing purposes, for example to grant the testing contract
with a balance of a particular token. This can be done using hevm cheat codes
.
To deploy a contract, you can use dapp create
:
dapp create Dapptutorial [<constructorArgs>] [<options>]
The --verify
flag verifies the contract on etherscan (requires ETHERSCAN_API_KEY
).
The commands of dapp
can be customized with environment variables or flags.
These variables can be set at the prompt or in a .dapprc
file.
Below is a list of the environment variables recognized by dapp
. You can additionally control
various block parameters when running unit tests by using the hevm specific environment
variables.
Variable | Default | Synopsis |
---|---|---|
DAPP_SRC |
src |
Directory for the project's Solidity contracts |
DAPP_LIB |
lib |
Directory for installed Dapp packages |
DAPP_OUT |
out |
Directory for compilation artifacts |
DAPP_ROOT |
. |
Root directory of compilation |
DAPP_SOLC_VERSION |
0.8.6 |
Solidity compiler version to use |
DAPP_SOLC |
n/a | solc binary to use |
DAPP_LIBRARIES |
automatically deployed | Library addresses to link to |
DAPP_SKIP_BUILD |
n/a | Avoid compiling this time |
DAPP_COVERAGE |
n/a | Print coverage data |
DAPP_LINK_TEST_LIBRARIES |
1 when testing; else 0 |
Compile with libraries |
DAPP_VERIFY_CONTRACT |
yes |
Attempt Etherscan verification |
DAPP_ASYNC |
n/a | Set to yes to skip waiting for etherscan verification to succeed |
DAPP_STANDARD_JSON |
$(dapp mk-standard-json) |
Solidity compilation options |
DAPP_SMTCHECKER |
n/a | Set to 1 to output the default model checker settings when using dapp mk-standard-json . Running dapp build will invoke the SMTChecker. |
DAPP_REMAPPINGS |
$(dapp remappings) |
Solidity remappings |
DAPP_BUILD_OPTIMIZE |
0 |
Activate Solidity optimizer (0 or 1 ) |
DAPP_BUILD_OPTIMIZE_RUNS |
200 |
Set the optimizer runs |
DAPP_VIA_IR |
0 |
Change compilation pipeline to go through the Yul intermediate representation (0 or 1 ) |
DAPP_TEST_MATCH |
n/a | Only run test methods matching a regex |
DAPP_TEST_VERBOSITY |
0 |
Sets how much detail dapp test logs. Verbosity 1 shows traces for failing tests, 2 shows logs for all tests, 3 shows traces for all tests |
DAPP_TEST_FFI |
0 |
Allow use of the ffi cheatcode in tests (0 or 1 ) |
DAPP_TEST_FUZZ_RUNS |
200 |
How many iterations to use for each property test in your project |
DAPP_TEST_DEPTH |
20 |
Number of transactions to sequence per invariant cycle |
DAPP_TEST_SMTTIMEOUT |
60000 |
Timeout passed to the smt solver for symbolic tests (in ms, and per smt query) |
DAPP_TEST_MAX_ITERATIONS |
n/a | The number of times hevm will revisit a particular branching point when symbolically executing |
DAPP_TEST_SOLVER |
z3 |
Solver to use for symbolic execution (cvc4 or z3 ) |
DAPP_TEST_MATCH |
n/a | Regex used to determine test methods to run |
DAPP_TEST_COV_MATCH |
n/a | Regex used to determine which files to print coverage reports for. Prints all imported files by default (excluding tests and libs). |
DAPP_TEST_REPLAY |
n/a | Calldata for a specific property test case to replay in the debugger |
HEVM_RPC |
n/a | Set to yes to have hevm fetch state from rpc when running unit tests |
ETH_RPC_URL |
n/a | The url of the rpc server that should be used for any rpc calls |
DAPP_TESTNET_RPC_PORT |
8545 |
Which port to expose the rpc server on when running dapp testnet |
DAPP_TESTNET_RPC_ADDRESS |
127.0.0.1 |
Which ip address to bind the rpc server to when running dapp testnet |
DAPP_TESTNET_CHAINID |
99 |
Which chain id to use when running dapp testnet |
DAPP_TESTNET_PERIOD |
0 |
Blocktime to use for dapp testnet . 0 means blocks are produced instantly as soon as a transaction is received |
DAPP_TESTNET_ACCOUNTS |
0 |
How many extra accounts to create when running dapp testnet (At least one is always created) |
DAPP_TESTNET_gethdir |
$HOME/.dapp/testnet |
Root directory that should be used for dapp testnet data |
DAPP_TESTNET_SAVE |
n/a | Name of the subdirectory under ${DAPP_TESTNET_gethdir}/snapshots where the chain data from the current dapp testnet invocation should be saved |
DAPP_TESTNET_LOAD |
n/a | Name of the subdirectory under ${DAPP_TESTNET_gethdir}/snapshots from which dapp testnet chain data should be loaded |
DAPP_BUILD_EXTRACT |
n/a | Set to a non null value to output .abi , .bin and .bin-runtime when using dapp build . Uses legacy build mode |
DAPP_BUILD_LEGACY |
n/a | Set to a non null value to compile using the --combined-json flag. This is provided for compatibility with older workflows |
A global (always loaded) config file is located in ~/.dapprc
. A local .dapprc
can also be defined in your project's root, which overrides variables in the global config.
Whenever you run a dapp
command the .dapprc
files are sourced in order (global first, then the one in the current working directory, if it exists). If you wish to set configuration variables, you must use export
as below:
export DAPP_SOLC_VERSION=0.8.6
export DAPP_REMAPPINGS=$(cat remappings.txt)
export DAPP_BUILD_OPTIMIZE=1
export DAPP_BUILD_OPTIMIZE_RUNS=1000000000
export DAPP_TEST_VERBOSITY=1
Under the hood .dapprc
is interpreted as a shell script, which means you can add additional scripting logic which will be run whenever you use dapp
. For example if you wanted to fuzz for many iterations in CI and only a few locally you could add this to your .dapprc
:
if [ "$CI" == "true" ]
then
export DAPP_TEST_FUZZ_RUNS=1000000 # In CI we want to fuzz for a long time.
else
export DAPP_TEST_FUZZ_RUNS=1000 # When developing locally we only want to fuzz briefly.
fi
There are multiple places to specify configuration options. They are read with the following precedence:
- command line flags
- local
.dapprc
- global
.dapprc
- locally set environment variables
You can specify a custom solc
version to run within dapp
with dapp --use <arg>
.
If the argument is of the form solc:x.y.z
, the appropriate solc version will temporarily installed.
If the argument contains a /
, it is interpreted as a path to a solc binary to be used.
You may also specify a solc version using the DAPP_SOLC_VERSION
environment variable, which is equivalent to running dapp --use solc:${DAPP_SOLC_VERSION}
manually.
You can install any supported solc
"standalone" (i.e. add it to your $PATH
) with:
nix-env -iA solc-static-versions.solc_x_y_z \
-if https://github.com/dapphub/dapptools/tarball/master
For a list of the supported solc
versions, check solc-static-versions.nix
.
dapp-init -- bootstrap a new dapp
Usage: dapp init
Initializes the current directory to the default dapp
structure,
installing ds-test
and creating two boilerplate contracts in the src
directory.
dapp-build -- compile the source code
Usage: dapp build [--extract]
--extract: After building, write the .abi, .bin and .bin-runtime. Implies `--legacy`
files from the solc json into $DAPP_OUT. Beware of contract
name collisions. This is provided for compatibility with older
workflows.
--optimize: activate the solidity optimizer.
--via-ir: change compilation pipeline to go through the Yul intermediate representation
--legacy: Compile using the `--combined-json` flag. Some options are
missing from this format. This is provided for compatibility with older
workflows.
Compiles the contracts in the src
directory.
The compiler options of the build are generated by the dapp mk-standard-json
command,
which infers most options from the project structure. For more customizability, you can define your own configuration json
by setting the file to the environment variable DAPP_STANDARD_JSON
.
By default, dapp build
uses dapp remappings
to resolve Solidity import paths.
You can override this with the DAPP_REMAPPINGS
environment variable.
Usage: dapp test [<options>]
Options:
-v, --verbose trace output for failing tests
--coverage print coverage data
--verbosity <number> sets the verbosity to <number>
--depth=<number> number of transactions to sequence per invariant cycle
--fuzz-runs <number> number of times to run fuzzing tests
--replay <string> rerun a particular test case
-m, --match <string> only run test methods matching regex
--cov-match <string> only print coverage for files matching regex
RPC options:
--rpc fetch remote state via ETH_RPC_URL
--rpc-url <url> fetch remote state via <url>
--rpc-block <number> block number (latest if not specified)
SMT options:
--smttimeout <number> timeout passed to the smt solver in ms (default 60000)
--solver <string> name of the smt solver to use (either "z3" or "cvc4")
--max-iterations <number> number of times we may revisit a particular branching point during symbolic execution
dapp tests are written in Solidity using the ds-test
module. To install it, run
dapp install ds-test
Every contract which inherits from DSTest
will be treated as a test contract, if it has a setUp()
function, it will be run before every test.
Every function prefixed with test
is expected to succeed, while functions
prefixed by testFail
are expected to fail.
Functions prefixed with prove
are run symbolically, expecting success while functions
prefixed proveFail
are run symbolically expecting failure.
The -v
flag prints call traces for failing tests, --verbosity 2
will show ds-test
events for
all tests, while --verbosity 3
will show call traces for all tests.
If you provide --rpc
, state will be fetched via rpc. Local changes take priority.
You can configure the testing environment using hevm specific environment variables.
To modify local state even more, you can use hevm cheat codes.
If your test function takes arguments, they will be randomly instantiated and the function will be run multiple times.
The number of times run is configurable using --fuzz-runs
.
To step through a test in hevm
interactive debugger, use dapp debug
.
dapp test --match <regex>
will only run tests that match the given
regular expression. This will be matched against the file path of the
test file, followed by the contract name and the test method, in the
form src/my_test_file.sol:TestContract.test_name()
. For example, to
only run tests from the contract ContractA
:
dapp test --match ':ContractA\.'
To run all tests, from all contracts, that contain either foo
or bar
in the test name:
dapp test --match '(foo|bar)'
To only run tests called 'test_this()' from TheContract
in the
src/test/a.t.sol
file:
dapp test --match 'src/test/a\.t\.sol:TheContract\.test_this\(\)'
By default, dapp test
also recompiles your contracts.
To skip this, you can set the environment variable DAPP_SKIP_BUILD=1
.
If you have any libraries in DAPP_SRC
or DAPP_LIB
with nonzero bytecode,
they will be deployed locally and linked to by any contracts referring to them.
This can be skipped by setting DAPP_LINK_TEST_LIBRARIES=0
.
dapp-debug -- run unit tests interactively (hevm)
Usage: dapp debug [<options>]
Options:
--rpc fetch remote state via ETH_RPC_URL
--rpc-url=<url> fetch remote state via <url>
--rpc-block=<number> block number (latest if not specified)
Enters the interactive debugger. See the hevm README for key bindings for navigation.
dapp-create -- deploy a compiled contract (--verify on Etherscan)
Usage: dapp create <contractname> or
dapp create <path>:<contractname>
Add --verify and export your ETHERSCAN_API_KEY to auto-verify on Etherscan
dapp-address -- determine address of newly generated contract
Usage: dapp address <sender> <nonce>
dapp-install -- install a smart contract library
Usage: dapp install <lib>
<lib> may be:
- a Dapphub repo (ds-foo)
- the URL of a Dapphub repo (https://github.com/dapphub/ds-foo)
- a path to a repo in another Github org (org-name/repo-name)
You can also specify a version (or branch / commit hash) for the repository by
suffixing the URL with @<version>
. dapp install
will then proceed to
clone the repository and then git checkout --recurse-submodules $version
.
If the project you want to install does not follow the typical dapp
project structure,
you may need to configure the DAPP_REMAPPINGS
environment variable to be able to find
it. For an example, see this repo.
dapp-uninstall -- remove a smart contract library
Usage: dapp uninstall <lib>
dapp-update -- fetch all upstream lib changes
Usage: dapp update [<lib>]
Updates a project submodule in the lib
subdirectory.
dapp-snapshot -- creates a snapshot of each test's gas usage
Usage: dapp snapshot
Saves a snapshot of each concrete test's gas usage in a .gas-snapshot
file.
dapp-check-snapshot -- check snapshot is up to date
Usage: dapp check-snapshot
Runs dapp snapshot
and exits with an error code if its output does not match the current .gas-snapshot
file.
dapp-upgrade -- pull & commit all upstream lib changes
Usage: dapp upgrade [<lib>]
Spins up a geth testnet.
dapp-verify-contract -- verify contract source on etherscan
Usage: dapp verify-contract <path>:<contractname> <address> [constructorArgs]
Example: dapp verify-contract src/auth/authorities/RolesAuthority.sol:RolesAuthority 0x9ed0e..
Requires ETHERSCAN_API_KEY
to be set.
seth chain
will be used to determine on which network the contract is to be verified.
Automatically run when the --verify
flag is passed to dapp create
.
Generates a Solidity settings input json using the structure of the current directory.
The following environment variables can be used to override settings:
DAPP_SRC
DAPP_REMAPPINGS
DAPP_BUILD_OPTIMIZE
DAPP_BUILD_OPTIMIZE_RUNS
DAPP_LIBRARIES
DAPP_SMTCHECKER