Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/agentkit": patch
---

Fixed three EVM address comparisons that were case-sensitive: the erc20 transfer guardrail that refuses to send tokens to the token's own contract, the sushi router's native-asset check, and the yelay vault lookup. Each compared addresses as raw strings, so the same address written in a different case did not match. They now compare lowercased, as the rest of the codebase and the Python SDK already do.
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,25 @@ describe("Transfer Action", () => {
expect(response).toContain(`Transaction hash for the transfer: ${TRANSACTION_HASH}`);
});

it("should refuse a transfer whose destination is the token contract in a different case", async () => {
mockMulticall.mockResolvedValueOnce([
{ result: "MockToken" }, // name
{ result: MOCK_DECIMALS }, // decimals
{ result: BigInt(100000 * 10 ** MOCK_DECIMALS) }, // balance
]);

const args = {
amount: MOCK_AMOUNT.toString(),
tokenAddress: "0xABCDEF1234567890123456789012345678901234",
destinationAddress: "0xabcdef1234567890123456789012345678901234",
};

const response = await actionProvider.transfer(mockWallet, args);

expect(mockWallet.sendTransaction).not.toHaveBeenCalled();
expect(response).toContain("Transfer destination is the token contract address");
});

it("should fail with an error", async () => {
mockMulticall.mockRejectedValue(new Error("Failed to get token details"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Important notes:
}

// Guardrails to prevent loss of funds
if (args.tokenAddress === args.destinationAddress) {
if (args.tokenAddress.toLowerCase() === args.destinationAddress.toLowerCase()) {
return "Error: Transfer destination is the token contract address. Refusing transfer to prevent loss of funds.";
}
if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,31 @@ describe("Sushi Action Provider", () => {
expect(result).toContain(`on ${getEvmChainById(chainId).shortName}`);
});

it("should treat a mixed-case native address as native", async () => {
const args: Parameters<(typeof actionProvider)["swap"]>[1] = {
amount: formatUnits(amountIn, tokenIn.decimals),
fromAssetAddress: nativeAddress.toUpperCase().replace("0X", "0x") as Address,
toAssetAddress: tokenOut.address,
maxSlippage: 0.005,
};

mockWallet.getBalance.mockResolvedValue(amountIn);
mockWallet.sendTransaction.mockResolvedValue(txHash);
mockWallet.waitForTransactionReceipt.mockResolvedValueOnce({
status: "success",
logs: getRouteLog({ tokenIn: nativeToken, tokenOut, amountIn, amountOut }),
});
mockedGetSwap.mockReturnValue(
getSuccessfullSwapResponse({ tokenIn: nativeToken, amountIn, tokenOut, amountOut }),
);

const result = await actionProvider.swap(mockWallet, args);

// No decimals lookup: the native asset has no ERC20 contract to read it from
expect(mockWallet.readContract).toHaveBeenCalledTimes(0);
expect(result).toContain("Swapped");
});

it("should fail if there isn't enough balance (native)", async () => {
const args: Parameters<(typeof actionProvider)["swap"]>[1] = {
amount: formatUnits(amountIn, tokenIn.decimals),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ async function fetchDecimals({
};
}

if (token === nativeAddress) {
if (token.toLowerCase() === nativeAddress) {
return { success: true, decimals: EvmNative.fromChainId(chainId).decimals };
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,23 @@ APY: 5.2%
const response = await provider.deposit(mockWallet, args);
expect(response).toContain("Deposit failed");
});

it("should find the vault when its address case differs from the API response", async () => {
const checksummedVault = "0xAbCdEf1234567890123456789012345678901234";
mockedFetch.mockResolvedValueOnce(
mockFetchResult(200, [{ ...mockVaults[0], address: checksummedVault }]),
);

const args = {
assets: MOCK_WHOLE_ASSETS,
vaultAddress: checksummedVault.toLowerCase(),
};

const response = await provider.deposit(mockWallet, args);

expect(response).not.toContain("Vault not found");
expect(response).toContain(`Deposited ${MOCK_WHOLE_ASSETS}`);
});
});

describe("redeem action", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ APY: ${vault.apy}%
const chainId = wallet.getNetwork().chainId! as ChainId;
const vaultsResponse = await fetch(`${YELAY_BACKEND_URL}/vaults?chainId=${chainId}`);
const vaults = (await vaultsResponse.json()) as VaultsDetailsResponse[];
const vault = vaults.find(vault => vault.address === args.vaultAddress);
const vault = vaults.find(
vault => vault.address.toLowerCase() === args.vaultAddress.toLowerCase(),
);

if (!vault) {
return "Error: Vault not found";
Expand Down Expand Up @@ -181,7 +183,9 @@ Important notes:
const chainId = wallet.getNetwork().chainId! as ChainId;
const vaultsResponse = await fetch(`${YELAY_BACKEND_URL}/vaults?chainId=${chainId}`);
const vaults = (await vaultsResponse.json()) as VaultsDetailsResponse[];
const vault = vaults.find(vault => vault.address === args.vaultAddress);
const vault = vaults.find(
vault => vault.address.toLowerCase() === args.vaultAddress.toLowerCase(),
);

if (!vault) {
return "Error: Vault not found";
Expand Down Expand Up @@ -287,7 +291,9 @@ It takes:
const chainId = wallet.getNetwork().chainId! as ChainId;
const vaultsResponse = await fetch(`${YELAY_BACKEND_URL}/vaults?chainId=${chainId}`);
const vaults = (await vaultsResponse.json()) as VaultsDetailsResponse[];
const vault = vaults.find(vault => vault.address === args.vaultAddress);
const vault = vaults.find(
vault => vault.address.toLowerCase() === args.vaultAddress.toLowerCase(),
);

if (!vault) {
return "Error: Vault not found";
Expand Down
Loading