Create a protection market

Learn how to create a trigger contract for a protection market.

Protection markets are intended to protect users of on-chain protocols against the kinds of smart contract or protocol failures that make investing in decentralized financial products seem riskier than investing in traditional asset classes. Some of the common problems and risks that investors in on-chain protocols face include flaws in smart contract logic, security vulnerabilities that hackers can exploit, and unscrupulous insiders.

To address these issues, the key to creating a protection market is writing and deploying a trigger contract that limits an investor's exposure if a protocol is hacked or a fund suffers a rug pull maneuver.

At a high-level, you can create a protection market by taking the following steps:‌

  1. Prepare a trigger contract so that it's compatible with the Cozy protocol.

  2. Develop the trigger contract logic to detect potential problems for one or more on-chain protocols.

  3. Deploy the trigger contract as a protection market to make protected investing and protected borrowing available for users who want purchase protection or supply assets.

Everything in this guide assumes that you have experience with JavaScript, ethers.js, and Solidity.

Prepare a trigger contract

Before you start creating a trigger contract of your own, let’s take a closer look at what’s involved. There are three key parts that go into creating a trigger contract for a protection market:

  • The abstract trigger contract.

  • The trigger contract that inherits the abstract trigger contract and contains the code to check for a trigger condition.

  • The deployment script that provides input parameters to the trigger contract when deployed.

Let’s review what’s in each of these files and what you do with them.

Review the abstract trigger contract

if you cloned the Cozy Developer Guides repository, you have an abstract trigger contract called ITrigger defined in the ITrigger.sol file in the contracts/interface/ folder. This abstract trigger contract serves as the base or parent contract for the trigger contract you will write for a protection market.

You don't need to make any changes to the abstract trigger contract. It's provided to make developing your own trigger contract easier. Before you start writing your own trigger contract, however, you might want to take a closer look at what’s in the ITrigger.sol abstract trigger contract first.

As you review the ITrigger.sol file, there are just a few lines to take note of:

  • The ITrigger.sol file specifies the data types and identifiers for the basic properties of the contract and define the constructor arguments that will be passed in from a deployment script after the contract is deployed on the network.

  • The ITrigger.sol file declares the isTriggered state variable and declares a TriggerActivated() event to emit if a condition has resulted in the isTriggered function returning true.

  • The getPlatformIds() function is used to get an array of platform identifiers, which represent the protocols protected by this trigger contract.

  • The checkTriggerCondition() function defines the logic that determines if the trigger condition for any of the protected protocols has occurred. This function is not implemented in the ITrigger.sol file. You’ll implement the checkTriggerCondition() function when you write the trigger contract that inherits from the abstract contract.

  • The checkAndToggleTrigger() function sets the isTriggered flag to true if the condition defined for the checkTriggerCondition() function is met. This function is implemented in the abstract contract and must not be modified.

Once you are familiar with the code in the ITrigger.sol file, you are ready to start coding your own trigger contract.

Prepare a trigger contract skeleton

Now that you are familiar with what's in the base trigger contract that your contract will inherit, you are ready to start writing the contract that contains the trigger logic for your protection market. Let’s start by creating a trigger contract skeleton. You can create the basic framework for the trigger contract in an empty file or you can use the MockTrigger.sol file in the developer-guides repository as a starting point. To illustrate what’s involved, we’ll create an empty file and copy and paste portions of the MockTrigger into the custom contract.

To create the basic framework for your custom trigger contract:

  1. Create a new empty file. For example, create a new file called TestTrigger.sol by running the following command:

    touch contracts/TestTrigger.sol
  2. Open the file in a text editor and insert a line with the version pragma to specify the compiler version this trigger contract requires. For example:

    pragma solidity ^0.8.5;
  3. Import the abstract trigger contract and set the current trigger contract to inherit it. For example:

    import "./interfaces/ITrigger.sol";
    contract TestTrigger is ITrigger {

    The shouldToggle function in the MockTrigger contract is only used for example purposes in that contract, so we don’t need to include it in our contract skeleton.

  4. Prepare some expected properties for the contract:

    Because the constructor arguments of the base contract depend on values that will be passed to the derived contract, they are defined like this in our TestTrigger contract:

    constructor(
    string memory _name,
    string memory _symbol,
    string memory _description,
    uint256[] memory _platformIds,
    address _recipient,
    bool _shouldToggle
    ) ITrigger(_name, _symbol, _description, _platformIds, _recipient) {
    }

    These basic properties are inherited from the base trigger contract. The values for the properties are supplied when the contract is deployed.

    We’ll get to that step in a minute, but first, let’s complete our skeleton trigger contract by adding a placeholder for the required checkTriggerCondition() function.

  5. Add a placeholder for checkTriggerCondition() function. For example:

    function checkTriggerCondition() internal override returns (bool) {
    }

Configure parameters in a deployment script

So far, we’ve reviewed the abstract contract and built a skeleton for the trigger contract. The next step in preparing the trigger contract is to configure required trigger parameters that will be supplied to the trigger contract when it is deployed:‌ These parameters provide the data to the constructor properties you saw in the abstract contract and consist of the following:

  • The trigger name is similar to the ERC-20 name property.

  • The trigger symbol is similar to the ERC-20 symbol property.

  • The description of the trigger that describes what the trigger does.

  • The numeric identifier for each protocol that your trigger monitors for failure conditions. For example, if this trigger covers the failure of a Yearn vault, and Yearn has an ID of 1, the platformId would be [1]. For a list of platform names and their ID numbers, see Platform identifiers for protocols.

  • The trigger recipient to define an address to which to distribute rewards. (Use a sybil.org verified address if you want users to know you created the protection.)

For example:

create-protection-market.ts
// Define required constructor parameters
const name = 'Mock Trigger'; // trigger name
const symbol = 'MOCK'; // trigger symbol
const description = 'A mock trigger that anyone can toggle'; // trigger description
const platformIds = [3]; // array of platform IDs that this trigger protects
const recipient = '0x1234567890AbcdEF1234567890aBcdef12345678'; // address of subsidy recipient
const shouldToggle = false; // specific to our MockTrigger, which we set to not be triggered at deployment

Develop the trigger contract logic

You are now ready to implement the actual trigger logic for your contract using the checkTriggerCondition() function that you inherited from the abstract contract. The checkTriggerCondition() function is where you write the core logic of your trigger contract, specifying the condition that should trigger protection for the protocol(s) defined by getPlatformIds() parameter.

Note that the checkAndToggleTrigger() and checkTriggerCondition() functions are crucial to ensuring your trigger behaves properly, so read the following section carefully:

  • You must implement checkTriggerCondition() to execute some logic and return true if the trigger event has occurred, and return false otherwise.

  • You must ensure that the only way to update the value of the isTriggered variable is through the checkTriggerCondition() function.

  • You must not modify the checkAndToggleTrigger() function inherited from the abstract contract. This function is implemented to conform to the interface and behavior expected by Cozy protection markets. The checkAndToggleTrigger() function is called by a protection market to determine the trigger's state.‌

You should also note that protection market triggers are "one-way" toggles. Each protection market stores its own isTriggered variable—separate from the trigger contract's isTriggered variable—which is initialized to false and toggled only once, when the trigger'scheckAndToggleTrigger() method returns true. Because it can only be toggled once, to avoid confusion, it's recommended that triggers themselves should also follow the same convention of only allowing "one-way" toggles.‌

The following is an example Mock Trigger contract that is also available in the Cozy Developer Guides repository.

MockTrigger.sol
pragma solidity ^0.8.5;
import "./interfaces/ITrigger.sol";
contract MockTrigger is ITrigger {
/// @notice If true, checkAndToggleTrigger will toggle the trigger on its next call
bool public shouldToggle;
constructor(
string memory _name,
string memory _symbol,
string memory _description,
uint256[] memory _platformIds,
address _recipient,
bool _shouldToggle
) ITrigger(_name, _symbol, _description, _platformIds, _recipient) {
shouldToggle = _shouldToggle;
// Verify market is not already triggered.
require(!checkTriggerCondition(), "Already triggered");
}
/**
* @notice Special function for this mock trigger to set whether or not the trigger should toggle
*/
function setShouldToggle(bool _shouldToggle) external {
require(!isTriggered, "Cannot set after trigger event");
shouldToggle = _shouldToggle;
}
/**
* @notice Returns true if the market has been triggered, false otherwise
*/
function checkTriggerCondition() internal view override returns (bool) {
return shouldToggle;
}
}

When testing a trigger contract, it is helpful to initialize your test suite using the test template in test/MockTrigger.test.ts. This file contains sample implementations of common test cases that you'll want to use when testing your trigger's functionality.

Deploy a protection market

Once your trigger contract is complete, you are ready to deploy a new protection market that uses that trigger.‌ You deploy a protection market by defining the following properties:

  • Trigger contract address.

  • Interest rate model contract address.

  • Underlying token that is supplied to and borrowed from that market.

Select an interest rate model

An interest rate model is a contract that takes current market parameters as inputs and returns what the market's borrowing interest rate should be. Before you deploy your protection market, you need to decide on the interest rate model to use for it.

The simplest option is to use the "Default Protection Market Interest Rate Model". You can find the contract address for this interest rate model on the Contract deployments page. This interest rate model is defined as follows:

  • 0% base rate (if nothing is borrowed, the borrow rate is zero).

  • At an 80% utilization ratio, the borrow rate is 20%, and the rate increases linearly between 0% and 80% utilization.

  • At a 100% utilization ratio, the borrow rate is 125%, and the rate increases linearly between 80% and 100% utilization.

If you want to use a different interest rate model, the Contract deployments page lists other available options. If none of the pre-deployed interest rate models suit your use case, you can deploy your own. In the cozy-developer-guides repository includes an abstract contract called IInterestRateModel. By inheriting this contract, you'll have the correct interface and layout for building and deploying any interest rate model you would like to use.

After you have selected an interest rate model, deploy the contract for it (if necessary), and save the contract address for use in later steps.

Set the underlying token

The next step in deploying a protection market is to choose the underlying ERC-20 token (or ETH) to use for your protection market. For example, if you are writing a trigger to protect a Yearn yUSDC vault, selecting USDC as the underlying token would enable users to borrow protected USDC from the protection market and immediately use it to mint protected yUSDC shares on Yearn. If you were to chose DAI as the underlying token, users would have to borrow protected DAI and swap it for USDC before they can get protected yUSDC shares.

To use ETH as the underlying token, use an address of 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE

To deploy a protection market with a given underlying token, a money market for that underlying token must exist. After you select an underlying token, you should verify that a money market for that token exists by calling the Comptroller.getCToken(underlying, AddressZero) function. The Comptroller stores a mapping—called getCToken—that maps an underlying token address to a trigger address to a market address. Money markets use the zero address as their trigger address. If the Comptroller.getCToken(underlying, AddressZero) check returns the zero address, a money market for the underlying token token does not exist, and you'll have to wait for one to be deployed by governance before continuing. (If you try to deploy without a supported underlying token money market, protection market deployment will revert).‌

The create-protection-market.ts script shows how to run this check when USDC is the underlying token. If a money market does exist, you can continue to the next step to deploy the final version of your trigger contract.

Deploy the protection market

You can now deploy your protection market in one of two ways:

  • By using the default interest rate model.

    // Option 1: Use the default interest rate model
    Comptroller.deployProtectionMarket(underlying, trigger);
  • By specifying an interest rate model.

    // Option 2: Specify an interest rate model
    Comptroller.deployProtectionMarket(underlying, trigger, interestRateModel)

where

  • underlying is the underlying token's address for this CozyToken market.

  • trigger is the address of your trigger contract.

  • interestRateModel is the address of your chosen interest rate model contract.

Because deployProtectionMarket is an overloaded function, when using ethers.js and the sample scripts to deploy a market, we must specify which syntax we're using.

The full process for deploying a trigger and using it to create a protection market is shown in the create-protection-market.ts script:

create-protection-market.ts
// Get instance of the Trigger ContractFactory with our signer attached
const MockTriggerFactory: ContractFactory = await hre.ethers.getContractFactory('MockTrigger', signer);
// Deploy the trigger contract (last constructor parameter is
// specific to the mock trigger contract)
const triggerParams = [name, symbol, description, platformIds, recipient, shouldToggle];
const trigger: Contract = await MockTriggerFactory.deploy(...triggerParams);
await trigger.deployed();
logSuccess(`MockTrigger deployed to ${trigger.address}`);
// Let's choose USDC as the underlying, so first we need to check if
// there's a USDC Money Market. We know that Money Markets have a
// trigger address of the zero address, so we use that to query the
// Comptroller for the Money Market address
const usdcAddress = getContractAddress('USDC', chainId);
const comptrollerAddress = getContractAddress('Comptroller', chainId);
const comptroller = new Contract(comptrollerAddress, comptrollerAbi, signer); // connect signer for sending transactions
const cozyUsdcAddress = await comptroller.getCToken(usdcAddress, AddressZero);
// If the returned address is the zero address, a money market does
// not exist and we cannot deploy a protection market with USDC as
// the underlying
if (cozyUsdcAddress === AddressZero) {
logFailure('No USDC Money Market exists. Exiting script');
return;
}
logSuccess(`Safe to continue: Found USDC Money Market at ${cozyUsdcAddress}`);
// If we're here, a USDC Money Market exists, so it's safe to create
// our new Protection Market. If we tried to create a new Protection
// Market before a USDC Money Market existed, our transaction would
// revert. Also, notice how we do not provide an `interestRateModel`
// address--this means we'll use the default interest rate model
// specified by the `ProtectionMarketFactory` contract. If you want
// to use a custom interest rate model, develop, test, and deploy your
// interest rate model, then use the commented out version below
const tx = await comptroller['deployProtectionMarket(address,address)'](usdcAddress, trigger.address);
// const tx = await comptroller['deployProtectionMarket(address,address,address)'](usdcAddress, trigger.address, interestRateModelAddress);
// This should emit a ProtectionMarketListed event on success, so
// let's check for that event. If not found, this method will throw
// and print the Failure error codes which can be looked up in
// ErrorReporter.sol
const { log, receipt } = await findLog(tx, comptroller, 'ProtectionMarketListed', provider);
logSuccess(`Success! Protection Market deployed to ${log?.args.cToken} in transaction ${receipt.transactionHash}`);

Verify your protection market

After you deploy your trigger contract and interest rate model contract to create a protection market, you can verify that your protection market exists by looking at the Cozy subgraph.

Your protection market will be initialized with a collateral factor of zero and use the specified trigger, underlying , and interestRateModel addresses. Note that you can change the interest rate model by submitting a governance proposal.

Tips for trigger contract developers

Trigger contract logic can sometimes have unintended consequences or undesirable effects. This section provides some tips and suggestions to help you avoid the pitfalls.

Consider the gas required for computation

In a protection market, the trigger contract always calls the checkAndToggleTrigger() method immediately before every borrow and redeem action. This call is required to mitigate front-running. However, each call incurs a cost in gas for execution and this increases the cost of borrowing and redeeming funds.

In writing trigger contract logic, you should try to keep the gas costs for checking the trigger condition as low as possible, as users will pay the cost on every borrow and redeem action. For example, you can reduce the gas required tor checking the trigger condition by:

  • Keeping trigger logic as simple as possible.

  • Minimizing operations that read from or write to storage.

  • Minimizing calls to external contracts or reading data from other contracts.

You can also find general guidelines for limiting gas usage in Solidity gas optimization tips and Solidity tips and tricks to save gas and reduce bytecode size.

Avoid code that encourages front-running

If it's known that a trigger condition will occur before reflected on-chain, that trigger may be susceptible to front-running. This would incentive suppliers to race to withdraw their funds immediately before a trigger is toggled, while borrowers try to max out their borrows immediately before their debt is forgiven. This is not an ideal property for triggers and consequently these types of triggers should be avoided.

Let's consider a Yearn V2 vault's pricePerShare as a trigger condition. Under normal operation, this value should only ever increase, though there are expected decreases immediately after each harvest. Therefore we may want to encode a trigger condition that says "if the value of pricePerShare drops and remains at some decreased value for X hours, toggle the trigger". This seems logical—if pricePerShare decreases, it may have been expected as part of a harvest, so let's wait to see if it recovers before toggling the trigger.

But this type of trigger is a prime example of a trigger that's susceptible to frontrunning. Let's say the trigger condition used a 12 hour duration. After 11 hours have elapsed, everyone knows that the trigger is likely to toggle very soon. As a result, suppliers will race to pull out their funds to remove the risk of losing funds, and borrowers will race to borrow as much as possible knowing they won't have to pay back debt. This "race" is not a desirable property for average users, so consider avoiding triggers that result in this type of scenario.

Data availability

Every borrow and redeem calls your trigger's checkAndToggleTrigger() method. This method does not take any inputs, and therefore your trigger's condition cannot be dependent on function inputs or call data. This also means your trigger will not be able to use a Merkle proof to access and validate historical state. If you need access to historical state, you'll need to save data to storage on each call to checkAndToggleTrigger().

Governance

While you can initialize the parameters for your protection market, governance can change the following properties and activities:

  • Interest rate model used.

  • Borrowing cap.

  • Mints, borrow, transfer, and liquidation operations.

Platform identifiers for protocols

Platform identifiers are used to label DeFi protocols in the Cozy applications. The protocol does not require you to use them in your trigger contracts.

The following table lists the numeric platform identifiers for currently supported protocols.

Platform identifier

Protocol name

0

Cozy

1

Yearn

2

Aave

3

Curve

4

Compound

5

Uniswap

6

Badger

7

Saddle

8

Alchemix

Cozy is actively adding support for new protocols and corresponding platform identifiers. If you don't see a platform identifier for a protocol you would like to add, contact Cozy support.