# 1. Preparation
Complete these steps before the workshop, and let me know if you have any issues or need any help!
## 1.1 VSCode
If you don't have a text editor or IDE to work in, [VSCode](https://code.visualstudio.com/) is a popular one, so install that
## 1.2 Volta
[Volta](https://volta.sh/) is used to manage [node](https://nodejs.org/en/) versions. Install by running:
```sh
# Install volta
$ curl https://get.volta.sh | bash
# Install node
$ volta install node
```
## 1.3 Yarn
[Yarn](https://classic.yarnpkg.com/en/) is a package manager used to install and manage a project's dependencies
```sh
$ npm install --global yarn
```
## 1.4 Setup the template project
Open the Terminal app and follow the steps shown in the code below
```sh
# 1.4.1 Navigate to the directory where you want the project to live. Below
# we assume we want the project to live in our Documents folder
cd ~/Documents/
# 1.4.2 Clone the repository. This project now lives in the
# ~/Documents/cozy-developer-guides folder
$ git clone https://github.com/Cozy-Finance/cozy-developer-guides.git
$ cd cozy-developer-guides
# 1.4.3 Install dependencies. This installs Hardhat (for running a local
# Ethereum blockchain), ethers.js (for interacting with the blockchain), and more
$ yarn
# 1.4.4 Now let's open the project in VSCode. Do this by first opening VSCode,
# then clicking File > Open Workspace and selecting the cozy-developer-guides
# folder. Now press the keyboard shortcut `Cmd + ~` to open a terminal
# in your VSCode window. You can enter the below commands in that terminal
# 1.4.5 Copy the template file containing environment variables. The below
# command copies the file called `.env.template` and names the copy `.env`.
# After running this command, open the .env file and fill in the two fields.
# For convenience, you can use the below private key:
# 0x548e1b48b83ec372eafcf6f95b538828d40a070d22516da5d86091cd273619bc
cp .env.example .env
# 1.4.6 Compile contracts as confirmation that setup is complete
$ yarn compile
# 1.4.7 Run a sample script of one of the guides. If you chose a private key
# that corresponds to an address that has no ETH, that's ok. In that case
# the below command will error with "InvalidInputError: sender doesn't have
# enough funds to send tx"
$ yarn hardhat run guides/buy-protection.ts
```
## 1.5 Additional Context
For context, it's recommended to read the [Creating a Protection Market](https://app.gitbook.com/@cozy-finance-1/s/cozy-docs/for-developers/guides/how-do-i-create-a-protection-market) guide before continuing to familiarize yourself with the process we'll be following.
# 2. Live Session
Our trigger contract is useless if we can't verify it works properly. So before we start developing our trigger contract, we'll need to ensure we'll be able to test our contract.
First make sure the VSCode file explorer is open on the left-hand side. Do this by clicking `Cmd + Shift + E` and you should see a list of files and folders.
## 2.1 Test Setup
Right-click on the folder called test and click "New File", and call the file `CompoundInvariant.test.ts`. This will be the test file used to test our trigger contract, which will ensure a Compound invariant holds
The developer guides comes with a simple `MockTrigger` contract, so let's use that for now. Paste the following code into this file to write a simple test that ensures the `MockTrigger` contract deploys.
```typescript
import { artifacts, ethers, waffle } from 'hardhat';
import { expect } from 'chai';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address';
import { MockTrigger } from '../typechain';
const { deployContract } = waffle; // helper method for deploying contracts
describe('CompoundInvariant', function () {
// Define variables that we'll need in our tests
let deployer: SignerWithAddress, recipient: SignerWithAddress;
let trigger: MockTrigger;
let triggerParams: any[] = []; // trigger params deployment parameters
before(async () => {
// Use the default Ethereum accounts provided by Hardhat
[deployer, recipient] = await ethers.getSigners();
});
beforeEach(async () => {
// Define the properties of this trigger
triggerParams = [
'Mock Trigger Trigger', // name
'MOCK-TRIG', // symbol
'Triggers whenever its told to trigger', // description
[4], // platform ID for Compound
recipient.address, // subsidy recipient
];
// Deploy the trigger
const mockTriggerArtifact = await artifacts.readArtifact('MockTrigger');
trigger = <MockTrigger>await deployContract(deployer, mockTriggerArtifact, triggerParams);
});
it('should deploy', async () => {
// Simple test to ensure the trigger deployed successfully
expect(trigger.address).to.not.equal(ethers.constants.AddressZero);
});
});
```
You can now run `yarn test` in your terminal and if you did everything correctly, you should see something like this, showing that our deployment test was successful!
```
CompoundInvariant
✓ should deploy
1 passing (12s)
```
## 2.2 Trigger Development
Now that we have our test setup ready, let's write our trigger. Luckily for you, that's already done. Follow the steps below
1. Right click on the `contracts` folder, click "New Folder", and name it `test`
2. Open the repository containing our trigger contracts
1. Copy the `MockCozyToken.sol` and `MockCToken.sol` contracts into the `contracts/test` folder we just created
2. Copy the `CompoundInvariant.sol`, `ICToken.sol`, and `ITrigger.sol` files into the contracts folder
3. Some notes on the above contracts, which we'll explain in more detail during the session:
1. Contract names that start with `I` are interfaces used to interact with other contracts. You'll notice that `ITrigger.sol` and `TriggerInterface.sol` are identical, with the former name being my preferred naming convention and the latter being Compound's
2. Contracts that start with `Mock*` are special contracts intended to replace real mainnet contracts. Mocks have the same interfaces as real contracts, but we can set the values to anything we want, which makes them great for testing different scenarios
3. In the terminal, run `yarn compile`. If you did it right, you should see something like the below
```
Successfully generated Typechain artifacts!
✨ Done in 4.84s.
```
This is of course the most important part of the trigger development process, so during the workshop we'll talk through this step in more detail.
## 2.3 Trigger Testing
This final step is to test our contract. To save time, replace everything in `CompoundInvariant.test.ts` with the contents below. We'll walk through this file during the workshop
```typescript
import { artifacts, ethers, waffle } from 'hardhat';
import { expect } from 'chai';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address';
import { MockCozyToken, MockCToken, CompoundInvariant } from '../typechain';
const { deployContract } = waffle;
const { formatBytes32String } = ethers.utils;
describe('CompoundInvariant', function () {
let deployer: SignerWithAddress, recipient: SignerWithAddress;
let mockCUsdc: MockCToken;
let trigger: CompoundInvariant;
let triggerParams: any[] = []; // trigger params deployment parameters
before(async () => {
[deployer, recipient] = await ethers.getSigners();
});
beforeEach(async () => {
// Deploy Mock CToken
const mockCTokenArtifact = await artifacts.readArtifact('MockCToken');
mockCUsdc = <MockCToken>await deployContract(deployer, mockCTokenArtifact);
// Deploy CompoundInvariant trigger
triggerParams = [
'Compound Invariant Trigger', // name
'COMP-INV-TRIG', // symbol
'Triggers when the Compound invariant that reserves + supply = cash + borrows is violated', // description
[4], // platform ID for Compound
recipient.address, // subsidy recipient
mockCUsdc.address, // address of the Compound CToken market this trigger checks
];
const compoundInvariantArtifact = await artifacts.readArtifact('CompoundInvariant');
trigger = <CompoundInvariant>await deployContract(deployer, compoundInvariantArtifact, triggerParams);
});
it('deployment: should not deploy if market is already triggered', async () => {
// Break invariant of the CToken
await mockCUsdc.set(formatBytes32String('cash'), 0);
expect(await mockCUsdc.getCash()).to.equal(0);
// Try deploying the trigger
const compoundInvariantArtifact = await artifacts.readArtifact('CompoundInvariant');
await expect(deployContract(deployer, compoundInvariantArtifact, triggerParams)).to.be.revertedWith(
'Market already triggered'
);
});
it('checkAndToggleTrigger: does nothing when called on a valid market', async () => {
expect(await trigger.isTriggered()).to.be.false;
await trigger.checkAndToggleTrigger();
expect(await trigger.isTriggered()).to.be.false;
});
it('checkAndToggleTrigger: toggles trigger when called on a broken market', async () => {
expect(await trigger.isTriggered()).to.be.false;
await mockCUsdc.set(formatBytes32String('totalSupply'), 0);
expect(await trigger.isTriggered()).to.be.false; // trigger not updated yet, so still expect false
const tx = await trigger.checkAndToggleTrigger();
await expect(tx).to.emit(trigger, 'TriggerActivated');
expect(await trigger.isTriggered()).to.be.true;
});
it('checkAndToggleTrigger: returns a boolean with the value of isTriggered', async () => {
// Deploy our helper contract for testing, which has a state variable called isTriggered that stores the last
// value returned from trigger.checkAndToggleTrigger()
const mockCozyTokenArtifact = await artifacts.readArtifact('MockCozyToken');
const mockCozyToken = <MockCozyToken>await deployContract(deployer, mockCozyTokenArtifact, [trigger.address]);
expect(await mockCozyToken.isTriggered()).to.be.false;
// Break the CToken
await mockCUsdc.set(formatBytes32String('totalReserves'), 0);
await mockCozyToken.checkAndToggleTrigger();
expect(await mockCozyToken.isTriggered()).to.be.true;
});
it('tolerance: only considers invariant as violated when > 0.000001% error', async () => {
// Duplicate the logic from the isMarketTriggered method
const getPercent = (reserves: bigint, borrows: bigint, supply: bigint, exchangeRate: bigint, cash: bigint) => {
const lhs = (supply * exchangeRate) / WAD + reserves;
const rhs = cash + borrows;
const diff = lhs > rhs ? lhs - rhs : rhs - lhs;
const denominator = lhs < rhs ? lhs : rhs;
return (diff * WAD) / denominator;
};
// Read the tolerance from the contract and verify the value
const tolerance = (await trigger.tolerance()).toBigInt();
expect(tolerance).to.equal(10n ** 12n); // 10^12 = 10^-6 * 10^18 = 0.000001%
// Use the same values we initialize the MockCToken with
const WAD = 10n ** 18n;
let totalReserves = 5359893964073n; // units of USDC
let totalBorrows = 3681673803163527n; // units of USDC
let totalSupply = 20287132947568793418n; // units of cUSDC
let exchangeRateStored = 219815665774648n; // units of 10^(18 + underlyingDecimals - 8)
let cash = 783115726329188n; // units of USDC
// Calculate the initial percent error using the same method as the contract
const percent1 = getPercent(totalReserves, totalBorrows, totalSupply, exchangeRateStored, cash);
// This percent should be smaller than our tolerance
expect(percent1 < tolerance).to.be.true;
// Because it's within tolerance, trigger should not toggle
await trigger.checkAndToggleTrigger();
expect(await trigger.isTriggered()).to.be.false;
// Define new values that put the error slightly above our tolerance
totalReserves = 10000000000n;
totalBorrows = 10000n;
totalSupply = 10000000000n;
exchangeRateStored = 100000n;
cash = 10000000001n;
await mockCUsdc.set(formatBytes32String('totalReserves'), totalReserves.toString());
await mockCUsdc.set(formatBytes32String('totalBorrows'), totalBorrows.toString());
await mockCUsdc.set(formatBytes32String('totalSupply'), totalSupply.toString());
await mockCUsdc.set(formatBytes32String('exchangeRateStored'), exchangeRateStored.toString());
await mockCUsdc.set(formatBytes32String('cash'), cash.toString());
const percent2 = getPercent(totalReserves, totalBorrows, totalSupply, exchangeRateStored, cash);
expect(percent2 > tolerance).to.be.true;
// Our tolerance of 0.000001% = 1000000000000 as a wad
// New percent error = 1000100000000 as a wad
// Therefore difference = 100000000 as a wad
expect(percent2 - tolerance).to.equal(100000000n);
// Because it's outside tolerance, trigger should toggle
await trigger.checkAndToggleTrigger();
expect(await trigger.isTriggered()).to.be.true;
});
});
```
Now run `yarn test` in the terminal, and if you did everything correctly you should see something like this:
```
CompoundInvariant
✓ deployment: should not deploy if market is already triggered (454ms)
✓ checkAndToggleTrigger: does nothing when called on a valid market (63ms)
✓ checkAndToggleTrigger: toggles trigger when called on a broken market (120ms)
✓ checkAndToggleTrigger: returns a boolean with the value of isTriggered (375ms)
✓ tolerance: only considers invariant as violated when > 0.000001% error (173ms)
5 passing (11s)
✨ Done in 20.21s.
```
## 2.4 Trigger Deployment
Because we're working in the cozy-developer-guides project, which already has a sample `create-protection-market` script in the `guides` folder, this step should be pretty simple!
We'll assume the Compound Market this trigger will cover is cDAI, and therefore it makes sense for us to use DAI as the underlying.
Follow these steps to create your new protection market:
1. Copy the `create-protection-market.ts` file and rename it to `create-compound-protection-market.ts`
2. Under `STEP 1`, we'll need to set our trigger properties to something like this:
```typescript
const name = 'Compound Invariant Trigger'; // trigger name
const symbol = 'COMP-INV-TRIG'; // trigger symbol
const description = 'Triggers when the Compound invariant that reserves + supply = cash + borrows is violated'; // trigger description
const platformIds = [4]; // array of platform IDs that this trigger protects
const recipient = '0x1234567890AbcdEF1234567890aBcdef12345678'; // address of subsidy recipient
```
3. On lines 35–41, you can see we're deploying the `MockTrigger` here, but we want to deploy our `CompoundInvariant` trigger
1. Press `Opt + Cmd + F` to open the Find and Replace menu
2. We want to find all instances of `MockTrigger` and replace them with `CompoundInvariant`
4. Notice how the `CompoundInvariant` has one additional input to it's constructor—the address of the cToken this trigger checks. This script runs against Rinkeby by default, so we need to specify the address of the cDAI token. We can do this by modifying line 39 as follows:
```typescript
const cDaiAddress = '0x6D7F0754FFeb405d23C51CE938289d4835bE3b14'; // new line
const trigger: Contract = await CompoundInvariantFactory.deploy(name, symbol, description, platformIds, recipient, cDaiAddress); // new input at the end
```
5. On line 46, you can see we already have DAI as the underlying token for the market, so we can leave this as-is
6. If you want to change the account you are deploying from, update the `PRIVATE_KEY` variable in the `.env` file (for this workshop, you can leave it)
7. Run `yarn hardhat run guides/create-compound-protection-market.ts `
If you did everything correctly, you should see something like this logged in your terminal:
```
✓ CompoundInvariant deployed to 0xF26018FeD98BA1272C88C6Fb701b67E7232Bf8F1
Duplicate definition of ActionPaused (ActionPaused(string,bool), ActionPaused(address,string,bool))
✓ Safe to continue: Found DAI Money Market at 0xe2A99B56CA4B9462498148abC431cDd45619BE2D
✓ Success! Protection Market deployed to 0xdbE7e4C79AD1d50c2DEAC2F14e3767D0aD8E9c44 in transaction 0xe1d2d406b09c846d9461a83f78952bc076d868c690a5a423ca4524232d5382cb
✨ Done in 33.01s.
```
This means you successfully deployed your Protection Market against a *forked Rinkeby*, because that is the default network configuration of this repository. Deploying against a forked Rinkeby means you ran a local chain that has the same state as the Rinkeby network, and deployed your Protection Market on your local copy of Rinkeby.
Because it worked on your local Rinkeby, we can confidently deploy on the real Rinkeby.
If you want deploy this on the real Rinkeby network, all you need to do is specify the Rinkeby network when you run the script, which you can do by adding `--network rinkeby` to the end of your command: `yarn hardhat run guides/create-compound-protection-market.ts --network rinkeby`