Create a protection market
Learn how to create a trigger contract for a protection market.
Last updated
Learn how to create a trigger contract for a protection market.
Last updated
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:
Prepare a trigger contract so that it's compatible with the Cozy protocol.
Develop the trigger contract logic to detect potential problems for one or more on-chain protocols.
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.
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.
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.
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:
Create a new empty file. For example, create a new file called TestTrigger.sol
by running the following command:
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:
Import the abstract trigger contract and set the current trigger contract to inherit it. For example:
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.
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:
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.
Add a placeholder for checkTriggerCondition()
function. For example:
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:
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.
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.
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.
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.
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.
You can now deploy your protection market in one of two ways:
By using the default interest rate model.
By specifying an interest rate model.
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:
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.
Trigger contract logic can sometimes have unintended consequences or undesirable effects. This section provides some tips and suggestions to help you avoid the pitfalls.
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.
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.
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()
.
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 are used to label DeFi protocols in Cozy applications. The protocol does not require you to use them in your trigger contracts, but they will help your protection market get proper surfacing.
The list of platform identifiers is maintained in the Cozy-Finance/platform-ids repo. Please consult the list to get the proper ID for the trigger you're developing.
Cozy is actively adding support for new protocols and corresponding platform identifiers. If the platform you're writing a trigger for doesn't have a platform ID assigned in the repo above, please submit a PR to add it!