Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions python/coinbase-agentkit/changelog.d/946.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed Morpho withdraw action to dynamically fetch token decimals instead of hardcoding 18 decimals
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,16 @@ def deposit(self, wallet_provider: EvmWalletProvider, args: dict[str, Any]) -> s
description="""
This tool allows withdrawing assets from a Morpho Vault. It takes:
- vault_address: The address of the Morpho Vault to withdraw from
- assets: The amount of assets to withdraw in atomic units
- assets: The amount of assets to withdraw in whole units
Examples for WETH:
- 1 WETH
- 0.1 WETH
- 0.01 WETH
- receiver: The address to receive the shares
""",
- token_address: The address of the token to determine decimal precision
Important notes:
- Make sure to use the exact amount provided. Do not convert units for assets for this action.
- Please use a token address for the token_address field. If you are unsure of the token address, please clarify before continuing.""",
schema=MorphoWithdrawSchema,
)
def withdraw(self, wallet_provider: EvmWalletProvider, args: dict[str, Any]) -> str:
Expand All @@ -122,14 +129,21 @@ def withdraw(self, wallet_provider: EvmWalletProvider, args: dict[str, Any]) ->
if assets <= Decimal("0.0"):
return "Error: Assets amount must be greater than 0"

atomic_assets = Web3.to_wei(assets, "ether")
try:
decimals = wallet_provider.read_contract(
contract_address=args["token_address"],
abi=ERC20_ABI,
function_name="decimals",
args=[],
)

contract = Web3().eth.contract(address=args["vault_address"], abi=METAMORPHO_ABI)
encoded_data = contract.encode_abi(
"withdraw", args=[atomic_assets, args["receiver"], args["receiver"]]
)
atomic_assets = int(assets * (10**decimals))

contract = Web3().eth.contract(address=args["vault_address"], abi=METAMORPHO_ABI)
encoded_data = contract.encode_abi(
"withdraw", args=[atomic_assets, args["receiver"], args["receiver"]]
)

try:
params = {
"to": args["vault_address"],
"data": encoded_data,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ class MorphoWithdrawSchema(BaseModel):
"""Input schema for Morpho Vault withdraw action."""

vault_address: str = Field(..., description="The address of the Morpho Vault to withdraw from")
assets: str = Field(..., description="The amount of assets to withdraw in atomic units")
assets: str = Field(..., description="The amount of assets to withdraw in whole units")
receiver: str = Field(..., description="The address to receive the withdrawn assets")
token_address: str = Field(
..., description="The address of the assets token for decimal precision"
)
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,50 @@ def test_morpho_withdraw_success():
"""Test successful morpho withdraw with valid parameters."""
mock_wallet = MagicMock()
mock_wallet.send_transaction.return_value = MOCK_TX_HASH
mock_wallet.read_contract.return_value = MOCK_DECIMALS

with patch("web3.eth.Contract") as mock_contract:
mock_contract.return_value.encode_abi.return_value = b"encoded_data"
result = morpho_action_provider().withdraw(
mock_wallet,
{
"vault_address": MOCK_VAULT_ADDRESS,
"assets": "1.0",
"receiver": MOCK_RECEIVER,
"token_address": MOCK_TOKEN_ADDRESS,
},
)

result = morpho_action_provider().withdraw(
mock_wallet,
{"vault_address": MOCK_VAULT_ADDRESS, "assets": "1.0", "receiver": MOCK_RECEIVER},
)
assert MOCK_TX_HASH in result
assert "Withdrawn 1.0" in result
mock_wallet.send_transaction.assert_called_once()
mock_wallet.wait_for_transaction_receipt.assert_called_once_with(MOCK_TX_HASH)
mock_wallet.read_contract.assert_called_once()

assert MOCK_TX_HASH in result
assert "Withdrawn 1.0" in result
mock_wallet.send_transaction.assert_called_once()
mock_wallet.wait_for_transaction_receipt.assert_called_once_with(MOCK_TX_HASH)

def test_morpho_withdraw_non_18_decimal_token():
"""Test morpho withdraw with a non-18-decimal token (e.g. USDC with 6 decimals)."""
mock_wallet = MagicMock()
mock_wallet.send_transaction.return_value = MOCK_TX_HASH
mock_wallet.read_contract.return_value = 6 # USDC has 6 decimals

result = morpho_action_provider().withdraw(
mock_wallet,
{
"vault_address": MOCK_VAULT_ADDRESS,
"assets": "100",
"receiver": MOCK_RECEIVER,
"token_address": MOCK_TOKEN_ADDRESS,
},
)

assert MOCK_TX_HASH in result
assert "Withdrawn 100" in result

# Verify the read_contract was called to fetch decimals
mock_wallet.read_contract.assert_called_once()

# Verify send_transaction was called (the encoded data would contain the correct atomic amount)
mock_wallet.send_transaction.assert_called_once()
mock_wallet.wait_for_transaction_receipt.assert_called_once_with(MOCK_TX_HASH)


def test_morpho_withdraw_zero_amount():
Expand All @@ -145,7 +176,12 @@ def test_morpho_withdraw_zero_amount():

result = morpho_action_provider().withdraw(
mock_wallet,
{"vault_address": MOCK_VAULT_ADDRESS, "assets": "0.0", "receiver": MOCK_RECEIVER},
{
"vault_address": MOCK_VAULT_ADDRESS,
"assets": "0.0",
"receiver": MOCK_RECEIVER,
"token_address": MOCK_TOKEN_ADDRESS,
},
)

assert "Error: Assets amount must be greater than 0" in result
Expand All @@ -158,7 +194,12 @@ def test_morpho_withdraw_negative_amount():

result = morpho_action_provider().withdraw(
mock_wallet,
{"vault_address": MOCK_VAULT_ADDRESS, "assets": "-1.0", "receiver": MOCK_RECEIVER},
{
"vault_address": MOCK_VAULT_ADDRESS,
"assets": "-1.0",
"receiver": MOCK_RECEIVER,
"token_address": MOCK_TOKEN_ADDRESS,
},
)

assert "Error: Assets amount must be greater than 0" in result
Expand All @@ -175,6 +216,7 @@ def test_morpho_withdraw_invalid_amount():
"vault_address": MOCK_VAULT_ADDRESS,
"assets": "invalid_amount",
"receiver": MOCK_RECEIVER,
"token_address": MOCK_TOKEN_ADDRESS,
},
)

Expand All @@ -183,16 +225,19 @@ def test_morpho_withdraw_transaction_error():
"""Test morpho withdraw with transaction error."""
mock_wallet = MagicMock()
mock_wallet.send_transaction.side_effect = Exception("Transaction failed")
mock_wallet.read_contract.return_value = MOCK_DECIMALS

with patch("web3.eth.Contract") as mock_contract:
mock_contract.return_value.encode_abi.return_value = b"encoded_data"

result = morpho_action_provider().withdraw(
mock_wallet,
{"vault_address": MOCK_VAULT_ADDRESS, "assets": "1.0", "receiver": MOCK_RECEIVER},
)
result = morpho_action_provider().withdraw(
mock_wallet,
{
"vault_address": MOCK_VAULT_ADDRESS,
"assets": "1.0",
"receiver": MOCK_RECEIVER,
"token_address": MOCK_TOKEN_ADDRESS,
},
)

assert "Error withdrawing from Morpho Vault" in result
assert "Error withdrawing from Morpho Vault" in result


# Network Support Tests
Expand Down