Skip to content

Commit

Permalink
feat: Added Compound Finance Python Actions
Browse files Browse the repository at this point in the history
Adds Compound Finance actions get portfolio details, supply, borrow, repay, and withdraw. Includes unit and integration (with CDP) tests for the actions. Utility functions are added to make getting borrow, supply, and health details easily accessible.
  • Loading branch information
mikeghen committed Feb 3, 2025
1 parent 312fb10 commit 80aadcb
Show file tree
Hide file tree
Showing 27 changed files with 2,673 additions and 50 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
run: poetry install --with dev

- name: Run tests
run: poetry run make test
run: poetry run make test -m "not integration"

test-cdp-langchain-python:
runs-on: ubuntu-latest
Expand Down
6 changes: 5 additions & 1 deletion python/cdp-agentkit-core/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ local-docs: docs

.PHONY: test
test:
poetry run pytest
poetry run pytest -m "not integration"

.PHONY: test-integration
test-integration:
poetry run pytest -m "integration"
15 changes: 14 additions & 1 deletion python/cdp-agentkit-core/cdp_agentkit_core/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from cdp_agentkit_core.actions.cdp_action import CdpAction # noqa: I001

from cdp_agentkit_core.actions.address_reputation import AddressReputationAction
from cdp_agentkit_core.actions.compound.borrow import CompoundBorrowAction
from cdp_agentkit_core.actions.compound.portfolio_details import CompoundPortfolioDetailsAction
from cdp_agentkit_core.actions.compound.repay import CompoundRepayAction
from cdp_agentkit_core.actions.compound.supply import CompoundSupplyAction
from cdp_agentkit_core.actions.compound.withdraw import CompoundWithdrawAction
from cdp_agentkit_core.actions.deploy_contract import DeployContractAction
from cdp_agentkit_core.actions.deploy_nft import DeployNftAction
from cdp_agentkit_core.actions.deploy_token import DeployTokenAction
Expand All @@ -20,10 +25,12 @@
from cdp_agentkit_core.actions.trade import TradeAction
from cdp_agentkit_core.actions.transfer import TransferAction
from cdp_agentkit_core.actions.transfer_nft import TransferNftAction
from cdp_agentkit_core.actions.weth.wrap_eth import WrapEthAction
from cdp_agentkit_core.actions.weth.unwrap_eth import UnwrapWethAction
from cdp_agentkit_core.actions.wow.buy_token import WowBuyTokenAction
from cdp_agentkit_core.actions.wow.create_token import WowCreateTokenAction
from cdp_agentkit_core.actions.wow.sell_token import WowSellTokenAction
from cdp_agentkit_core.actions.wrap_eth import WrapEthAction



# WARNING: All new CdpAction subclasses must be imported above, otherwise they will not be discovered
Expand All @@ -42,6 +49,11 @@ def get_all_cdp_actions() -> list[type[CdpAction]]:
"CDP_ACTIONS",
"CdpAction",
"AddressReputationAction",
"CompoundBorrowAction",
"CompoundPortfolioDetailsAction",
"CompoundRepayAction",
"CompoundSupplyAction",
"CompoundWithdrawAction",
"DeployNftAction",
"DeployTokenAction",
"DeployContractAction",
Expand All @@ -54,6 +66,7 @@ def get_all_cdp_actions() -> list[type[CdpAction]]:
"TradeAction",
"TransferAction",
"TransferNftAction",
"UnwrapWethAction",
"WowBuyTokenAction",
"WowCreateTokenAction",
"WowSellTokenAction",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Compound AgentKit Actions
These actions allow you to supply ETH or USDC to Compound V3 markets on Base.

## Actions
The actions in this package are intended to support agents that want to interact with Compound V3 markets on Base. It supports the following actions:

- `supply`: Supply ETH or USDC to Compound V3 markets on Base.
- `borrow`: Borrow ETH or USDC from Compound V3 markets on Base.
- `repay`: Repay ETH or USDC to Compound V3 markets on Base.
- `withdraw`: Withdraw ETH or USDC from Compound V3 markets on Base.
- `get_portfolio_details`: Get the portfolio details for the Compound V3 markets on Base.

## Supported Compound Markets (aka. Comets)

### Base
- USDC Comet
- Supply Assets: USDC, WETH, cbBTC, cbETH, wstETH
- Borrow Asset: USDC

### Base Sepolia
- USDC Comet
- Supply Assets: USDC, WETH
- Borrow Asset: USDC

## Limitations and Assumptions
- Only supports the default wallet (i.e., `wallet.default_address`).
- Only supports one Comet contract, the Base/Base Sepolia USDC Comet
- The only borrowable asset is USDC as a result of the above.
- Native ETH is not supported, supply must be done with Wrapped ETH (WETH).
- The `approve` transaction needed for `supply` and `repay` is included in the action.
- The amounts sent to these actions are _whole units_ of the asset (e.g., 0.01 ETH, 100 USDC).
- Token symbols are the `asset_id` (lowercase) rather than the symbol. There's currently no way to get the symbol from the cdp `Asset` model object.

## Funded by Compound Grants Program
Compound Actions for AgentKit is funded by the Compound Grants Program. Learn more about the Grant on Questbook [here](https://new.questbook.app/dashboard/?role=builder&chainId=10&proposalId=678c218180bdbe26619c3ae8&grantId=66f29bb58868f5130abc054d).

## Future Work
- [ ] Support for bulk actions that perform common operations like leverage, deleverage, swap collateral, etc.
- [ ] Add `symbol` to the `Asset` model in the Coinbase CDP SDK, a correctly cased version of the `asset_id`.
- [ ] Add Compound Base Sepolia cbETH Comet to support on Base Sepolia testnet CDP API.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from cdp_agentkit_core.actions.compound.borrow import CompoundBorrowAction
from cdp_agentkit_core.actions.compound.portfolio_details import CompoundPortfolioDetailsAction
from cdp_agentkit_core.actions.compound.repay import CompoundRepayAction
from cdp_agentkit_core.actions.compound.supply import CompoundSupplyAction
from cdp_agentkit_core.actions.compound.withdraw import CompoundWithdrawAction

__all__ = [
"CompoundBorrowAction",
"CompoundPortfolioDetailsAction",
"CompoundRepayAction",
"CompoundSupplyAction",
"CompoundWithdrawAction"
]
102 changes: 102 additions & 0 deletions python/cdp-agentkit-core/cdp_agentkit_core/actions/compound/borrow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from collections.abc import Callable
from decimal import Decimal
from typing import Literal

from cdp import Asset, Wallet
from pydantic import BaseModel, Field

from cdp_agentkit_core.actions import CdpAction
from cdp_agentkit_core.actions.compound.constants import (
CUSDCV3_ABI,
CUSDCV3_MAINNET_ADDRESS,
CUSDCV3_TESTNET_ADDRESS,
)
from cdp_agentkit_core.actions.compound.utils import get_health_ratio_after_borrow

# Constants
COMPOUND_BORROW_PROMPT = """
This tool allows you to borrow WETH or USDC from Compound V3 markets on Base.
It takes the following inputs:
- asset_id: The asset to borrow, either `weth` or `usdc`
- amount: The amount of assets to borrow in whole units
Examples for WETH:
- 1 WETH
- 0.1 WETH
- 0.01 WETH
Important notes:
- Ensure you have sufficient collateral to borrow against
- For any borrowing, make sure to have some ETH for gas fees
- Be aware of your borrowing capacity and liquidation risks
"""


class CompoundBorrowInput(BaseModel):
"""Input argument schema for borrowing assets from a Compound market."""

asset_id: Literal["weth", "usdc"] = Field(
...,
description="The asset ID to borrow from the Compound market, either `weth` or `usdc`",
)
amount: str = Field(
...,
description="The amount of the asset to borrow from the Compound market, e.g. 0.125 weth; 19.99 usdc",
)


def compound_borrow(wallet: Wallet, asset_id: Literal["weth", "usdc"], amount: str) -> str:
"""Borrow assets from a Compound market.
Args:
wallet (Wallet): The wallet to receive the borrowed assets.
asset_id (Literal['weth', 'usdc']): The asset ID to borrow from the Compound market.
amount (str): The amount of the asset to borrow from the Compound market.
Returns:
str: A message containing the borrowing details.
"""
# Get the asset details
asset = Asset.fetch(wallet.network_id, asset_id)
adjusted_amount = str(int(asset.to_atomic_amount(Decimal(amount))))

# Determine which Compound market to use based on network
is_mainnet = wallet.network_id == "base-mainnet"
compound_address = CUSDCV3_MAINNET_ADDRESS if is_mainnet else CUSDCV3_TESTNET_ADDRESS

# Check if position would be healthy after borrow
projected_health_ratio = get_health_ratio_after_borrow(
wallet,
compound_address,
adjusted_amount
)

if projected_health_ratio < 1:
return f"Error: Borrowing {amount} {asset_id.upper()} would result in an unhealthy position. Health ratio would be {projected_health_ratio:.2f}"

try:
# Use withdraw method to borrow from Compound
borrow_result = wallet.invoke_contract(
contract_address=compound_address,
method="withdraw",
args={
"asset": asset.contract_address,
"amount": adjusted_amount
},
abi=CUSDCV3_ABI,
).wait()

return f"Borrowed {amount} {asset_id.upper()} from Compound V3.\nTransaction hash: {borrow_result.transaction_hash}\nTransaction link: {borrow_result.transaction_link}"

except Exception as e:
return f"Error borrowing {amount} {asset_id.upper()} from Compound: {e!s}"


class CompoundBorrowAction(CdpAction):
"""Compound borrow action."""

name: str = "compound_borrow"
description: str = COMPOUND_BORROW_PROMPT
args_schema: type[BaseModel] | None = CompoundBorrowInput
func: Callable[..., str] = compound_borrow
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
CUSDCV3_MAINNET_ADDRESS = "0xb125E6687d4313864e53df431d5425969c15Eb2F" # Base USDC
CUSDCV3_TESTNET_ADDRESS = "0x571621Ce60Cebb0c1D442B5afb38B1663C6Bf017" # Base Sepolia USDC

CUSDCV3_ABI = [
{
"inputs": [
{"internalType": "address", "name": "asset", "type": "address"},
{"internalType": "uint256", "name": "amount", "type": "uint256"}
],
"name": "supply",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "asset", "type": "address"},
{"internalType": "uint256", "name": "amount", "type": "uint256"}
],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "priceFeed", "type": "address"}
],
"name": "getPrice",
"outputs": [
{"internalType": "uint256", "name": "", "type": "uint256"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "account", "type": "address"}
],
"name": "borrowBalanceOf",
"outputs": [
{"internalType": "uint256", "name": "", "type": "uint256"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "numAssets",
"outputs": [
{"internalType": "uint8", "name": "", "type": "uint8"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "uint8", "name": "i", "type": "uint8"}
],
"name": "getAssetInfo",
"outputs": [
{
"components": [
{"internalType": "uint8", "name": "offset", "type": "uint8"},
{"internalType": "address", "name": "asset", "type": "address"},
{"internalType": "address", "name": "priceFeed", "type": "address"},
{"internalType": "uint64", "name": "scale", "type": "uint64"},
{"internalType": "uint64", "name": "borrowCollateralFactor", "type": "uint64"},
{"internalType": "uint64", "name": "liquidateCollateralFactor", "type": "uint64"},
{"internalType": "uint64", "name": "liquidationFactor", "type": "uint64"},
{"internalType": "uint128", "name": "supplyCap", "type": "uint128"}
],
"internalType": "struct CometCore.AssetInfo",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "baseToken",
"outputs": [
{"internalType": "address", "name": "", "type": "address"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "baseTokenPriceFeed",
"outputs": [
{"internalType": "address", "name": "", "type": "address"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "account", "type": "address"},
{"internalType": "address", "name": "asset", "type": "address"}
],
"name": "collateralBalanceOf",
"outputs": [
{"internalType": "uint128", "name": "balance", "type": "uint128"},
],
"stateMutability": "view",
"type": "function"
}
]
Loading

0 comments on commit 80aadcb

Please sign in to comment.