From 46c0dd61c26066f56fbe5b73910cc2bdf87c08bd Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Sun, 9 Aug 2026 12:56:25 +0700 Subject: [PATCH] fix(wow): fail closed when graduated Uniswap quote is missing Quoter errors returned amountOut=0, which became minOut/minEth=0 on buy/sell for graduated tokens. Refuse the trade instead of submitting unprotected slippage. --- .../action_providers/wow/utils.py | 34 +++++++------ .../src/action-providers/wow/utils.ts | 50 +++++++++++-------- .../wow/wowActionProvider.test.ts | 13 +++++ 3 files changed, 63 insertions(+), 34 deletions(-) diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/wow/utils.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/wow/utils.py index 2a4daeb79..4d21b20ef 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/action_providers/wow/utils.py +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/wow/utils.py @@ -63,18 +63,21 @@ def get_buy_quote( amount_eth_in_wei_int = int(amount_eth_in_wei) has_graduated = get_has_graduated(wallet_provider, token_address) - token_quote = ( - has_graduated - and ( - get_uniswap_quote(wallet_provider, token_address, amount_eth_in_wei_int, "buy") - ).amount_out - ) or wallet_provider.read_contract( + if has_graduated: + quote = get_uniswap_quote( + wallet_provider, token_address, amount_eth_in_wei_int, "buy" + ) + # Quoter failures used to yield amount_out=0 → min_tokens=0 on buy. + if quote.error or not quote.amount_out or quote.amount_out <= 0: + raise ValueError(quote.error or "Failed fetching buy quote") + return int(quote.amount_out) + + return wallet_provider.read_contract( contract_address=token_address, abi=WOW_ABI, function_name="getEthBuyQuote", args=[amount_eth_in_wei_int], ) - return token_quote def get_sell_quote( @@ -94,15 +97,18 @@ def get_sell_quote( amount_tokens_in_wei_int = int(amount_tokens_in_wei) has_graduated = get_has_graduated(wallet_provider, token_address) - token_quote = ( - has_graduated - and ( - get_uniswap_quote(wallet_provider, token_address, amount_tokens_in_wei_int, "sell") - ).amount_out - ) or wallet_provider.read_contract( + if has_graduated: + quote = get_uniswap_quote( + wallet_provider, token_address, amount_tokens_in_wei_int, "sell" + ) + # Quoter failures used to yield amount_out=0 → min_eth=0 on sell. + if quote.error or not quote.amount_out or quote.amount_out <= 0: + raise ValueError(quote.error or "Failed fetching sell quote") + return int(quote.amount_out) + + return wallet_provider.read_contract( contract_address=token_address, abi=WOW_ABI, function_name="getTokenSellQuote", args=[amount_tokens_in_wei_int], ) - return token_quote diff --git a/typescript/agentkit/src/action-providers/wow/utils.ts b/typescript/agentkit/src/action-providers/wow/utils.ts index 811d25731..f17ccea37 100644 --- a/typescript/agentkit/src/action-providers/wow/utils.ts +++ b/typescript/agentkit/src/action-providers/wow/utils.ts @@ -38,16 +38,21 @@ export async function getBuyQuote( ): Promise { const hasGraduated = await getHasGraduated(wallet, tokenAddress); - const tokenQuote = ( - hasGraduated - ? (await getUniswapQuote(wallet, tokenAddress, Number(amountEthInWei), "buy")).amountOut - : await wallet.readContract({ - address: tokenAddress as `0x${string}`, - abi: WOW_ABI, - functionName: "getEthBuyQuote", - args: [amountEthInWei], - }) - ) as string | number; + if (hasGraduated) { + const quote = await getUniswapQuote(wallet, tokenAddress, Number(amountEthInWei), "buy"); + // Quoter failures used to return amountOut=0, which became minOut=0 on buy(). + if (quote.error || !quote.amountOut || quote.amountOut <= 0) { + throw new Error(quote.error || "Failed fetching buy quote"); + } + return quote.amountOut.toString(); + } + + const tokenQuote = (await wallet.readContract({ + address: tokenAddress as `0x${string}`, + abi: WOW_ABI, + functionName: "getEthBuyQuote", + args: [amountEthInWei], + })) as string | number | bigint; return tokenQuote.toString(); } @@ -67,16 +72,21 @@ export async function getSellQuote( ): Promise { const hasGraduated = await getHasGraduated(wallet, tokenAddress); - const tokenQuote = ( - hasGraduated - ? (await getUniswapQuote(wallet, tokenAddress, Number(amountTokensInWei), "sell")).amountOut - : await wallet.readContract({ - address: tokenAddress as `0x${string}`, - abi: WOW_ABI, - functionName: "getTokenSellQuote", - args: [amountTokensInWei], - }) - ) as string | number; + if (hasGraduated) { + const quote = await getUniswapQuote(wallet, tokenAddress, Number(amountTokensInWei), "sell"); + // Quoter failures used to return amountOut=0, which became minEth=0 on sell(). + if (quote.error || !quote.amountOut || quote.amountOut <= 0) { + throw new Error(quote.error || "Failed fetching sell quote"); + } + return quote.amountOut.toString(); + } + + const tokenQuote = (await wallet.readContract({ + address: tokenAddress as `0x${string}`, + abi: WOW_ABI, + functionName: "getTokenSellQuote", + args: [amountTokensInWei], + })) as string | number | bigint; return tokenQuote.toString(); } diff --git a/typescript/agentkit/src/action-providers/wow/wowActionProvider.test.ts b/typescript/agentkit/src/action-providers/wow/wowActionProvider.test.ts index 8536c3a1d..46be86b2c 100644 --- a/typescript/agentkit/src/action-providers/wow/wowActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/wow/wowActionProvider.test.ts @@ -217,6 +217,19 @@ describe("WowActionProvider", () => { const response = await provider.buyToken(mockWallet, args); expect(response).toBe(`Error buying Zora Wow ERC20 memecoin: ${error}`); }); + + it("should refuse buy when quote fails closed (no minOut=0 trade)", async () => { + const args = { + contractAddress: MOCK_CONTRACT_ADDRESS, + amountEthInWei: MOCK_AMOUNT_ETH_IN_WEI.toString(), + }; + + (getBuyQuote as jest.Mock).mockRejectedValue(new Error("Failed fetching buy quote")); + + const response = await provider.buyToken(mockWallet, args); + expect(response).toContain("Failed fetching buy quote"); + expect(mockWallet.sendTransaction).not.toHaveBeenCalled(); + }); }); describe("createToken", () => {