Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(TXL-399): Use contract abi to decode the amounts of token balance changes #4775

Merged
merged 8 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
6 changes: 3 additions & 3 deletions packages/transaction-controller/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ module.exports = merge(baseConfig, {
coverageThreshold: {
global: {
branches: 94.42,
functions: 97.46,
lines: 98.44,
statements: 98.46,
functions: 97.45,
lines: 98.37,
statements: 98.38,
},
},

Expand Down
217 changes: 135 additions & 82 deletions packages/transaction-controller/src/utils/simulation.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { LogDescription } from '@ethersproject/abi';
import { Interface } from '@ethersproject/abi';
import { type Hex } from '@metamask/utils';

import {
SimulationInvalidResponseError,
Expand All @@ -11,27 +12,45 @@ import {
SupportedToken,
type GetSimulationDataRequest,
} from './simulation';
import type { SimulationResponseLog } from './simulation-api';
import type {
SimulationResponseLog,
SimulationResponseTransaction,
} from './simulation-api';
import {
simulateTransactions,
type SimulationResponse,
} from './simulation-api';

jest.mock('./simulation-api');

const USER_ADDRESS_MOCK = '0x123';
const OTHER_ADDRESS_MOCK = '0x456';
const CONTRACT_ADDRESS_1_MOCK = '0x789';
const CONTRACT_ADDRESS_2_MOCK = '0xDEF';
const BALANCE_1_MOCK = '0x0';
const BALANCE_2_MOCK = '0x1';
const DIFFERENCE_MOCK = '0x1';
const VALUE_MOCK = '0x4';
const TOKEN_ID_MOCK = '0x5';
const OTHER_TOKEN_ID_MOCK = '0x6';
// Utility function to encode addresses and values to 32-byte ABI format
const encodeTo32ByteHex = (value: string | number): Hex => {
// Pad to 32 bytes (64 characters) and add '0x' prefix
return `0x${BigInt(value).toString(16).padStart(64, '0')}`;
};

// getSimulationData returns values in hex format with leading zeros trimmed.
const trimLeadingZeros = (hexString: Hex): Hex => {
const trimmed = hexString.replace(/^0x0+/u, '0x') as Hex;
return trimmed === '0x' ? '0x0' : trimmed;
};

const USER_ADDRESS_MOCK = '0x123' as Hex;
const OTHER_ADDRESS_MOCK = '0x456' as Hex;
const CONTRACT_ADDRESS_1_MOCK = '0x789' as Hex;
const CONTRACT_ADDRESS_2_MOCK = '0xDEF' as Hex;
const BALANCE_1_MOCK = '0x0' as Hex;
const BALANCE_2_MOCK = '0x1' as Hex;
const DIFFERENCE_MOCK = '0x1' as Hex;
const VALUE_MOCK = '0x4' as Hex;
const TOKEN_ID_MOCK = '0x5' as Hex;
const OTHER_TOKEN_ID_MOCK = '0x6' as Hex;
const ERROR_CODE_MOCK = 123;
const ERROR_MESSAGE_MOCK = 'Test Error';

// Regression test – leading zero in user address
const USER_ADDRESS_WITH_LEADING_ZEROS = '0x0123' as Hex;

const REQUEST_MOCK: GetSimulationDataRequest = {
chainId: '0x1',
from: USER_ADDRESS_MOCK,
Expand Down Expand Up @@ -87,10 +106,16 @@ const PARSED_WRAPPED_ERC20_DEPOSIT_EVENT_MOCK = {
args: [USER_ADDRESS_MOCK, { toHexString: () => VALUE_MOCK }],
} as unknown as LogDescription;

const defaultResponseTx: SimulationResponseTransaction = {
return: encodeTo32ByteHex('0x0'),
callTrace: { calls: [], logs: [] },
stateDiff: { pre: {}, post: {} },
};

const RESPONSE_NESTED_LOGS_MOCK: SimulationResponse = {
transactions: [
{
return: '0x1',
...defaultResponseTx,
callTrace: {
calls: [
{
Expand All @@ -105,10 +130,6 @@ const RESPONSE_NESTED_LOGS_MOCK: SimulationResponse = {
],
logs: [],
},
stateDiff: {
pre: {},
post: {},
},
},
],
};
Expand All @@ -129,22 +150,12 @@ function createLogMock(contractAddress: string) {
* @param logs - The logs.
* @returns Mock API response.
*/
function createEventResponseMock(logs: SimulationResponseLog[]) {
function createEventResponseMock(
logs: SimulationResponseLog[],
): SimulationResponse {
return {
transactions: [
{
return: '0x',
callTrace: {
calls: [],
logs,
},
stateDiff: {
pre: {},
post: {},
},
},
],
} as unknown as SimulationResponse;
transactions: [{ ...defaultResponseTx, callTrace: { calls: [], logs } }],
};
}

/**
Expand All @@ -160,20 +171,14 @@ function createNativeBalanceResponse(
return {
transactions: [
{
callTrace: {
calls: [],
logs: [],
},
...defaultResponseTx,
return: encodeTo32ByteHex(previousBalance),
stateDiff: {
pre: {
[USER_ADDRESS_MOCK]: {
balance: previousBalance,
},
[USER_ADDRESS_MOCK]: { balance: previousBalance },
},
post: {
[USER_ADDRESS_MOCK]: {
balance: newBalance,
},
[USER_ADDRESS_MOCK]: { balance: newBalance },
},
},
},
Expand All @@ -194,37 +199,13 @@ function createBalanceOfResponse(
return {
transactions: [
...previousBalances.map((previousBalance) => ({
return: previousBalance,
callTrace: {
calls: [],
logs: [],
},
stateDiff: {
pre: {},
post: {},
},
...defaultResponseTx,
return: encodeTo32ByteHex(previousBalance),
})),
{
return: '0xabc',
callTrace: {
calls: [],
logs: [],
},
stateDiff: {
pre: {},
post: {},
},
},
defaultResponseTx,
...newBalances.map((newBalance) => ({
return: newBalance,
callTrace: {
calls: [],
logs: [],
},
stateDiff: {
pre: {},
post: {},
},
...defaultResponseTx,
return: encodeTo32ByteHex(newBalance),
})),
],
} as unknown as SimulationResponse;
Expand Down Expand Up @@ -317,6 +298,7 @@ describe('Simulation Utils', () => {
it.each([
{
title: 'ERC-20 token',
from: USER_ADDRESS_MOCK,
parsedEvent: PARSED_ERC20_TRANSFER_EVENT_MOCK,
tokenType: SupportedToken.ERC20,
tokenStandard: SimulationTokenStandard.erc20,
Expand All @@ -326,15 +308,28 @@ describe('Simulation Utils', () => {
},
{
title: 'ERC-721 token',
from: USER_ADDRESS_MOCK,
parsedEvent: PARSED_ERC721_TRANSFER_EVENT_MOCK,
tokenType: SupportedToken.ERC721,
tokenStandard: SimulationTokenStandard.erc721,
tokenId: TOKEN_ID_MOCK,
previousBalances: [OTHER_ADDRESS_MOCK],
newBalances: [USER_ADDRESS_MOCK],
},
{
// Regression test – leading zero in user address
title: 'ERC-721 token – where user address has leadding zero',
from: USER_ADDRESS_WITH_LEADING_ZEROS,
parsedEvent: PARSED_ERC721_TRANSFER_EVENT_MOCK,
tokenType: SupportedToken.ERC721,
tokenStandard: SimulationTokenStandard.erc721,
tokenId: TOKEN_ID_MOCK,
previousBalances: [OTHER_ADDRESS_MOCK],
newBalances: [USER_ADDRESS_WITH_LEADING_ZEROS],
},
{
title: 'ERC-1155 token via single event',
from: USER_ADDRESS_MOCK,
parsedEvent: PARSED_ERC1155_TRANSFER_SINGLE_EVENT_MOCK,
tokenType: SupportedToken.ERC1155,
tokenStandard: SimulationTokenStandard.erc1155,
Expand All @@ -344,6 +339,7 @@ describe('Simulation Utils', () => {
},
{
title: 'ERC-1155 token via batch event',
from: USER_ADDRESS_MOCK,
parsedEvent: PARSED_ERC1155_TRANSFER_BATCH_EVENT_MOCK,
tokenType: SupportedToken.ERC1155,
tokenStandard: SimulationTokenStandard.erc1155,
Expand All @@ -353,6 +349,7 @@ describe('Simulation Utils', () => {
},
{
title: 'wrapped ERC-20 token',
from: USER_ADDRESS_MOCK,
parsedEvent: PARSED_WRAPPED_ERC20_DEPOSIT_EVENT_MOCK,
tokenType: SupportedToken.ERC20_WRAPPED,
tokenStandard: SimulationTokenStandard.erc20,
Expand All @@ -362,6 +359,7 @@ describe('Simulation Utils', () => {
},
{
title: 'legacy ERC-721 token',
from: USER_ADDRESS_MOCK,
parsedEvent: PARSED_ERC721_TRANSFER_EVENT_MOCK,
tokenType: SupportedToken.ERC721_LEGACY,
tokenStandard: SimulationTokenStandard.erc721,
Expand All @@ -372,6 +370,7 @@ describe('Simulation Utils', () => {
])(
'on $title',
async ({
from,
parsedEvent,
tokenStandard,
tokenType,
Expand All @@ -389,7 +388,10 @@ describe('Simulation Utils', () => {
createBalanceOfResponse(previousBalances, newBalances),
);

const simulationData = await getSimulationData(REQUEST_MOCK);
const simulationData = await getSimulationData({
chainId: '0x1',
from,
});

expect(simulationData).toStrictEqual({
nativeBalanceChange: undefined,
Expand All @@ -398,8 +400,8 @@ describe('Simulation Utils', () => {
standard: tokenStandard,
address: CONTRACT_ADDRESS_1_MOCK,
id: tokenId,
previousBalance: BALANCE_1_MOCK,
newBalance: BALANCE_2_MOCK,
previousBalance: trimLeadingZeros(BALANCE_1_MOCK),
newBalance: trimLeadingZeros(BALANCE_2_MOCK),
difference: DIFFERENCE_MOCK,
isDecrease: false,
},
Expand Down Expand Up @@ -501,8 +503,8 @@ describe('Simulation Utils', () => {
standard: SimulationTokenStandard.erc20,
address: CONTRACT_ADDRESS_1_MOCK,
id: undefined,
previousBalance: BALANCE_2_MOCK,
newBalance: BALANCE_1_MOCK,
previousBalance: trimLeadingZeros(BALANCE_2_MOCK),
newBalance: trimLeadingZeros(BALANCE_1_MOCK),
difference: DIFFERENCE_MOCK,
isDecrease: true,
},
Expand Down Expand Up @@ -545,17 +547,17 @@ describe('Simulation Utils', () => {
standard: SimulationTokenStandard.erc721,
address: CONTRACT_ADDRESS_1_MOCK,
id: TOKEN_ID_MOCK,
previousBalance: BALANCE_1_MOCK,
newBalance: BALANCE_2_MOCK,
previousBalance: trimLeadingZeros(BALANCE_1_MOCK),
newBalance: trimLeadingZeros(BALANCE_2_MOCK),
difference: DIFFERENCE_MOCK,
isDecrease: false,
},
{
standard: SimulationTokenStandard.erc721,
address: CONTRACT_ADDRESS_1_MOCK,
id: OTHER_TOKEN_ID_MOCK,
previousBalance: BALANCE_1_MOCK,
newBalance: BALANCE_2_MOCK,
previousBalance: trimLeadingZeros(BALANCE_1_MOCK),
newBalance: trimLeadingZeros(BALANCE_2_MOCK),
difference: DIFFERENCE_MOCK,
isDecrease: false,
},
Expand Down Expand Up @@ -738,9 +740,60 @@ describe('Simulation Utils', () => {
standard: SimulationTokenStandard.erc20,
address: CONTRACT_ADDRESS_1_MOCK,
id: undefined,
previousBalance: BALANCE_1_MOCK,
newBalance: BALANCE_2_MOCK,
difference: DIFFERENCE_MOCK,
previousBalance: trimLeadingZeros(BALANCE_1_MOCK),
newBalance: trimLeadingZeros(BALANCE_2_MOCK),
difference: '0x1',
isDecrease: false,
},
],
});
});

// Ensures no regression of bug https://github.com/MetaMask/metamask-extension/issues/26521
it('decodes raw balanceOf output correctly for ERC20 token with extra zeros', async () => {
const DECODED_BALANCE_BEFORE = '0x134c31d252';
const DECODED_BALANCE_AFTER = '0x134c31d257';
const EXPECTED_BALANCE_CHANGE = '0x5';

// Contract returns 64 extra zeros in raw output of balanceOf.
// Abi decoding should ignore them.
const encodeOutputWith64ExtraZeros = (value: string) =>
(encodeTo32ByteHex(value) + ''.padStart(64, '0')) as Hex;
const RAW_BALANCE_BEFORE = encodeOutputWith64ExtraZeros(
DECODED_BALANCE_BEFORE,
);
const RAW_BALANCE_AFTER = encodeOutputWith64ExtraZeros(
DECODED_BALANCE_AFTER,
);

mockParseLog({
erc20: PARSED_ERC20_TRANSFER_EVENT_MOCK,
});

simulateTransactionsMock
.mockResolvedValueOnce(
createEventResponseMock([createLogMock(CONTRACT_ADDRESS_2_MOCK)]),
)
.mockResolvedValueOnce({
transactions: [
{ ...defaultResponseTx, return: RAW_BALANCE_BEFORE },
defaultResponseTx,
{ ...defaultResponseTx, return: RAW_BALANCE_AFTER },
],
});

const simulationData = await getSimulationData(REQUEST_MOCK);

expect(simulationData).toStrictEqual({
nativeBalanceChange: undefined,
tokenBalanceChanges: [
{
standard: SimulationTokenStandard.erc20,
address: CONTRACT_ADDRESS_2_MOCK,
id: undefined,
previousBalance: DECODED_BALANCE_BEFORE,
newBalance: DECODED_BALANCE_AFTER,
difference: EXPECTED_BALANCE_CHANGE,
isDecrease: false,
},
],
Expand Down
Loading
Loading