├── .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 |
9 |
10 |
11 |
12 |
13 | [](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 |
--------------------------------------------------------------------------------