diff --git a/python/coinbase-agentkit/changelog.d/946.bugfix.md b/python/coinbase-agentkit/changelog.d/946.bugfix.md new file mode 100644 index 000000000..b72b9a758 --- /dev/null +++ b/python/coinbase-agentkit/changelog.d/946.bugfix.md @@ -0,0 +1 @@ +Fixed Morpho withdraw action to dynamically fetch token decimals instead of hardcoding 18 decimals diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/morpho/morpho_action_provider.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/morpho/morpho_action_provider.py index 4188c80cb..3fdd99f59 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/action_providers/morpho/morpho_action_provider.py +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/morpho/morpho_action_provider.py @@ -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: @@ -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, diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/morpho/schemas.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/morpho/schemas.py index b143e1e9e..1274711ac 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/action_providers/morpho/schemas.py +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/morpho/schemas.py @@ -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" + ) diff --git a/python/coinbase-agentkit/tests/action_providers/morpho/test_morpho_action_provider.py b/python/coinbase-agentkit/tests/action_providers/morpho/test_morpho_action_provider.py index c9645fba1..c3398b614 100644 --- a/python/coinbase-agentkit/tests/action_providers/morpho/test_morpho_action_provider.py +++ b/python/coinbase-agentkit/tests/action_providers/morpho/test_morpho_action_provider.py @@ -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(): @@ -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 @@ -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 @@ -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, }, ) @@ -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