Skip to content

Commit

Permalink
Add Token Deployment Script (#34)
Browse files Browse the repository at this point in the history
* Add Token Deployment Script

This patch adds saddle scripts to deploy, verify and match a cToken from a JSON configuration passed in on the command line. The goal is to make life very easy for deployment, specifically with builds from release versions of the Compound Protocol. Note: this does not _force_ developers to use release versions, but it means that developers can choose to deploy from versions of Compound that were audited. I've added significant usage notes to the README.

* Add Docker Builds

This patch adds Docker builds to our CI pipeline. This is the last step in letting the community run verified deployments. Every time a commit is pushed, an image and built and pushed to Docker Hub tagged with the commit (and if the commit is tagged, that will be included as a tag for Docker Hub as well).
  • Loading branch information
hayesgm committed Apr 24, 2020
1 parent 303b73d commit 18aa599
Show file tree
Hide file tree
Showing 11 changed files with 560 additions and 9 deletions.
31 changes: 31 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,33 @@ jobs:
at: ~/repo
- run: yarn run lint

docker:
docker:
- image: cimg/go:1.13
steps:
- run: |
if [ -z "$DOCKER_USER" -o -z "$DOCKER_PASS" -o -z "$DOCKER_REPO" ]; then
echo "No DOCKER_REPO, DOCKER_USER or DOCKER_PASS, skipping Docker build..."
circleci-agent step halt
fi
- checkout
- setup_remote_docker:
docker_layer_caching: true
- run: |
echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin
set -x
IMAGE_NAME=${CIRCLE_PROJECT_USERNAME/-/}/$CIRCLE_PROJECT_REPONAME
COMMIT_TAG=$(git rev-parse --short HEAD)
docker build -t $IMAGE_NAME:$COMMIT_TAG .
docker push $IMAGE_NAME:$COMMIT_TAG
if [ -n "$CIRCLE_TAG" ]; then
docker build -t $IMAGE_NAME:$CIRCLE_TAG .
docker push $IMAGE_NAME:$CIRCLE_TAG
fi
workflows:
version: 2
build-test-and-deploy:
Expand All @@ -166,3 +193,7 @@ workflows:
- coverage
- lint
- verify
- docker:
filters:
tags:
only: /^v.*/
4 changes: 3 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ junit.xml
.build
.last_confs
.git
script/certora
script/certora
.saddle_history
.circleci
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ scenario/.tscache
tests/scenarios/
junit.xml
.build
.last_confs
.last_confs
.saddle_history
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,91 @@ This command will start a saddle console conencted to Goerli testnet (see [Saddl
'10000000000000000000000000'
```

Deploying a CToken from Source
------------------------------

Note: you will need to set `~/.ethereum/<network>` with your private key or assign your private key to the environment variable `ACCOUNT`.

Note: for all sections including Etherscan verification, you must set the `ETHERSCAN_API_KEY` to a valid API Key from [Etherscan](https://etherscan.io/apis).

To deploy a new cToken, you can run the `token:deploy`. command, as follows. If you set `VERIFY=true`, the script will verify the token on Etherscan as well. The JSON here is the token config JSON, which should be specific to the token you wish to list.

```bash
npx saddle -n rinkeby script token:deploy '{
"underlying": "0x577D296678535e4903D59A4C929B718e1D575e0A",
"comptroller": "$Comptroller",
"interestRateModel": "$Base200bps_Slope3000bps",
"initialExchangeRateMantissa": "2.0e18",
"name": "Compound Kyber Network Crystal",
"symbol": "cKNC",
"decimals": "8",
"admin": "$Timelock"
}'
```

If you only want to verify an existing token an Etherscan, make sure `ETHERSCAN_API_KEY` is set and run `token:verify` with the first argument as the token address and the second as the token config JSON:

```bash
npx saddle -n rinkeby script token:verify 0x19B674715cD20626415C738400FDd0d32D6809B6 '{
"underlying": "0x577D296678535e4903D59A4C929B718e1D575e0A",
"comptroller": "$Comptroller",
"interestRateModel": "$Base200bps_Slope3000bps",
"initialExchangeRateMantissa": "2.0e18",
"name": "Compound Kyber Network Crystal",
"symbol": "cKNC",
"decimals": "8",
"admin": "$Timelock"
}'
```

Finally, to see if a given deployment matches this version of the Compound Protocol, you can run `token:match` with a token address and token config:

```bash
npx saddle -n rinkeby script token:match 0x19B674715cD20626415C738400FDd0d32D6809B6 '{
"underlying": "0x577D296678535e4903D59A4C929B718e1D575e0A",
"comptroller": "$Comptroller",
"interestRateModel": "$Base200bps_Slope3000bps",
"initialExchangeRateMantissa": "2.0e18",
"name": "Compound Kyber Network Crystal",
"symbol": "cKNC",
"decimals": "8",
"admin": "$Timelock"
}'
```

## Deploying a CToken from Docker Build
---------------------------------------

To deploy a specific version of the Compound Protocol, you can use the `token:deploy` script through Docker:

```bash
docker run --env ETHERSCAN_API_KEY --env VERIFY=true --env ACCOUNT=0x$(cat ~/.ethereum/rinkeby) compoundfinance/compound-protocol:latest npx saddle -n rinkeby script token:deploy '{
"underlying": "0x577D296678535e4903D59A4C929B718e1D575e0A",
"comptroller": "$Comptroller",
"interestRateModel": "$Base200bps_Slope3000bps",
"initialExchangeRateMantissa": "2.0e18",
"name": "Compound Kyber Network Crystal",
"symbol": "cKNC",
"decimals": "8",
"admin": "$Timelock"
}'
```

To match a deployed contract against a given version of the Compound Protocol, you can run `token:match` through Docker, passing a token address and config:

```bash
docker run --env ACCOUNT=0x$(cat ~/.ethereum/rinkeby) compoundfinance/compound-protocol:latest npx saddle -n rinkeby script token:match 0xF1BAd36CB247C82Cb4e9C2874374492Afb50d565 '{
"underlying": "0x577D296678535e4903D59A4C929B718e1D575e0A",
"comptroller": "$Comptroller",
"interestRateModel": "$Base200bps_Slope3000bps",
"initialExchangeRateMantissa": "2.0e18",
"name": "Compound Kyber Network Crystal",
"symbol": "cKNC",
"decimals": "8",
"admin": "$Timelock"
}'
```

Discussion
----------

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"solparse": "^2.2.8"
},
"dependencies": {
"eth-saddle": "0.1.10"
"eth-saddle": "0.1.17"
},
"resolutions": {
"scrypt.js": "https://registry.npmjs.org/@compound-finance/ethereumjs-wallet/-/ethereumjs-wallet-0.6.3.tgz",
Expand Down
7 changes: 6 additions & 1 deletion saddle.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ module.exports = {
web3: {
gas: [
{env: "GAS"},
{default: "4600000"}
{default: "5600000"}
],
gas_price: [
{env: "GAS_PRICE"},
Expand Down Expand Up @@ -235,5 +235,10 @@ module.exports = {
'Contracts': value
}, null, 4));
});
},
scripts: {
'token:deploy': "script/saddle/deployToken.js",
'token:verify': "script/saddle/verifyToken.js",
'token:match': "script/saddle/matchToken.js"
}
}
67 changes: 67 additions & 0 deletions script/saddle/deployToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
let { loadConf } = require('./support/tokenConfig');

function printUsage() {
console.log(`
usage: npx saddle script token:deploy {tokenConfig}
note: pass VERIFY=true and ETHERSCAN_API_KEY=<api key> to verify contract on Etherscan
example:
npx saddle -n rinkeby script token:deploy '{
"underlying": "0x577D296678535e4903D59A4C929B718e1D575e0A",
"comptroller": "$Comptroller",
"interestRateModel": "$Base200bps_Slope3000bps",
"initialExchangeRateMantissa": "2.0e18",
"name": "Compound Kyber Network Crystal",
"symbol": "cKNC",
"decimals": "8",
"admin": "$Timelock"
}'
`);
}

function sleep(timeout) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, timeout);
});
}

(async function() {
if (args.length !== 1) {
return printUsage();
}

let conf = loadConf(args[0], addresses);
if (!conf) {
return printUsage();
}

console.log(`Deploying cToken with ${JSON.stringify(conf)}`);

let deployArgs = [conf.underlying, conf.comptroller, conf.interestRateModel, conf.initialExchangeRateMantissa.toString(), conf.name, conf.symbol, conf.decimals, conf.admin];
let contract = await saddle.deploy('CErc20Immutable', deployArgs);

console.log(`Deployed contract to ${contract._address}`);

if (env['VERIFY']) {
const etherscanApiKey = env['ETHERSCAN_API_KEY'];
if (etherscanApiKey === undefined || etherscanApiKey.length === 0) {
throw new Error(`ETHERSCAN_API_KEY must be set if using VERIFY flag...`);
}

console.log(`Sleeping for 30 seconds then verifying contract on Etherscan...`);
await sleep(30000); // Give Etherscan time to learn about contract
console.log(`Now verifying contract on Etherscan...`);

await saddle.verify(etherscanApiKey, contract._address, 'CErc20Immutable', deployArgs, 0);
console.log(`Contract verified at https://${network}.etherscan.io/address/${contract._address}`);
}

return {
...conf,
address: contract._address
};
})();
45 changes: 45 additions & 0 deletions script/saddle/matchToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
let { loadAddress, loadConf } = require('./support/tokenConfig');

function printUsage() {
console.log(`
usage: npx saddle script token:match address {tokenConfig}
This checks to see if the deployed byte-code matches this version of the Compound Protocol.
example:
npx saddle -n rinkeby script token:match 0x19B674715cD20626415C738400FDd0d32D6809B6 '{
"underlying": "0x577D296678535e4903D59A4C929B718e1D575e0A",
"comptroller": "$Comptroller",
"interestRateModel": "$Base200bps_Slope3000bps",
"initialExchangeRateMantissa": "2.0e18",
"name": "Compound Kyber Network Crystal",
"symbol": "cKNC",
"decimals": "8",
"admin": "$Timelock"
}'
`);
}

(async function() {
if (args.length !== 2) {
return printUsage();
}

let address = loadAddress(args[0], addresses);
let conf = loadConf(args[1], addresses);
if (!conf) {
return printUsage();
}

console.log(`Matching cToken at ${address} with ${JSON.stringify(conf)}`);

let deployArgs = [conf.underlying, conf.comptroller, conf.interestRateModel, conf.initialExchangeRateMantissa.toString(), conf.name, conf.symbol, conf.decimals, conf.admin];

await saddle.match(address, 'CErc20Immutable', deployArgs);

return {
...conf,
address
};
})();
83 changes: 83 additions & 0 deletions script/saddle/support/tokenConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@

function getRaw(config, key, required=true) {
let value = config[key];
if (!value) {
throw new Error(`Config missing required key \`${key}\``);
}
return value;
}

function getString(config, key, required=true) {
let value = getRaw(config, key, required);
if (value === "" && required) {
throw new Error(`Config missing required key \`${key}\``);
}
return value || "";
}

function loadAddress(value, addresses, errorMessage=null) {
if (value.startsWith("$")) {
let contract = value.slice(1,);
let address = addresses[contract];
if (!address) {
throw new Error(`Cannot find deploy address for \`${contract}\``);
}
return address;
} else if (value.startsWith("0x")) {
return value;
} else {
throw new Error(errorMessage || `Invalid address \`${value}\``);
}
}

function getAddress(addresses, config, key, required=true) {
let value = getString(config, key, required);
return loadAddress(
value,
addresses,
`Invalid address for \`${key}\`=${value}`,
required
);
}

function getNumber(config, key, required=true) {
let value = getRaw(config, key, required);
let result = Number(value);
if (Number.isNaN(result)) {
throw new Error(`Invalid number for \`${key}\`=${value}`);
} else {
return Number(result);
}
}

function loadConf(configArg, addresses) {
let config;
if (!configArg) {
return null;
}

try {
config = JSON.parse(configArg)
} catch (e) {
console.log();
console.error(e);
return null;
}
const conf = {
underlying: getAddress(addresses, config, 'underlying'),
comptroller: getAddress(addresses, config, 'comptroller'),
interestRateModel: getAddress(addresses, config, 'interestRateModel'),
initialExchangeRateMantissa: getNumber(config, 'initialExchangeRateMantissa'),
name: getString(config, 'name'),
symbol: getString(config, 'symbol'),
decimals: getNumber(config, 'decimals'),
admin: getAddress(addresses, config, 'admin'),
};

return conf;
}

module.exports = {
loadAddress,
loadConf
};
Loading

0 comments on commit 18aa599

Please sign in to comment.