Create an invest/divest contract

Learn how to create an invest/divest contract for an investment opportunity.

Create an invest/divest contract

Invest/divest smart contracts are used to programmatically borrow from Cozy protection markets and/or Cozy unprotected money markets and invest into DeFi products for investors in on-chain protocols, such as Yearn Finance.

At a high-level, the contract needs the following external functions to be implemented:

  1. invest: Automates borrowing from Cozy markets and investing the borrowed funds in a DeFi product.

  2. divest: Automates closing positions in a DeFi product that were opened with the contract's invest function and repays debt in the Cozy market.

  3. claimRewards: If the investment opportunity accrues reward tokens, this function is required to claim them and transfer them to the investor's wallet.

Additionally, invest/divest contracts are intended to be used by delegatecalling to them from a proxy wallet to borrow, invest, and divest on behalf of the user. This means these contracts cannot be used directly with EOAs, as EOAs cannot execute code and therefore the investments will occur on behalf of the invest contract, and not on behalf of the user.

At Cozy, we use DSProxy wallets alongside a DSProxyMulticall helper contract which allows batched execution of a sequence of contract calls, without the need for deploying a new "script" contract for each sequence (additional context can be found in DSProxyMulticall.sol in the Cozy Developer Guides). The address of the DSProxyMultiCall contract can be found in the Cozy Subgraph.

The contract examples shown throughout this document can be referenced from the CozyInvestCurve3CryptoEth.sol and CozyInvestConvex.sol contracts in the Cozy Developer Guides repository. CozyInvestCurve3CryptoEth.sol is an example of investing with ETH, and CozyInvestConvex.sol is an example of investing with ERC20 tokens.

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

Contracts used by an invest/divest contract

An invest/divest contract needs to know the addresses of a few contracts found on the Cozy Subgraph:

  • The address of the protection market contract to borrow from.

  • If the underlying asset of the protection market is Ether (ETH), the address of the Maxmillion contract is required. The Maximillion contract is used to repay ETH debt to the protection market.

    • Note: Cozy uses 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE as the address the represent ETH.

  • Optional: If you'd like to support unprotected investing, the address of the unprotected money market to borrow from.

In addition to the above, any contracts specific to investing and divesting from the target DeFi product are required (ie: a Curve liquidity pool, a Yearn Vault, etc.).

Interfaces required for Cozy contracts

The following snippet includes interfaces for interacting with Cozy contracts required for borrowing.

Develop the invest/divest contract constructor

The constructor for the invest/divest contract should be used to initialize any declared state variables required by the contract logic that were not initialized at the time they were declared. The state variables required may vary between different investment opportunities, but there are a few that should always be initialized:

  • The protection market contract address to borrow from.

  • If the underlying asset is ETH, the Maximillion contract address to repay borrows with.

The following snippet is an example of a constructor for an invest/divest contract for borrowing ETH from a market and investing into the Curve tricrypto pool.

Note: The contract above uses the constructor to initalize state variables for required contract addresses to allow re-use among different chains; this is not required.

Develop the invest logic

The invest function logic should automate borrowing from a valid protection market and investing the borrowed funds into a DeFi product.

The following snippet is an example of an invest function on an invest/divest contract for borrowing ETH and investing the borrowed ETH into the Curve tricrypto pool.

Let's decompose the CozyInvestCurve3CryptoEth.sol snippet above:

  1. An error check to ensure that a valid market is requested to be borrowed from. In this case, unprotected borrowing is also supported (optional).

  2. Borrow from the market, wrapped within an error check to ensure the borrow was successful. Note: The borrow will fail if there is insufficient collateral supplied to the market by the sender for the desired borrow amount.

  3. Logic specific to Curve for adding ETH liquidity to a pool and staking Curve LP tokens.

Develop the divest logic

The divest function logic should automate closing positions in a DeFi product that were entered with the contract's invest function, repaying borrowed funds to Cozy markets, and transfering any earnings to the investor's wallet.

Logic for divesting ETH

The following snippet is an example of a divest function on an invest/divest contract for borrowing ETH and investing the borrowed ETH into the Curve tricrypto pool.

Let's decompose the CozyInvestCurve3CryptoEth.sol snippet above:

  1. An error check to ensure that a valid market is requested to repay debt to. In this case, unprotected borrowing is also supported (optional).

  2. Logic specific to Curve for closing the investment position; unstaking Curve LP tokens and withdrawing liquidity from Curve (in this case, ETH).

  3. Cozy market debt repayment. Any remaining funds after repayment are transferred to the user. In this case, the underlying asset is ETH, so the Maximillion contract is used which includes error code handling.

  4. Curve reward tokens are claimed and transferred to the user. This is only required for investment opportunities that accrue rewards.

Logic for divesting ERC20 tokens

There are a few notable differences for divesting ERC20 tokens, as shown in the following example of a divest function on an invest/divest contract for borrowing WBTC and depositing the borrowed WBTC into the Curve TBTC pool for receipt tokens, then staking the receipt tokens in the Convex TBTC pool.

Let's decompose the CozyInvestConvex.sol snippet above:

  1. An _excessTokens function parameter is supported, which allows the caller to transfer additional quantities of the underlying asset to repay borrows if the investment withdrawal does not fully cover the debt owed to the Cozy market.

  2. An error check to ensure that a valid market is requested to repay debt to. In this case, unprotected borrowing is also supported (optional).

  3. Logic specific to Convex and Curve for closing the investment position; unstaking Curve LP tokens in Convex and withdrawing liquidity from Curve (in this case, WBTC).

  4. Cozy market debt repayment. Any remaining funds after repayment are transferred to the user. For ERC20 tokens, make sure to include error code checks for repaying borrows. In this case, we use a custom defined executeMaxRepay function which includes the error check and supports debt repayment with _excessTokens.

  5. Any remaining underlying tokens (in this case, WBTC) are transferred to the user after debt repayment.

  6. Convex reward tokens are claimed and transferred to the user. This is only required for investment opportunities that accrue rewards.

Develop the reward claim logic

If the investment opportunity accrues rewards, this function is required to claim them and transfer them to the investor's wallet. This function should also be used in the divest function.

The following snippet is an example of a claimRewards function on an invest/divest contract for claiming staking rewards from Curve.

Testing

When writing tests for invest/divest contracts, some setup is required in your test suite to replicate production behaviour. The following steps should be implemented in your test suite setup fixture:

  1. Deploy the new invest/divest contract.

  2. Deploy a proxy wallet. Cozy uses a proxy wallet for all borrows and investment opportunities.

  3. Supply funds to the protection market used in the test suite.

  4. Give the user's proxy wallet collateral so they can successfully borrow from Cozy markets.

Examples on how to implement the above in a development environment using JavaScript and Hardhat can be found in the CozyInvestCurve3CryptoEth.test.ts and CozyInvestConvex.test.ts files in the Cozy Developer Guides repository. The test suites in these examples also include examples of test cases.

Once there is sufficient test coverage, you are ready to deploy your new invest/divest contract.

Last updated

Was this helpful?