Skip to content

Commit 7b7d60b

Browse files
committed
fix evm transfer token metadata fallback
1 parent ab04322 commit 7b7d60b

6 files changed

Lines changed: 187 additions & 9 deletions

File tree

packages/acala-evm-transfer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"dev": "subql codegen && subql build && docker-compose pull && docker-compose up --remove-orphans",
1111
"prepack": "rm -rf dist && npm run build",
1212
"test": "subql build && subql-node-ethereum test",
13+
"test:unit": "bun test tests/token.test.ts",
1314
"build:typechain": "typechain --target=ethers-v5 --out-dir=contracts abis/*.json",
1415
"build:develop": "NODE_ENV=develop subql codegen && NODE_ENV=develop subql build"
1516
},

packages/acala-evm-transfer/src/mappings/approve.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ export async function handleApprove(call: ApproveTransaction) {
1010
const receipt = await call.receipt();
1111
const success = receipt.status;
1212
const address = call.to;
13-
const token = await getToken(address);
13+
const token = await getToken(address, {
14+
handler: "handleApprove",
15+
blockHeight: call.blockNumber,
16+
transactionHash: call.hash,
17+
});
1418

1519
const approve = new Approve(
1620
call.hash,
Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,113 @@
11
import { Token } from "../types";
22
import { Erc20__factory } from "../types/contracts";
33

4-
export async function getToken (address: string) {
4+
export type TokenLookupContext = {
5+
handler?: string;
6+
blockHeight?: bigint | number | string;
7+
transactionHash?: string;
8+
logIndex?: bigint | number | string;
9+
};
10+
11+
type MetadataMethod = "name" | "symbol" | "decimals";
12+
13+
const FALLBACK_SYMBOL = "UNKNOWN";
14+
const FALLBACK_DECIMALS = BigInt(0);
15+
16+
export async function getToken (address: string, context: TokenLookupContext = {}) {
517
let token = await Token.get(address);
618

719
if (token) {
820
return token;
921
}
1022

1123
const erc20 = Erc20__factory.connect(address, api);
12-
const name = await erc20.name();
13-
const symbol = await erc20.symbol();
14-
const decimals = await erc20.decimals();
24+
const name = await readMetadata(address, "name", () => erc20.name(), context);
25+
const symbol = await readMetadata(address, "symbol", () => erc20.symbol(), context);
26+
const decimals = await readMetadata(address, "decimals", () => erc20.decimals(), context);
1527

16-
token = new Token(address, name, symbol, BigInt(decimals.toString()));
28+
token = new Token(
29+
address,
30+
name ?? fallbackName(address),
31+
symbol ?? FALLBACK_SYMBOL,
32+
decimals === undefined ? FALLBACK_DECIMALS : BigInt(decimals.toString())
33+
);
1734

1835
await token.save();
1936

2037
return token;
2138
}
39+
40+
async function readMetadata<T> (
41+
address: string,
42+
method: MetadataMethod,
43+
read: () => Promise<T>,
44+
context: TokenLookupContext
45+
): Promise<T | undefined> {
46+
try {
47+
return await read();
48+
} catch (error) {
49+
if (!isContractMetadataFailure(error)) {
50+
throw error;
51+
}
52+
53+
logger.warn(`Failed to read ERC20 metadata: address=${address} method=${method}${formatContext(context)} error=${formatError(error)}`);
54+
return undefined;
55+
}
56+
}
57+
58+
function fallbackName (address: string): string {
59+
return `Unknown Token ${address}`;
60+
}
61+
62+
function formatContext (context: TokenLookupContext): string {
63+
const parts: string[] = [];
64+
65+
if (context.handler) parts.push(`handler=${context.handler}`);
66+
if (context.blockHeight !== undefined) parts.push(`block=${context.blockHeight.toString()}`);
67+
if (context.transactionHash) parts.push(`tx=${context.transactionHash}`);
68+
if (context.logIndex !== undefined) parts.push(`logIndex=${context.logIndex.toString()}`);
69+
70+
return parts.length ? ` ${parts.join(" ")}` : "";
71+
}
72+
73+
function formatError (error: unknown): string {
74+
const code = errorCode(error);
75+
76+
if (error instanceof Error) {
77+
return `${error.name}: ${error.message}${code ? ` code=${code}` : ""}`;
78+
}
79+
80+
if (typeof error === "string") {
81+
return error;
82+
}
83+
84+
return code ? `code=${code}` : "unknown";
85+
}
86+
87+
function errorCode (error: unknown): string | undefined {
88+
if (typeof error !== "object" || error === null || !("code" in error)) {
89+
return undefined;
90+
}
91+
92+
const code = error.code;
93+
return typeof code === "string" ? code : undefined;
94+
}
95+
96+
function isContractMetadataFailure (error: unknown): boolean {
97+
const code = errorCode(error);
98+
99+
if (code === "CALL_EXCEPTION" || code === "BAD_DATA") {
100+
return true;
101+
}
102+
103+
if (!(error instanceof Error)) {
104+
return false;
105+
}
106+
107+
if (code !== undefined && code !== "INVALID_ARGUMENT") {
108+
return false;
109+
}
110+
111+
return error.message.includes("data out-of-bounds")
112+
|| error.message.includes("could not decode result data");
113+
}

packages/acala-evm-transfer/src/mappings/transfer-event.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ export async function handleTransferEvent(event: TransferLog) {
99
if (!event.args) return;
1010

1111
const address = event.address;
12-
const token = await getToken(address);
12+
const token = await getToken(address, {
13+
handler: "handleTransferEvent",
14+
blockHeight: event.block.number,
15+
transactionHash: event.transactionHash,
16+
logIndex: event.logIndex,
17+
});
1318

1419
const transferEvent = new TransferEvent(
1520
`${event.transactionHash}-${event.logIndex}`,
@@ -24,4 +29,3 @@ export async function handleTransferEvent(event: TransferLog) {
2429

2530
await transferEvent.save();
2631
}
27-

packages/acala-evm-transfer/src/mappings/transfer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ export async function handleTransfer(call: TransferTransaction) {
1212
const receipt = await call.receipt();
1313
const success = receipt.status;
1414
const address = call.to;
15-
let token = await getToken(address);
15+
const token = await getToken(address, {
16+
handler: "handleTransfer",
17+
blockHeight: call.blockNumber,
18+
transactionHash: call.hash,
19+
});
1620

1721
const transfer = new Transfer(
1822
call.hash,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { beforeEach, expect, mock, test } from "bun:test";
2+
3+
const records = new Map<string, unknown>();
4+
let warnings: string[] = [];
5+
let erc20Contract: {
6+
name: () => Promise<string>;
7+
symbol: () => Promise<string>;
8+
decimals: () => Promise<number>;
9+
};
10+
11+
mock.module("../src/types/contracts", () => ({
12+
Erc20__factory: {
13+
connect: () => erc20Contract,
14+
},
15+
}));
16+
17+
const { getToken } = await import("../src/mappings/token");
18+
19+
beforeEach(() => {
20+
records.clear();
21+
warnings = [];
22+
23+
Object.assign(globalThis, {
24+
api: {},
25+
logger: {
26+
warn: (message: string) => warnings.push(message),
27+
},
28+
store: {
29+
get: async (entity: string, id: string) => records.get(`${entity}:${id}`),
30+
set: async (entity: string, id: string, value: unknown) => {
31+
records.set(`${entity}:${id}`, value);
32+
},
33+
remove: async (entity: string, id: string) => {
34+
records.delete(`${entity}:${id}`);
35+
},
36+
},
37+
});
38+
});
39+
40+
test("persists fallback token metadata when decimals reverts", async () => {
41+
const address = "0x00000000000000000000000000000000006036414";
42+
const callException = new Error('call revert exception data="0x"');
43+
Object.assign(callException, { code: "CALL_EXCEPTION" });
44+
45+
erc20Contract = {
46+
name: async () => "Broken Metadata Token",
47+
symbol: async () => "BMT",
48+
decimals: async () => {
49+
throw callException;
50+
},
51+
};
52+
53+
const token = await getToken(address, {
54+
handler: "handleTransferEvent",
55+
blockHeight: 6036414,
56+
transactionHash: "0xdeadbeef",
57+
logIndex: 12,
58+
});
59+
60+
expect(token.id).toBe(address);
61+
expect(token.name).toBe("Broken Metadata Token");
62+
expect(token.symbol).toBe("BMT");
63+
expect(token.decimals).toBe(0n);
64+
expect(records.get(`Token:${address}`)).toBe(token);
65+
expect(warnings).toHaveLength(1);
66+
expect(warnings[0]).toContain(`address=${address}`);
67+
expect(warnings[0]).toContain("method=decimals");
68+
expect(warnings[0]).toContain("handler=handleTransferEvent");
69+
expect(warnings[0]).toContain("block=6036414");
70+
expect(warnings[0]).toContain("tx=0xdeadbeef");
71+
expect(warnings[0]).toContain("logIndex=12");
72+
expect(warnings[0]).toContain("code=CALL_EXCEPTION");
73+
});

0 commit comments

Comments
 (0)