├── .dapprc ├── .env.example ├── .gitattributes ├── .gitignore ├── .gitmodules ├── .gitpod.yml ├── LICENSE ├── Makefile ├── README.md ├── img ├── chainlink-dapptools.png └── chainlink-foundry.png ├── remappings.txt ├── scripts ├── common.sh ├── contract-size.sh ├── deploy.sh ├── estimate-gas.sh ├── helper-config.sh ├── run-temp-testnet.sh └── test-deploy.sh ├── shell.nix └── src ├── APIConsumer.sol ├── KeepersCounter.sol ├── PriceFeedConsumer.sol ├── VRFConsumerV2.sol └── test ├── APIConsumer.t.sol ├── KeepersCounter.t.sol ├── PriceFeedConsumer.t.sol ├── VRFConsumerV2.t.sol ├── mocks ├── LinkToken.sol ├── MockOracle.sol ├── MockV3Aggregator.sol └── MockVRFCoordinatorV2.sol └── utils └── Cheats.sol /.dapprc: -------------------------------------------------------------------------------- 1 | # Make dependencies available 2 | export DAPP_REMAPPINGS=$(cat remappings.txt) 3 | 4 | export DAPP_SOLC_VERSION=0.8.7 5 | # If you're getting an "invalid character at offset" error, comment this out. 6 | export DAPP_LINK_TEST_LIBRARIES=0 7 | export DAPP_TEST_VERBOSITY=1 8 | export DAPP_TEST_SMTTIMEOUT=500000 9 | 10 | # Optimize your contracts before deploying to reduce runtime execution costs. 11 | # Check out the docs to learn more: https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html#optimizer-options 12 | # export DAPP_BUILD_OPTIMIZE=1 13 | # export DAPP_BUILD_OPTIMIZE_RUNS=1000000 14 | 15 | # set so that we can deploy to local node w/o hosted private keys 16 | export ETH_RPC_ACCOUNTS=true 17 | 18 | if [ "$DEEP_FUZZ" = "true" ] 19 | then 20 | export DAPP_TEST_FUZZ_RUNS=50000 # Fuzz for a long time if DEEP_FUZZ is set to true. 21 | else 22 | export DAPP_TEST_FUZZ_RUNS=100 # Only fuzz briefly if DEEP_FUZZ is not set to true. 23 | fi 24 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | export ALCHEMY_API_KEY=YOUR_API_KEY 2 | export ETH_FROM=YOUR_DEFAULT_SENDER_ACCOUNT 3 | export ETHERSCAN_API_KEY=YOUR_API_KEY 4 | 5 | # # Optional Default Network 6 | # export ETH_RPC_URL=http://localhost:8545 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .dapprc linguist-language=Shell 2 | *.sol linguist-language=Solidity 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | 4 | # Dapptools 5 | out/ 6 | .env 7 | cache 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/ds-test"] 2 | path = lib/ds-test 3 | url = https://github.com/dapphub/ds-test 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 7 | ignore = dirty 8 | [submodule "lib/chainlink-brownie-contracts"] 9 | path = lib/chainlink-brownie-contracts 10 | url = https://github.com/smartcontractkit/chainlink-brownie-contracts 11 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - name: dAppTools 3 | command: curl https://dapp.tools/install | sh && make 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # include .env file and export its env vars 2 | # (-include to ignore error if it does not exist) 3 | -include .env 4 | 5 | install: solc update npm 6 | 7 | # dapp deps 8 | update:; dapp update 9 | 10 | # install solc version 11 | # example to install other versions: `make solc 0_8_2` 12 | SOLC_VERSION := 0_8_7 13 | solc:; nix-env -f https://github.com/dapphub/dapptools/archive/master.tar.gz -iA solc-static-versions.solc_${SOLC_VERSION} 14 | 15 | # Build & test 16 | build :; dapp build 17 | test :; dapp test # --ffi # enable if you need the `ffi` cheat code on HEVM 18 | clean :; dapp clean 19 | lint :; yarn run lint 20 | estimate :; ./scripts/estimate-gas.sh ${contract} 21 | size :; ./scripts/contract-size.sh ${contract} 22 | 23 | # Deployment helper 24 | deploy :; ./scripts/deploy.sh 25 | 26 | # TODO 27 | # mainnet 28 | deploy-mainnet: export ETH_RPC_URL = $(call network,mainnet) 29 | deploy-mainnet: export NETWORK=mainnet 30 | deploy-mainnet: check-api-key deploy 31 | 32 | # kovan 33 | deploy-kovan: export ETH_RPC_URL = $(call network,kovan) 34 | deploy-kovan: export NETWORK=kovan 35 | deploy-kovan: check-api-key deploy 36 | 37 | # rinkeby 38 | deploy-rinkeby: export ETH_RPC_URL = $(call network,rinkeby) 39 | deploy-rinkeby: export NETWORK=rinkeby 40 | deploy-rinkeby: check-api-key deploy 41 | 42 | check-api-key: 43 | ifndef ALCHEMY_API_KEY 44 | $(error ALCHEMY_API_KEY is undefined) 45 | endif 46 | 47 | 48 | # Returns the URL to deploy to a hosted node. 49 | # Requires the ALCHEMY_API_KEY env var to be set. 50 | # The first argument determines the network (mainnet / rinkeby / ropsten / kovan / goerli) 51 | define network 52 | https://eth-$1.alchemyapi.io/v2/${ALCHEMY_API_KEY} 53 | endef 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Note: As [Dapptools has passed the torch to Foundry](https://github.com/dapphub/dapptools/pull/927/files) we recommend you check out the [foundry-starter-kit](https://github.com/smartcontractkit/foundry-starter-kit) instead. 2 | 3 | # DappTools Starter Kit 4 | 5 |
6 |

7 | 8 | Chainlink DappTools logo 9 | 10 |

11 |
12 | 13 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/smartcontractkit/dapptools-starter-kit) 14 | 15 | - [DappTools Starter Kit](#dapptools-starter-kit) 16 | - [Installation](#installation) 17 | - [Requirements](#requirements) 18 | - [Getting Started](#getting-started) 19 | - [Testing](#testing) 20 | - [Deploying](#deploying) 21 | - [Setup your Account/ethsign](#setup-your-accountethsign) 22 | - [Setup your .env file](#setup-your-env-file) 23 | - [Testnet & Mainnet Deployment](#testnet--mainnet-deployment) 24 | - [Local Testnet](#local-testnet) 25 | - [Verifying on Etherscan](#verifying-on-etherscan) 26 | - [Interacting with your contracts](#interacting-with-your-contracts) 27 | - [Resources](#resources) 28 | - [TODO](#todo) 29 | 30 | **This is based on [dapptools-template](https://github.com/gakonst/dapptools-template)** 31 | 32 | You can view a [foundry-starter-kit here](https://github.com/smartcontractkit/foundry-starter-kit). 33 | 34 | See the [#TODO](#todo) list at the bottom for a list of things to complete. 35 | 36 | Implementation of the following 4 Chainlink features using the [DappTools](https://dapp.tools/) development environment: 37 | 38 | - [Chainlink Price Feeds](https://docs.chain.link/docs/using-chainlink-reference-contracts) 39 | - [Chainlink VRF](https://docs.chain.link/docs/chainlink-vrf/) 40 | - [Chainlink Keepers](https://docs.chain.link/docs/chainlink-keepers/introduction/) 41 | - [Chainlink API](https://docs.chain.link/docs/request-and-receive-data/) 42 | 43 | # Installation 44 | 45 | ## Requirements 46 | 47 | - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 48 | - [DappTools](https://github.com/dapphub/dapptools#installation) 49 | 50 | And you probably already have `make` installed... but if not [try looking here.](https://askubuntu.com/questions/161104/how-do-i-install-make) 51 | 52 | ## Getting Started 53 | 54 | ```sh 55 | git clone https://github.com/smartcontractkit/dapptools-starter-kit 56 | cd dapptools-starter-kit 57 | make # This installs the project's dependencies. 58 | make test 59 | ``` 60 | 61 | ## Testing 62 | 63 | ``` 64 | make test 65 | ``` 66 | 67 | or 68 | 69 | ``` 70 | dapp test 71 | ``` 72 | 73 | All the commands from [dapptools](https://dapp.tools/) work with this repo, like `dapp build`, `ethsign`, and `dapp test`. 74 | 75 | # Deploying 76 | 77 | To deploy, you first need to setup your `ethsign` and your `.env` file. 78 | 79 | ## Setup your Account/ethsign 80 | 81 | To get your private keys into dapptools, you can either use a keystore or `ethsign`. `ethsign` comes with the install of `dapptools`. For `ethsign`, run the following: 82 | 83 | ```bash 84 | ethsign import 85 | ``` 86 | 87 | And you'll be prompted for your private key, and a password. You can get a private key from a wallet like [Metamask](https://metamask.io/). Once successful, add the address of the private key to your `.env` file under an `ETH_FROM` variable. See the `.env.example` file for an example. 88 | 89 | See the [`Makefile`](./Makefile#25) for more context on how this works under the hood. 90 | 91 | If you're going to deploy to a testnet, make sure you have testnet [ETH and LINK in your wallet](https://faucets.chain.link/)! 92 | 93 | ## Setup your .env file 94 | 95 | You can see in the `.env.example` an example of what your `.env` should look like (to deploy to a live network). 96 | 97 | 1. `ALCHEMY_API_KEY`: You can find this from getting an [Alchemy](https://www.alchemy.com/) account. 98 | 2. `ETH_FROM`: The address of your wallet you want to send transactions from. You must have the private key of the address you want to use loaded into your `ethsign`, see above for this. 99 | 3. (Optional)`ETHERSCAN_API_KEY`: For verifying contracts on etherscan. 100 | 4. (Optional)`ETH_RPC_URL`: For having a default deployment network when using `make deploy`. This 101 | 102 | ## Testnet & Mainnet Deployment 103 | 104 | Set your `ETH_RPC_URL` or `ALCHEMY_API_KEY` in your `.env` file, then run one of the following: 105 | 106 | Counters (Keeper Compatible Contract): 107 | 108 | ```bash 109 | make deploy CONTRACT=KeepersCounter 110 | ``` 111 | 112 | Price Feed: 113 | 114 | ```bash 115 | make deploy CONTRACT=PriceFeedConsumer 116 | ``` 117 | 118 | Chainlink VRF Consumer: 119 | 120 | ```bash 121 | make deploy CONTRACT=VRFConsumerV2 122 | ``` 123 | 124 | Chainlink API contract: 125 | 126 | ```bash 127 | make deploy CONTRACT=APIConsumer 128 | ``` 129 | 130 | You can change their deployment parameters in their respective `deploy` file in the `scripts` folder. All the constructor arguments are created in the `./scripts/helper-config.sh` folder. This is where you can assign different constructor arguments across networks. 131 | 132 | ### Local Testnet 133 | 134 | ``` 135 | # on one terminal 136 | dapp testnet 137 | ``` 138 | 139 | Change your `ETH_RPC_URL` to `http://127.0.0.1:8545` 140 | 141 | Then run your deploy script. 142 | 143 | ### Verifying on Etherscan 144 | 145 | After deploying your contract you can verify it on Etherscan using: 146 | 147 | ``` 148 | ETHERSCAN_API_KEY= dapp verify-contract /: 149 | ``` 150 | 151 | For example: 152 | 153 | ``` 154 | ETHERSCAN_API_KEY=123456765 dapp verify-contract ./src/KeepersCounter.sol:KeepersCounter 0x23456534212536435424 155 | ``` 156 | 157 | Check out the [dapp documentation](https://github.com/dapphub/dapptools/tree/master/src/dapp#dapp-verify-contract) to see how 158 | verifying contracts work with DappTools. 159 | 160 | # Interacting with your contracts 161 | 162 | To interact with our contracts, we use the `seth` command. Let's say we've deployed our `PriceFeedConsumer.sol` to kovan, and now we want to call the `getLatestPrice` function. How do we do this? 163 | 164 | ``` 165 | ETH_RPC_URL= seth call "getLatestPrice()" 166 | ``` 167 | 168 | For example: 169 | 170 | ``` 171 | ETH_RPC_URL=https://alchemy.io/adsfasdf seth call 0xd39F749195Ab1B4772fBB496EDAF56729ee36E55 "getLatestPrice()" 172 | ``` 173 | 174 | This will give us an output like `0x0000000000000000000000000000000000000000000000000000004c17b125c0` which is the hex of `326815000000` 175 | 176 | This is to call transactions (not spend gas). To change the state of the blockchain (spend gas) we'd use `seth send`. Let's say we have a `VRFConsumer` contract deployed, and we want to call `getRandomNumber`: 177 | 178 | First, we'd need to send our contract some LINK on the Kovan Chain: 179 | 180 | ``` 181 | ETH_RPC_URL= ETH_FROM= seth send "transfer(address,uint256)" 1000000000000000000 182 | ``` 183 | 184 | Like: 185 | 186 | ``` 187 | ETH_RPC_URL=https://alchemy.io/adfasdf ETH_FROM=0x12345 seth send 0xa36085F69e2889c224210F603D836748e7dC0088 "transfer(address,uint256)" 0xa74576956E24a8Fa768723Bd5284BcBE1Ea03adA 100000000000000000 188 | ``` 189 | 190 | Where `100000000000000000` = 1 LINK 191 | 192 | Then, we could call the `getRandomNumber` function: 193 | 194 | ``` 195 | ETH_RPC_URL= ETH_FROM= seth send "getRandomNumber()" 196 | ``` 197 | 198 | And after a slight delay, read the result: 199 | 200 | ``` 201 | ETH_RPC_URL= seth call "randomResult()" 202 | ``` 203 | 204 | As you can see... it would be great to have these scripted in our `scripts` folder. If you think you want to contribute, please make a PR! 205 | 206 | # Resources 207 | 208 | - [DappTools](https://dapp.tools) 209 | - [Hevm Docs](https://github.com/dapphub/dapptools/blob/master/src/hevm/README.md) 210 | - [Dapp Docs](https://github.com/dapphub/dapptools/tree/master/src/dapp/README.md) 211 | - [Seth Docs](https://github.com/dapphub/dapptools/tree/master/src/seth/README.md) 212 | - [DappTools Overview](https://www.youtube.com/watch?v=lPinWgaNceM) 213 | - [Chainlink](https://docs.chain.link) 214 | - [Awesome-DappTools](https://github.com/rajivpo/awesome-dapptools) 215 | 216 | # TODO 217 | 218 | [x] Enable network & contract choice from the command line 219 | ie: make deploy-rinkeby contract=KeepersCounter 220 | 221 | [x] Add mockOracle for any API calls 222 | 223 | [ ] Add scripts that interact with deployed contracts 224 | 225 | [x] Fix Chainlink VRF deploy script 226 | 227 | [x] Add config for parametatizing variables across networks and contracts 228 | -------------------------------------------------------------------------------- /img/chainlink-dapptools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/dapptools-starter-kit/59e4efe5d9daceb0b7cf282038bc15a17131699c/img/chainlink-dapptools.png -------------------------------------------------------------------------------- /img/chainlink-foundry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/dapptools-starter-kit/59e4efe5d9daceb0b7cf282038bc15a17131699c/img/chainlink-foundry.png -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/=lib/openzeppelin-contracts/ 2 | ds-test/=lib/ds-test/src/ 3 | @chainlink/=lib/chainlink-brownie-contracts/ 4 | -------------------------------------------------------------------------------- /scripts/common.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | if [[ ${DEBUG} ]]; then 6 | set -x 7 | fi 8 | 9 | # All contracts are output to `out/addresses.json` by default 10 | OUT_DIR=${OUT_DIR:-$PWD/out} 11 | ADDRESSES_FILE=${ADDRESSES_FILE:-$OUT_DIR/"addresses.json"} 12 | # default to localhost rpc 13 | ETH_RPC_URL=${ETH_RPC_URL:-http://localhost:8545} 14 | 15 | # green log helper 16 | GREEN='\033[0;32m' 17 | NC='\033[0m' # No Color 18 | log() { 19 | printf '%b\n' "${GREEN}${*}${NC}" 20 | echo "" 21 | } 22 | 23 | # Coloured output helpers 24 | if command -v tput >/dev/null 2>&1; then 25 | if [ $(($(tput colors 2>/dev/null))) -ge 8 ]; then 26 | # Enable colors 27 | TPUT_RESET="$(tput sgr 0)" 28 | TPUT_YELLOW="$(tput setaf 3)" 29 | TPUT_RED="$(tput setaf 1)" 30 | TPUT_BLUE="$(tput setaf 4)" 31 | TPUT_GREEN="$(tput setaf 2)" 32 | TPUT_WHITE="$(tput setaf 7)" 33 | TPUT_BOLD="$(tput bold)" 34 | fi 35 | fi 36 | 37 | # ensure ETH_FROM is set and give a meaningful error message 38 | if [[ -z ${ETH_FROM} ]]; then 39 | echo "ETH_FROM not found, please set it and re-run the last command." >&2 40 | exit 1 41 | fi 42 | 43 | # Make sure address is checksummed 44 | if [ "$ETH_FROM" != "$(seth --to-checksum-address "$ETH_FROM")" ]; then 45 | echo "ETH_FROM not checksummed, please format it with 'seth --to-checksum-address
'" >&2 46 | exit 1 47 | fi 48 | 49 | # Setup addresses file 50 | cat >"$ADDRESSES_FILE" < deploy ContractName arg1 arg2 arg3` 57 | # (or omit the env vars if you have already set them) 58 | deploy() { 59 | NAME=$1 60 | ARGS=${@:2} 61 | 62 | if [[ -z "${NAME}" ]]; then 63 | echo "ERROR. File name not provided" >&2 64 | exit 1 65 | fi 66 | 67 | # find file path 68 | CONTRACT_PATH=$(find ./src -name $NAME.sol) 69 | CONTRACT_PATH=${CONTRACT_PATH:2} 70 | if [[ -z "${CONTRACT_PATH}" ]]; then 71 | echo "ERROR. File \"${NAME}\" does not exist" >&2 72 | exit 1 73 | fi 74 | 75 | # select the filename and the contract in it 76 | PATTERN=".contracts[\"$CONTRACT_PATH\"].$NAME" 77 | 78 | # Compile / Build 79 | dapp build 80 | if [[ $? -ne 0 ]]; then 81 | echo "ERROR. dapp build failed" >&2 82 | exit 1 83 | fi 84 | 85 | # get the constructor's signature 86 | ABI=$(jq -r "$PATTERN.abi // empty" out/dapp.sol.json) 87 | if [[ $? -ne 0 ]] || [[ -z "${ABI}" ]]; then 88 | echo "ERROR. Unable to get the contract ABI" >&2 89 | exit 1 90 | fi 91 | 92 | SIG=$(echo "$ABI" | seth --abi-constructor) 93 | if [[ $? -ne 0 ]] || [[ -z "${SIG}" ]]; then 94 | echo "ERROR. Unable to get the constructor signature" >&2 95 | exit 1 96 | fi 97 | 98 | # get the bytecode from the compiled file 99 | BYTECODE=0x$(jq -r "$PATTERN.evm.bytecode.object" out/dapp.sol.json) 100 | if [[ $? -ne 0 ]]; then 101 | echo "ERROR. Unable to get the bytecode from the compiled file" >&2 102 | exit 1 103 | fi 104 | 105 | # estimate gas 106 | GAS=$(seth estimate --create "$BYTECODE" "$SIG" $ARGS --rpc-url "$ETH_RPC_URL") 107 | if [[ $? -ne 0 ]]; then 108 | echo "ERROR. Unable to estimate gas" >&2 109 | exit 1 110 | fi 111 | 112 | # deploy 113 | ADDRESS=$(dapp create "$NAME" $ARGS -- --gas "$GAS" --rpc-url "$ETH_RPC_URL") 114 | if [[ $? -ne 0 ]]; then 115 | echo "ERROR. Unable to deploy app" >&2 116 | exit 1 117 | fi 118 | 119 | # save the addrs to the json 120 | # TODO: It'd be nice if we could evolve this into a minimal versioning system 121 | # e.g. via commit / chainid etc. 122 | saveContract "$NAME" "$ADDRESS" 123 | 124 | echo "$ADDRESS" 125 | } 126 | 127 | # Call as `saveContract ContractName 0xYourAddress` to store the contract name 128 | # & address to the addresses json file 129 | saveContract() { 130 | # create an empty json if it does not exist 131 | if [[ ! -e $ADDRESSES_FILE ]]; then 132 | echo "{}" >"$ADDRESSES_FILE" 133 | fi 134 | result=$(cat "$ADDRESSES_FILE" | jq -r ". + {\"$1\": \"$2\"}") 135 | printf %s "$result" >"$ADDRESSES_FILE" 136 | } 137 | 138 | estimate_gas() { 139 | NAME=$1 140 | ARGS=${@:2} 141 | 142 | if [[ -z "${NAME}" ]]; then 143 | echo "ERROR. File name not provided" >&2 144 | exit 1 145 | fi 146 | 147 | # select the filename and the contract in it 148 | PATTERN=".contracts[\"src/$NAME.sol\"].$NAME" 149 | 150 | # get the constructor's signature 151 | ABI=$(jq -r "$PATTERN.abi // empty" out/dapp.sol.json) 152 | if [[ $? -ne 0 ]] || [[ -z "${ABI}" ]]; then 153 | echo "ERROR. Unable to get the contract ABI" >&2 154 | exit 1 155 | fi 156 | 157 | SIG=$(echo "$ABI" | seth --abi-constructor) 158 | if [[ $? -ne 0 ]] || [[ -z "${SIG}" ]]; then 159 | echo "ERROR. Unable to get the constructor signature" >&2 160 | exit 1 161 | fi 162 | 163 | # get the bytecode from the compiled file 164 | BYTECODE=0x$(jq -r "$PATTERN.evm.bytecode.object" out/dapp.sol.json) 165 | if [[ $? -ne 0 ]]; then 166 | echo "ERROR. Unable to get the bytecode from the compiled file" >&2 167 | exit 1 168 | fi 169 | # estimate gas 170 | GAS=$(seth estimate --create "$BYTECODE" "$SIG" $ARGS --rpc-url "$ETH_RPC_URL") 171 | if [[ $? -ne 0 ]]; then 172 | echo "ERROR. Unable to estimate gas" >&2 173 | exit 1 174 | fi 175 | 176 | TXPRICE_RESPONSE=$(curl -sL https://api.txprice.com/v1) 177 | response=$(jq '.code' <<<"$TXPRICE_RESPONSE") 178 | if [[ $response != "200" ]]; then 179 | echo "Could not get gas information from ${TPUT_BOLD}txprice.com${TPUT_RESET}: https://api.txprice.com/v1" 180 | echo "response code: $response" 181 | else 182 | rapid=$(($(jq '.blockPrices[0].estimatedPrices[0].maxFeePerGas' <<<"$TXPRICE_RESPONSE"))) 183 | fast=$(($(jq '.blockPrices[0].estimatedPrices[1].maxFeePerGas' <<<"$TXPRICE_RESPONSE"))) 184 | standard=$(($(jq '.blockPrices[0].estimatedPrices[2].maxFeePerGas' <<<"$TXPRICE_RESPONSE"))) 185 | slow=$(($(jq '.blockPrices[0].estimatedPrices[3].maxFeePerGas' <<<"$TXPRICE_RESPONSE"))) 186 | basefee$(($(jq '.blockPrices[0].baseFeePerGas' <<<"$TXPRICE_RESPONSE"))) 187 | echo "Gas prices from ${TPUT_BOLD}txprice.com${TPUT_RESET}: https://api.txprice.com/v1" 188 | echo " \ 189 | ${TPUT_RED}Rapid: $rapid gwei ${TPUT_RESET} \n 190 | ${TPUT_YELLOW}Fast: $fast gwei \n 191 | ${TPUT_BLUE}Standard: $standard gwei \n 192 | ${TPUT_GREEN}Slow: $slow gwei${TPUT_RESET}" | column -t 193 | size=$(contract_size "$NAME") 194 | echo "Estimated Gas cost for deployment of $NAME: ${TPUT_BOLD}$GAS${TPUT_RESET} units of gas" 195 | echo "Contract Size: ${size} bytes" 196 | echo "Total cost for deployment:" 197 | rapid_cost=$(echo "scale=5; $GAS*$rapid" | bc) 198 | fast_cost=$(echo "scale=5; $GAS*$fast" | bc) 199 | standard_cost=$(echo "scale=5; $GAS*$standard" | bc) 200 | slow_cost=$(echo "scale=5; $GAS*$slow" | bc) 201 | echo " \ 202 | ${TPUT_RED}Rapid: $rapid_cost ETH ${TPUT_RESET} \n 203 | ${TPUT_YELLOW}Fast: $fast_cost ETH \n 204 | ${TPUT_BLUE}Standard: $standard_cost ETH \n 205 | ${TPUT_GREEN}Slow: $slow_cost ETH ${TPUT_RESET}" | column -t 206 | fi 207 | } 208 | 209 | contract_size() { 210 | NAME=$1 211 | ARGS=${@:2} 212 | # select the filename and the contract in it 213 | PATTERN=".contracts[\"src/$NAME.sol\"].$NAME" 214 | 215 | # get the bytecode from the compiled file 216 | BYTECODE=0x$(jq -r "$PATTERN.evm.bytecode.object" out/dapp.sol.json) 217 | length=$(echo "$BYTECODE" | wc -m) 218 | echo $(($length / 2)) 219 | } 220 | -------------------------------------------------------------------------------- /scripts/contract-size.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | . $(dirname $0)/common.sh 6 | 7 | if [[ -z $contract ]]; then 8 | if [[ -z ${1} ]];then 9 | echo '"$contract" env variable is not set. Set it to the name of the contract you want to estimate size for.' 10 | exit 1 11 | else 12 | contract=${1} 13 | fi 14 | fi 15 | contract_size=$(contract_size ${contract}) 16 | echo "Contract Name: ${contract}" 17 | echo "Contract Size: ${contract_size} bytes" 18 | echo "$(( 24576 - ${contract_size} )) bytes left to reach the smart contract size limit of 24576 bytes." 19 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | # import the deployment helpers 6 | . $(dirname $0)/common.sh 7 | 8 | # import config with arguments based on contract and network 9 | . $(dirname $0)/helper-config.sh 10 | 11 | # Deploy 12 | # Contract will be counter unless overriden on the command line 13 | : ${CONTRACT:=KeepersCounter} 14 | echo "Deploying $CONTRACT to $NETWORK with arguments: $arguments" 15 | Addr=$(deploy $CONTRACT $arguments) 16 | log "$CONTRACT deployed at:" $Addr 17 | -------------------------------------------------------------------------------- /scripts/estimate-gas.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | . $(dirname $0)/common.sh 6 | 7 | if [[ -z $contract ]]; then 8 | if [[ -z ${1} ]];then 9 | echo '"$contract" env variable is not set. Set it to the name of the contract you want to estimate gas cost for.' 10 | exit 1 11 | else 12 | contract=${1} 13 | fi 14 | fi 15 | 16 | estimate_gas $contract 17 | 18 | 19 | -------------------------------------------------------------------------------- /scripts/helper-config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Defaults to Rinkeby 4 | interval=1 5 | vrf_coordinator=0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B 6 | link_token=0x01be23585060835e02b77ef475b0cc51aa1e0709 7 | keyhash=0xd89b2bf150e3b9e13446986e571fb9cab24b13cea0a43ea20a6049a85cc807cc # gasLane 8 | fee=100000000000000000 9 | price_feed=0x8A753747A1Fa494EC906cE90E9f37563A8AF630e 10 | oracle=0xc57b33452b4f7bb189bb5afae9cc4aba1f7a4fd8 11 | jobId=6b88e0402e5d415eb946e528b8e0c7ba 12 | # Add your subId here! 13 | subId=1 14 | 15 | # Defaults to Counter arguments 16 | arguments=1 17 | 18 | if [ "$NETWORK" = "kovan" ] 19 | then 20 | interval=1 21 | price_feed=0x9326BFA02ADD2366b30bacB125260Af641031331 22 | link_token=0xa36085F69e2889c224210F603D836748e7dC0088 23 | fee=100000000000000000 24 | oracle=0xc57b33452b4f7bb189bb5afae9cc4aba1f7a4fd8 25 | jobId=d5270d1c311941d0b08bead21fea7747 26 | # Add your subId here! 27 | subId=1 28 | elif [ "$NETWORK" = "rinkeby" ] 29 | then 30 | interval=1 31 | vrf_coordinator=0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B 32 | link_token=0x01be23585060835e02b77ef475b0cc51aa1e0709 33 | keyhash=0xd89b2bf150e3b9e13446986e571fb9cab24b13cea0a43ea20a6049a85cc807cc # gasLane 34 | fee=100000000000000000 35 | price_feed=0x8A753747A1Fa494EC906cE90E9f37563A8AF630e 36 | oracle=0xc57b33452b4f7bb189bb5afae9cc4aba1f7a4fd8 37 | jobId=6b88e0402e5d415eb946e528b8e0c7ba 38 | # Add your subId here! 39 | subId=1 40 | fi 41 | 42 | if [ "$CONTRACT" = "KeepersCounter" ] 43 | then 44 | arguments=$interval 45 | elif [ "$CONTRACT" = "PriceFeedConsumer" ] 46 | then 47 | arguments=$price_feed 48 | elif [ "$CONTRACT" = "VRFConsumerV2" ] 49 | then 50 | arguments="$subId $vrf_coordinator $link_token $keyhash" 51 | elif [ "$CONTRACT" = "APIConsumer" ] 52 | then 53 | arguments="$oracle $jobId $fee $link_token" 54 | fi 55 | -------------------------------------------------------------------------------- /scripts/run-temp-testnet.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | # Utility for running a temporary dapp testnet w/ an ephemeral account 6 | # to be used for deployment tests 7 | 8 | # make a temp dir to store testnet info 9 | export TMPDIR=$(mktemp -d) 10 | 11 | # clean up 12 | trap 'killall geth && sleep 3 && rm -rf "$TMPDIR"' EXIT 13 | trap "exit 1" SIGINT SIGTERM 14 | 15 | # test helper 16 | error() { 17 | printf 1>&2 "fail: function '%s' at line %d.\n" "${FUNCNAME[1]}" "${BASH_LINENO[0]}" 18 | printf 1>&2 "got: %s" "$output" 19 | exit 1 20 | } 21 | 22 | # launch the testnet 23 | dapp testnet --dir "$TMPDIR" & 24 | # wait for it to launch (can't go <3s) 25 | sleep 3 26 | 27 | # set the RPC URL to the local testnet 28 | export ETH_RPC_URL=http://127.0.0.1:8545 29 | 30 | # get the created account (it's unlocked so we only need to set the address) 31 | export ETH_FROM=$(seth ls --keystore $TMPDIR/8545/keystore | cut -f1) 32 | -------------------------------------------------------------------------------- /scripts/test-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | # bring up the network 6 | . $(dirname $0)/run-temp-testnet.sh 7 | 8 | # run the deploy script 9 | . $(dirname $0)/deploy.sh 10 | 11 | # get the address 12 | addr=$(jq -r '.Greeter' out/addresses.json) 13 | 14 | # the initial greeting must be empty 15 | greeting=$(seth call $addr 'greeting()(string)') 16 | [[ $greeting = "" ]] || error 17 | 18 | # set it to a value 19 | seth send $addr \ 20 | 'greet(string memory)' '"yo"' \ 21 | --keystore $TMPDIR/8545/keystore \ 22 | --password /dev/null 23 | 24 | sleep 1 25 | 26 | # should be set afterwards 27 | greeting=$(seth call $addr 'greeting()(string)') 28 | [[ $greeting = "yo" ]] || error 29 | 30 | echo "Success." 31 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import (builtins.fetchGit rec { 3 | name = "dapptools-${rev}"; 4 | url = https://github.com/dapphub/dapptools; 5 | rev = "adcc076b1441b3a928a8f0b42b2a63f05d9bcf0d"; 6 | }) {}; 7 | 8 | in 9 | pkgs.mkShell { 10 | src = null; 11 | name = "dapptools-template"; 12 | buildInputs = with pkgs; [ 13 | pkgs.dapp 14 | pkgs.seth 15 | pkgs.go-ethereum-unlimited 16 | pkgs.hevm 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /src/APIConsumer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol"; 5 | 6 | /** 7 | * @title The APIConsumer contract 8 | * @notice An API Consumer contract that makes GET requests to obtain 24h trading volume of ETH in USD 9 | */ 10 | contract APIConsumer is ChainlinkClient { 11 | using Chainlink for Chainlink.Request; 12 | 13 | uint256 public volume; 14 | address private immutable oracle; 15 | bytes32 private immutable jobId; 16 | uint256 private immutable fee; 17 | 18 | event DataFullfilled(uint256 volume); 19 | 20 | /** 21 | * @notice Executes once when a contract is created to initialize state variables 22 | * 23 | * @param _oracle - address of the specific Chainlink node that a contract makes an API call from 24 | * @param _jobId - specific job for :_oracle: to run; each job is unique and returns different types of data 25 | * @param _fee - node operator price per API call / data request 26 | * @param _link - LINK token address on the corresponding network 27 | * 28 | * Network: Rinkeby 29 | * Oracle: 0xc57b33452b4f7bb189bb5afae9cc4aba1f7a4fd8 30 | * Job ID: 6b88e0402e5d415eb946e528b8e0c7ba 31 | * Fee: 0.1 LINK 32 | */ 33 | constructor( 34 | address _oracle, 35 | bytes32 _jobId, 36 | uint256 _fee, 37 | address _link 38 | ) { 39 | if (_link == address(0)) { 40 | setPublicChainlinkToken(); 41 | } else { 42 | setChainlinkToken(_link); 43 | } 44 | oracle = _oracle; 45 | jobId = _jobId; 46 | fee = _fee; 47 | } 48 | 49 | /** 50 | * @notice Creates a Chainlink request to retrieve API response, find the target 51 | * data, then multiply by 1000000000000000000 (to remove decimal places from data). 52 | * 53 | * @return requestId - id of the request 54 | */ 55 | function requestVolumeData() public returns (bytes32 requestId) { 56 | Chainlink.Request memory request = buildChainlinkRequest( 57 | jobId, 58 | address(this), 59 | this.fulfill.selector 60 | ); 61 | 62 | // Set the URL to perform the GET request on 63 | request.add("get", "https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD"); 64 | 65 | // Set the path to find the desired data in the API response, where the response format is: 66 | // {"RAW": 67 | // {"ETH": 68 | // {"USD": 69 | // { 70 | // "VOLUME24HOUR": xxx.xxx, 71 | // } 72 | // } 73 | // } 74 | // } 75 | // request.add("path", "RAW.ETH.USD.VOLUME24HOUR"); // Chainlink nodes prior to 1.0.0 support this format 76 | request.add("path", "RAW,ETH,USD,VOLUME24HOUR"); // Chainlink nodes 1.0.0 and later support this format 77 | 78 | // Multiply the result by 1000000000000000000 to remove decimals 79 | int256 timesAmount = 10**18; 80 | request.addInt("times", timesAmount); 81 | 82 | // Sends the request 83 | return sendChainlinkRequestTo(oracle, request, fee); 84 | } 85 | 86 | /** 87 | * @notice Receives the response in the form of uint256 88 | * 89 | * @param _requestId - id of the request 90 | * @param _volume - response; requested 24h trading volume of ETH in USD 91 | */ 92 | function fulfill(bytes32 _requestId, uint256 _volume) 93 | public 94 | recordChainlinkFulfillment(_requestId) 95 | { 96 | volume = _volume; 97 | emit DataFullfilled(volume); 98 | } 99 | 100 | /** 101 | * @notice Witdraws LINK from the contract 102 | * @dev Implement a withdraw function to avoid locking your LINK in the contract 103 | */ 104 | function withdrawLink() external {} 105 | } 106 | -------------------------------------------------------------------------------- /src/KeepersCounter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "@chainlink/contracts/src/v0.8/interfaces/KeeperCompatibleInterface.sol"; 5 | 6 | /** 7 | * @title The Counter contract 8 | * @notice A keeper-compatible contract that increments counter variable at fixed time intervals 9 | */ 10 | contract KeepersCounter is KeeperCompatibleInterface { 11 | /** 12 | * Public counter variable 13 | */ 14 | uint256 public counter; 15 | 16 | /** 17 | * Use an interval in seconds and a timestamp to slow execution of Upkeep 18 | */ 19 | uint256 public immutable interval; 20 | uint256 public lastTimeStamp; 21 | 22 | /** 23 | * @notice Executes once when a contract is created to initialize state variables 24 | * 25 | * @param updateInterval - Period of time between two counter increments expressed as UNIX timestamp value 26 | */ 27 | constructor(uint256 updateInterval) { 28 | interval = updateInterval; 29 | lastTimeStamp = block.timestamp; 30 | 31 | counter = 0; 32 | } 33 | 34 | /** 35 | * @notice Checks if the contract requires work to be done 36 | */ 37 | function checkUpkeep( 38 | bytes memory /* checkData */ 39 | ) 40 | public 41 | override 42 | returns ( 43 | bool upkeepNeeded, 44 | bytes memory /* performData */ 45 | ) 46 | { 47 | upkeepNeeded = (block.timestamp - lastTimeStamp) > interval; 48 | // We don't use the checkData in this example. The checkData is defined when the Upkeep was registered. 49 | } 50 | 51 | /** 52 | * @notice Performs the work on the contract, if instructed by :checkUpkeep(): 53 | */ 54 | function performUpkeep( 55 | bytes calldata /* performData */ 56 | ) external override { 57 | // add some verification 58 | (bool upkeepNeeded, ) = checkUpkeep(""); 59 | require(upkeepNeeded, "Time interval not met"); 60 | 61 | lastTimeStamp = block.timestamp; 62 | counter = counter + 1; 63 | // We don't use the performData in this example. The performData is generated by the Keeper's call to your checkUpkeep function 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/PriceFeedConsumer.sol: -------------------------------------------------------------------------------- 1 | 2 | // SPDX-License-Identifier: MIT 3 | pragma solidity ^0.8.0; 4 | 5 | import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; 6 | 7 | contract PriceFeedConsumer { 8 | 9 | AggregatorV3Interface internal priceFeed; 10 | 11 | /** 12 | * Network: Kovan 13 | * Aggregator: ETH/USD 14 | * Address: 0x9326BFA02ADD2366b30bacB125260Af641031331 15 | */ 16 | /** 17 | * Network: Mainnet 18 | * Aggregator: ETH/USD 19 | * Address: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 20 | */ 21 | constructor(address AggregatorAddress) { 22 | priceFeed = AggregatorV3Interface(AggregatorAddress); 23 | } 24 | 25 | /** 26 | * Returns the latest price 27 | */ 28 | function getLatestPrice() public view returns (int) { 29 | ( 30 | uint80 roundID, 31 | int price, 32 | uint startedAt, 33 | uint timeStamp, 34 | uint80 answeredInRound 35 | ) = priceFeed.latestRoundData(); 36 | return price; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/VRFConsumerV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // An example of a consumer contract that relies on a subscription for funding. 3 | pragma solidity ^0.8.7; 4 | 5 | import "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol"; 6 | import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; 7 | import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; 8 | 9 | /** 10 | * @title The VRFConsumerV2 contract 11 | * @notice A contract that gets random values from Chainlink VRF V2 12 | */ 13 | contract VRFConsumerV2 is VRFConsumerBaseV2 { 14 | VRFCoordinatorV2Interface immutable COORDINATOR; 15 | LinkTokenInterface immutable LINKTOKEN; 16 | 17 | // Your subscription ID. 18 | uint64 immutable s_subscriptionId; 19 | 20 | // The gas lane to use, which specifies the maximum gas price to bump to. 21 | // For a list of available gas lanes on each network, 22 | // see https://docs.chain.link/docs/vrf-contracts/#configurations 23 | bytes32 immutable s_keyHash; 24 | 25 | // Depends on the number of requested values that you want sent to the 26 | // fulfillRandomWords() function. Storing each word costs about 20,000 gas, 27 | // so 100,000 is a safe default for this example contract. Test and adjust 28 | // this limit based on the network that you select, the size of the request, 29 | // and the processing of the callback request in the fulfillRandomWords() 30 | // function. 31 | uint32 immutable s_callbackGasLimit = 100000; 32 | 33 | // The default is 3, but you can set this higher. 34 | uint16 immutable s_requestConfirmations = 3; 35 | 36 | // For this example, retrieve 2 random values in one request. 37 | // Cannot exceed VRFCoordinatorV2.MAX_NUM_WORDS. 38 | uint32 public immutable s_numWords = 2; 39 | 40 | uint256[] public s_randomWords; 41 | uint256 public s_requestId; 42 | address s_owner; 43 | 44 | event ReturnedRandomness(uint256[] randomWords); 45 | 46 | /** 47 | * @notice Constructor inherits VRFConsumerBaseV2 48 | * 49 | * @param subscriptionId - the subscription ID that this contract uses for funding requests 50 | * @param vrfCoordinator - coordinator, check https://docs.chain.link/docs/vrf-contracts/#configurations 51 | * @param keyHash - the gas lane to use, which specifies the maximum gas price to bump to 52 | */ 53 | constructor( 54 | uint64 subscriptionId, 55 | address vrfCoordinator, 56 | address link, 57 | bytes32 keyHash 58 | ) VRFConsumerBaseV2(vrfCoordinator) { 59 | COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator); 60 | LINKTOKEN = LinkTokenInterface(link); 61 | s_keyHash = keyHash; 62 | s_owner = msg.sender; 63 | s_subscriptionId = subscriptionId; 64 | } 65 | 66 | /** 67 | * @notice Requests randomness 68 | * Assumes the subscription is funded sufficiently; "Words" refers to unit of data in Computer Science 69 | */ 70 | function requestRandomWords() external onlyOwner { 71 | // Will revert if subscription is not set and funded. 72 | s_requestId = COORDINATOR.requestRandomWords( 73 | s_keyHash, 74 | s_subscriptionId, 75 | s_requestConfirmations, 76 | s_callbackGasLimit, 77 | s_numWords 78 | ); 79 | } 80 | 81 | /** 82 | * @notice Callback function used by VRF Coordinator 83 | * 84 | * @param requestId - id of the request 85 | * @param randomWords - array of random results from VRF Coordinator 86 | */ 87 | function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) 88 | internal 89 | override 90 | { 91 | s_randomWords = randomWords; 92 | emit ReturnedRandomness(randomWords); 93 | } 94 | 95 | modifier onlyOwner() { 96 | require(msg.sender == s_owner); 97 | _; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/APIConsumer.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../APIConsumer.sol"; 6 | import "./mocks/LinkToken.sol"; 7 | import "ds-test/test.sol"; 8 | import "./mocks/MockOracle.sol"; 9 | 10 | contract APIConsumerTest is DSTest { 11 | APIConsumer public apiConsumer; 12 | LinkToken public linkToken; 13 | MockOracle public mockOracle; 14 | 15 | bytes32 jobId; 16 | uint256 fee; 17 | bytes32 blank_bytes32; 18 | 19 | uint256 constant AMOUNT = 1 * 10**18; 20 | uint256 constant RESPONSE = 777; 21 | 22 | function setUp() public { 23 | linkToken = new LinkToken(); 24 | mockOracle = new MockOracle(address(linkToken)); 25 | apiConsumer = new APIConsumer( 26 | address(mockOracle), 27 | jobId, 28 | fee, 29 | address(linkToken) 30 | ); 31 | linkToken.transfer(address(apiConsumer), AMOUNT); 32 | } 33 | 34 | function test_can_make_request() public { 35 | bytes32 requestId = apiConsumer.requestVolumeData(); 36 | assertTrue(requestId != blank_bytes32); 37 | } 38 | 39 | function test_can_get_response() public { 40 | bytes32 requestId = apiConsumer.requestVolumeData(); 41 | mockOracle.fulfillOracleRequest(requestId, bytes32(RESPONSE)); 42 | assertTrue(apiConsumer.volume() == RESPONSE); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/KeepersCounter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../KeepersCounter.sol"; 6 | import "ds-test/test.sol"; 7 | import "./utils/Cheats.sol"; 8 | 9 | contract KeepersCounterTest is DSTest { 10 | KeepersCounter public counter; 11 | uint256 public staticTime; 12 | uint256 public INTERVAL; 13 | Cheats internal constant cheats = Cheats(HEVM_ADDRESS); 14 | 15 | function setUp() public { 16 | staticTime = block.timestamp; 17 | counter = new KeepersCounter(INTERVAL); 18 | cheats.warp(staticTime); 19 | } 20 | 21 | function test_checkup_returns_false_before_time() public { 22 | (bool upkeepNeeded, ) = counter.checkUpkeep("0x"); 23 | assertTrue(!upkeepNeeded); 24 | } 25 | 26 | function test_checkup_returns_true_after_time() public { 27 | cheats.warp(staticTime + INTERVAL + 1); // Needs to be more than the interval 28 | (bool upkeepNeeded, ) = counter.checkUpkeep("0x"); 29 | assertTrue(upkeepNeeded); 30 | } 31 | 32 | function test_performUpkeep_updates_time() public { 33 | // Arrange 34 | uint256 currentCounter = counter.counter(); 35 | cheats.warp(staticTime + INTERVAL + 1); // Needs to be more than the interval 36 | 37 | // Act 38 | counter.performUpkeep("0x"); 39 | 40 | // Assert 41 | assertTrue(counter.lastTimeStamp() == block.timestamp); 42 | assertTrue(currentCounter + 1 == counter.counter()); 43 | } 44 | // Try forge for using fuzzing 45 | // function test_fuzzing_example(bytes memory variant) public { 46 | // // We expect this to fail, no matter how different the input is! 47 | // cheats.expectRevert(bytes("Time interval not met")); 48 | // counter.performUpkeep(variant); 49 | // } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/PriceFeedConsumer.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "../PriceFeedConsumer.sol"; 5 | import "./mocks/MockV3Aggregator.sol"; 6 | import "ds-test/test.sol"; 7 | 8 | contract PriceFeedConsumerTest is DSTest { 9 | uint8 public constant DECIMALS = 18; 10 | int256 public constant INITIAL_ANSWER = 1 * 10**18; 11 | PriceFeedConsumer public priceFeedConsumer; 12 | MockV3Aggregator public mockV3Aggregator; 13 | 14 | function setUp() public { 15 | mockV3Aggregator = new MockV3Aggregator(DECIMALS, INITIAL_ANSWER); 16 | priceFeedConsumer = new PriceFeedConsumer(address(mockV3Aggregator)); 17 | } 18 | 19 | function test_consumer_returns_starting_value() public { 20 | int256 price = priceFeedConsumer.getLatestPrice(); 21 | assertTrue(price == INITIAL_ANSWER); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/VRFConsumerV2.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../VRFConsumerV2.sol"; 5 | import "./mocks/MockVRFCoordinatorV2.sol"; 6 | import "./mocks/LinkToken.sol"; 7 | import "./utils/Cheats.sol"; 8 | import "ds-test/test.sol"; 9 | 10 | contract VRFConsumerV2Test is DSTest { 11 | LinkToken public linkToken; 12 | MockVRFCoordinatorV2 public vrfCoordinator; 13 | VRFConsumerV2 public vrfConsumer; 14 | Cheats internal constant cheats = Cheats(HEVM_ADDRESS); 15 | 16 | uint96 constant FUND_AMOUNT = 1 * 10**18; 17 | 18 | // Initialized as blank, fine for testing 19 | uint64 subId; 20 | bytes32 keyHash; // gasLane 21 | 22 | event ReturnedRandomness(uint256[] randomWords); 23 | 24 | function setUp() public { 25 | linkToken = new LinkToken(); 26 | vrfCoordinator = new MockVRFCoordinatorV2(); 27 | subId = vrfCoordinator.createSubscription(); 28 | vrfCoordinator.fundSubscription(subId, FUND_AMOUNT); 29 | vrfConsumer = new VRFConsumerV2( 30 | subId, 31 | address(vrfCoordinator), 32 | address(linkToken), 33 | keyHash 34 | ); 35 | } 36 | 37 | function test_can_request_randomness() public { 38 | uint256 startingRequestId = vrfConsumer.s_requestId(); 39 | vrfConsumer.requestRandomWords(); 40 | assertTrue(vrfConsumer.s_requestId() != startingRequestId); 41 | } 42 | 43 | function test_can_get_random_response() public { 44 | vrfConsumer.requestRandomWords(); 45 | uint256 requestId = vrfConsumer.s_requestId(); 46 | 47 | uint256[] memory words = getWords(requestId); 48 | 49 | vrfCoordinator.fulfillRandomWords(requestId, address(vrfConsumer)); 50 | assertTrue(vrfConsumer.s_randomWords(0) == words[0]); 51 | assertTrue(vrfConsumer.s_randomWords(1) == words[1]); 52 | } 53 | 54 | 55 | function getWords(uint256 requestId) 56 | public 57 | view 58 | returns (uint256[] memory) 59 | { 60 | uint256[] memory words = new uint256[](vrfConsumer.s_numWords()); 61 | for (uint256 i = 0; i < vrfConsumer.s_numWords(); i++) { 62 | words[i] = uint256(keccak256(abi.encode(requestId, i))); 63 | } 64 | return words; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/mocks/LinkToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // @dev This contract has been adapted to fit with dappTools 4 | pragma solidity ^0.8.0; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | 8 | interface ERC677Receiver { 9 | function onTokenTransfer( 10 | address _sender, 11 | uint256 _value, 12 | bytes memory _data 13 | ) external; 14 | } 15 | 16 | contract LinkToken is ERC20 { 17 | uint256 initialSupply = 1000000000000000000000000; 18 | 19 | constructor() ERC20("LinkToken", "LINK") { 20 | _mint(msg.sender, initialSupply); 21 | } 22 | 23 | event Transfer( 24 | address indexed from, 25 | address indexed to, 26 | uint256 value, 27 | bytes data 28 | ); 29 | 30 | /** 31 | * @dev transfer token to a contract address with additional data if the recipient is a contact. 32 | * @param _to The address to transfer to. 33 | * @param _value The amount to be transferred. 34 | * @param _data The extra data to be passed to the receiving contract. 35 | */ 36 | function transferAndCall( 37 | address _to, 38 | uint256 _value, 39 | bytes memory _data 40 | ) public virtual returns (bool success) { 41 | super.transfer(_to, _value); 42 | // emit Transfer(msg.sender, _to, _value, _data); 43 | emit Transfer(msg.sender, _to, _value, _data); 44 | if (isContract(_to)) { 45 | contractFallback(_to, _value, _data); 46 | } 47 | return true; 48 | } 49 | 50 | // PRIVATE 51 | 52 | function contractFallback( 53 | address _to, 54 | uint256 _value, 55 | bytes memory _data 56 | ) private { 57 | ERC677Receiver receiver = ERC677Receiver(_to); 58 | receiver.onTokenTransfer(msg.sender, _value, _data); 59 | } 60 | 61 | function isContract(address _addr) private view returns (bool hasCode) { 62 | uint256 length; 63 | assembly { 64 | length := extcodesize(_addr) 65 | } 66 | return length > 0; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/mocks/MockOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@chainlink/contracts/src/v0.8/interfaces/ChainlinkRequestInterface.sol"; 5 | import "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol"; 6 | 7 | /** 8 | * @title The LinkTokenReceiver contract - used for the MockOracle below 9 | */ 10 | abstract contract LinkTokenReceiver { 11 | bytes4 private constant ORACLE_REQUEST_SELECTOR = 0x40429946; 12 | uint256 private constant SELECTOR_LENGTH = 4; 13 | uint256 private constant EXPECTED_REQUEST_WORDS = 2; 14 | uint256 private constant MINIMUM_REQUEST_LENGTH = 15 | SELECTOR_LENGTH + (32 * EXPECTED_REQUEST_WORDS); 16 | 17 | /** 18 | * @notice Called when LINK is sent to the contract via `transferAndCall` 19 | * @dev The data payload's first 2 words will be overwritten by the `_sender` and `_amount` 20 | * values to ensure correctness. Calls oracleRequest. 21 | * @param _sender Address of the sender 22 | * @param _amount Amount of LINK sent (specified in wei) 23 | * @param _data Payload of the transaction 24 | */ 25 | function onTokenTransfer( 26 | address _sender, 27 | uint256 _amount, 28 | bytes memory _data 29 | ) 30 | public 31 | onlyLINK 32 | validRequestLength(_data) 33 | permittedFunctionsForLINK(_data) 34 | { 35 | assembly { 36 | // solhint-disable-next-line avoid-low-level-calls 37 | mstore(add(_data, 36), _sender) // ensure correct sender is passed 38 | // solhint-disable-next-line avoid-low-level-calls 39 | mstore(add(_data, 68), _amount) // ensure correct amount is passed 40 | } 41 | // solhint-disable-next-line avoid-low-level-calls 42 | (bool success, ) = address(this).delegatecall(_data); // calls oracleRequest 43 | require(success, "Unable to create request"); 44 | } 45 | 46 | function getChainlinkToken() public view virtual returns (address); 47 | 48 | /** 49 | * @dev Reverts if not sent from the LINK token 50 | */ 51 | modifier onlyLINK() { 52 | require(msg.sender == getChainlinkToken(), "Must use LINK token"); 53 | _; 54 | } 55 | 56 | /** 57 | * @dev Reverts if the given data does not begin with the `oracleRequest` function selector 58 | * @param _data The data payload of the request 59 | */ 60 | modifier permittedFunctionsForLINK(bytes memory _data) { 61 | bytes4 funcSelector; 62 | assembly { 63 | // solhint-disable-next-line avoid-low-level-calls 64 | funcSelector := mload(add(_data, 32)) 65 | } 66 | require( 67 | funcSelector == ORACLE_REQUEST_SELECTOR, 68 | "Must use whitelisted functions" 69 | ); 70 | _; 71 | } 72 | 73 | /** 74 | * @dev Reverts if the given payload is less than needed to create a request 75 | * @param _data The request payload 76 | */ 77 | modifier validRequestLength(bytes memory _data) { 78 | require( 79 | _data.length >= MINIMUM_REQUEST_LENGTH, 80 | "Invalid request length" 81 | ); 82 | _; 83 | } 84 | } 85 | 86 | /** 87 | * @title The Chainlink Mock Oracle contract 88 | * @notice Chainlink smart contract developers can use this to test their contracts 89 | */ 90 | contract MockOracle is ChainlinkRequestInterface, LinkTokenReceiver { 91 | uint256 public constant EXPIRY_TIME = 5 minutes; 92 | uint256 private constant MINIMUM_CONSUMER_GAS_LIMIT = 400000; 93 | 94 | struct Request { 95 | address callbackAddr; 96 | bytes4 callbackFunctionId; 97 | } 98 | 99 | LinkTokenInterface internal LinkToken; 100 | mapping(bytes32 => Request) private commitments; 101 | 102 | event OracleRequest( 103 | bytes32 indexed specId, 104 | address requester, 105 | bytes32 requestId, 106 | uint256 payment, 107 | address callbackAddr, 108 | bytes4 callbackFunctionId, 109 | uint256 cancelExpiration, 110 | uint256 dataVersion, 111 | bytes data 112 | ); 113 | 114 | event CancelOracleRequest(bytes32 indexed requestId); 115 | 116 | /** 117 | * @notice Deploy with the address of the LINK token 118 | * @dev Sets the LinkToken address for the imported LinkTokenInterface 119 | * @param _link The address of the LINK token 120 | */ 121 | constructor(address _link) { 122 | LinkToken = LinkTokenInterface(_link); // external but already deployed and unalterable 123 | } 124 | 125 | /** 126 | * @notice Creates the Chainlink request 127 | * @dev Stores the hash of the params as the on-chain commitment for the request. 128 | * Emits OracleRequest event for the Chainlink node to detect. 129 | * @param _sender The sender of the request 130 | * @param _payment The amount of payment given (specified in wei) 131 | * @param _specId The Job Specification ID 132 | * @param _callbackAddress The callback address for the response 133 | * @param _callbackFunctionId The callback function ID for the response 134 | * @param _nonce The nonce sent by the requester 135 | * @param _dataVersion The specified data version 136 | * @param _data The CBOR payload of the request 137 | */ 138 | function oracleRequest( 139 | address _sender, 140 | uint256 _payment, 141 | bytes32 _specId, 142 | address _callbackAddress, 143 | bytes4 _callbackFunctionId, 144 | uint256 _nonce, 145 | uint256 _dataVersion, 146 | bytes calldata _data 147 | ) external override onlyLINK checkCallbackAddress(_callbackAddress) { 148 | bytes32 requestId = keccak256(abi.encodePacked(_sender, _nonce)); 149 | require( 150 | commitments[requestId].callbackAddr == address(0), 151 | "Must use a unique ID" 152 | ); 153 | // solhint-disable-next-line not-rely-on-time 154 | uint256 expiration = block.timestamp + EXPIRY_TIME; 155 | 156 | commitments[requestId] = Request(_callbackAddress, _callbackFunctionId); 157 | 158 | emit OracleRequest( 159 | _specId, 160 | _sender, 161 | requestId, 162 | _payment, 163 | _callbackAddress, 164 | _callbackFunctionId, 165 | expiration, 166 | _dataVersion, 167 | _data 168 | ); 169 | } 170 | 171 | /** 172 | * @notice Called by the Chainlink node to fulfill requests 173 | * @dev Given params must hash back to the commitment stored from `oracleRequest`. 174 | * Will call the callback address' callback function without bubbling up error 175 | * checking in a `require` so that the node can get paid. 176 | * @param _requestId The fulfillment request ID that must match the requester's 177 | * @param _data The data to return to the consuming contract 178 | * @return Status if the external call was successful 179 | */ 180 | function fulfillOracleRequest(bytes32 _requestId, bytes32 _data) 181 | external 182 | isValidRequest(_requestId) 183 | returns (bool) 184 | { 185 | Request memory req = commitments[_requestId]; 186 | delete commitments[_requestId]; 187 | require( 188 | gasleft() >= MINIMUM_CONSUMER_GAS_LIMIT, 189 | "Must provide consumer enough gas" 190 | ); 191 | // All updates to the oracle's fulfillment should come before calling the 192 | // callback(addr+functionId) as it is untrusted. 193 | // See: https://solidity.readthedocs.io/en/develop/security-considerations.html#use-the-checks-effects-interactions-pattern 194 | (bool success, ) = req.callbackAddr.call( 195 | abi.encodeWithSelector(req.callbackFunctionId, _requestId, _data) 196 | ); // solhint-disable-line avoid-low-level-calls 197 | return success; 198 | } 199 | 200 | /** 201 | * @notice Allows requesters to cancel requests sent to this oracle contract. Will transfer the LINK 202 | * sent for the request back to the requester's address. 203 | * @dev Given params must hash to a commitment stored on the contract in order for the request to be valid 204 | * Emits CancelOracleRequest event. 205 | * @param _requestId The request ID 206 | * @param _payment The amount of payment given (specified in wei) 207 | * @param _expiration The time of the expiration for the request 208 | */ 209 | function cancelOracleRequest( 210 | bytes32 _requestId, 211 | uint256 _payment, 212 | bytes4, 213 | uint256 _expiration 214 | ) external override { 215 | require( 216 | commitments[_requestId].callbackAddr != address(0), 217 | "Must use a unique ID" 218 | ); 219 | // solhint-disable-next-line not-rely-on-time 220 | require(_expiration <= block.timestamp, "Request is not expired"); 221 | 222 | delete commitments[_requestId]; 223 | emit CancelOracleRequest(_requestId); 224 | 225 | assert(LinkToken.transfer(msg.sender, _payment)); 226 | } 227 | 228 | /** 229 | * @notice Returns the address of the LINK token 230 | * @dev This is the public implementation for chainlinkTokenAddress, which is 231 | * an internal method of the ChainlinkClient contract 232 | */ 233 | function getChainlinkToken() public view override returns (address) { 234 | return address(LinkToken); 235 | } 236 | 237 | // MODIFIERS 238 | 239 | /** 240 | * @dev Reverts if request ID does not exist 241 | * @param _requestId The given request ID to check in stored `commitments` 242 | */ 243 | modifier isValidRequest(bytes32 _requestId) { 244 | require( 245 | commitments[_requestId].callbackAddr != address(0), 246 | "Must have a valid requestId" 247 | ); 248 | _; 249 | } 250 | 251 | /** 252 | * @dev Reverts if the callback address is the LINK token 253 | * @param _to The callback address 254 | */ 255 | modifier checkCallbackAddress(address _to) { 256 | require(_to != address(LinkToken), "Cannot callback to LINK"); 257 | _; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/test/mocks/MockV3Aggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @title MockV3Aggregator 6 | * @notice Based on the FluxAggregator contract 7 | * @notice Use this contract when you need to test 8 | * other contract's ability to read data from an 9 | * aggregator contract, but how the aggregator got 10 | * its answer is unimportant 11 | */ 12 | contract MockV3Aggregator { 13 | uint256 public constant version = 0; 14 | 15 | uint8 public decimals; 16 | int256 public latestAnswer; 17 | uint256 public latestTimestamp; 18 | uint256 public latestRound; 19 | 20 | mapping(uint256 => int256) public getAnswer; 21 | mapping(uint256 => uint256) public getTimestamp; 22 | mapping(uint256 => uint256) private getStartedAt; 23 | 24 | constructor(uint8 _decimals, int256 _initialAnswer) { 25 | decimals = _decimals; 26 | updateAnswer(_initialAnswer); 27 | } 28 | 29 | function updateAnswer(int256 _answer) public { 30 | latestAnswer = _answer; 31 | latestTimestamp = block.timestamp; 32 | latestRound++; 33 | getAnswer[latestRound] = _answer; 34 | getTimestamp[latestRound] = block.timestamp; 35 | getStartedAt[latestRound] = block.timestamp; 36 | } 37 | 38 | function updateRoundData( 39 | uint80 _roundId, 40 | int256 _answer, 41 | uint256 _timestamp, 42 | uint256 _startedAt 43 | ) public { 44 | latestRound = _roundId; 45 | latestAnswer = _answer; 46 | latestTimestamp = _timestamp; 47 | getAnswer[latestRound] = _answer; 48 | getTimestamp[latestRound] = _timestamp; 49 | getStartedAt[latestRound] = _startedAt; 50 | } 51 | 52 | function getRoundData(uint80 _roundId) 53 | external 54 | view 55 | returns ( 56 | uint80 roundId, 57 | int256 answer, 58 | uint256 startedAt, 59 | uint256 updatedAt, 60 | uint80 answeredInRound 61 | ) 62 | { 63 | return ( 64 | _roundId, 65 | getAnswer[_roundId], 66 | getStartedAt[_roundId], 67 | getTimestamp[_roundId], 68 | _roundId 69 | ); 70 | } 71 | 72 | function latestRoundData() 73 | external 74 | view 75 | returns ( 76 | uint80 roundId, 77 | int256 answer, 78 | uint256 startedAt, 79 | uint256 updatedAt, 80 | uint80 answeredInRound 81 | ) 82 | { 83 | return ( 84 | uint80(latestRound), 85 | getAnswer[latestRound], 86 | getStartedAt[latestRound], 87 | getTimestamp[latestRound], 88 | uint80(latestRound) 89 | ); 90 | } 91 | 92 | function description() external pure returns (string memory) { 93 | return "v0.6/tests/MockV3Aggregator.sol"; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/mocks/MockVRFCoordinatorV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol"; 5 | 6 | contract MockVRFCoordinatorV2 is VRFCoordinatorV2Mock { 7 | uint96 constant MOCK_BASE_FEE = 100000000000000000; 8 | uint96 constant MOCK_GAS_PRICE_LINK = 1e9; 9 | 10 | constructor() VRFCoordinatorV2Mock(MOCK_BASE_FEE, MOCK_GAS_PRICE_LINK) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/test/utils/Cheats.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | abstract contract Cheats { 5 | // sets the block timestamp to x 6 | function warp(uint256 x) public virtual; 7 | 8 | // sets the block number to x 9 | function roll(uint256 x) public virtual; 10 | 11 | // sets the slot loc of contract c to val 12 | function store( 13 | address c, 14 | bytes32 loc, 15 | bytes32 val 16 | ) public virtual; 17 | 18 | function ffi(string[] calldata) external virtual returns (bytes memory); 19 | 20 | function expectRevert(bytes calldata msg) external virtual; 21 | 22 | function expectEmit( 23 | bool, 24 | bool, 25 | bool, 26 | bool 27 | ) external virtual; 28 | } 29 | --------------------------------------------------------------------------------