From 3222656f39edbfe51c026f78a77e8a4f757b6aca Mon Sep 17 00:00:00 2001 From: Sreeraj S Date: Thu, 4 Apr 2024 12:38:55 +0530 Subject: [PATCH] feat(abstract-eth): support token flush directly from forwarderV4 contract Changes: If forwarder version 4 is passed to transaction builder, it will call flushToken method directly on forwarder contract TICKET: WP-1652 --- .../src/lib/transactionBuilder.ts | 4 +- modules/abstract-eth/src/lib/utils.ts | 60 +++++++++++++------ modules/abstract-eth/src/lib/walletUtil.ts | 2 + .../unit/transactionBuilder/flushTokens.ts | 35 ++++++++++- 4 files changed, 80 insertions(+), 21 deletions(-) diff --git a/modules/abstract-eth/src/lib/transactionBuilder.ts b/modules/abstract-eth/src/lib/transactionBuilder.ts index e79eb9f73e..f0db25f1f0 100644 --- a/modules/abstract-eth/src/lib/transactionBuilder.ts +++ b/modules/abstract-eth/src/lib/transactionBuilder.ts @@ -216,7 +216,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { break; case TransactionType.FlushTokens: this.setContract(transactionJson.to); - const { forwarderAddress, tokenAddress } = decodeFlushTokensData(transactionJson.data); + const { forwarderAddress, tokenAddress } = decodeFlushTokensData(transactionJson); this.forwarderAddress(forwarderAddress); this.tokenAddress(tokenAddress); break; @@ -720,7 +720,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { * @returns {TxData} The Ethereum transaction data */ private buildFlushTokensTransaction(): TxData { - return this.buildBase(flushTokensData(this._forwarderAddress, this._tokenAddress)); + return this.buildBase(flushTokensData(this._forwarderAddress, this._tokenAddress, this._forwarderVersion)); } /** diff --git a/modules/abstract-eth/src/lib/utils.ts b/modules/abstract-eth/src/lib/utils.ts index 8753b38415..c01a1fef39 100644 --- a/modules/abstract-eth/src/lib/utils.ts +++ b/modules/abstract-eth/src/lib/utils.ts @@ -69,6 +69,8 @@ import { defaultForwarderVersion, createV4ForwarderTypes, v4CreateForwarderMethodId, + flushTokensTypesv4, + flushForwarderTokensMethodIdV4, } from './walletUtil'; import { EthTransactionData } from './types'; @@ -178,10 +180,20 @@ export function sendMultiSigTokenData( * @param forwarderAddress The forwarder address to flush * @param tokenAddress The token address to flush from */ -export function flushTokensData(forwarderAddress: string, tokenAddress: string): string { - const params = [forwarderAddress, tokenAddress]; - const method = EthereumAbi.methodID('flushForwarderTokens', flushTokensTypes); - const args = EthereumAbi.rawEncode(flushTokensTypes, params); +export function flushTokensData(forwarderAddress: string, tokenAddress: string, forwarderVersion: number): string { + let params: string[]; + let method: Uint8Array; + let args: Uint8Array; + + if (forwarderVersion >= 4) { + params = [tokenAddress]; + method = EthereumAbi.methodID('flushTokens', flushTokensTypesv4); + args = EthereumAbi.rawEncode(flushTokensTypesv4, params); + } else { + params = [forwarderAddress, tokenAddress]; + method = EthereumAbi.methodID('flushForwarderTokens', flushTokensTypes); + args = EthereumAbi.rawEncode(flushTokensTypes, params); + } return addHexPrefix(Buffer.concat([method, args]).toString('hex')); } @@ -430,23 +442,34 @@ export function decodeNativeTransferData(data: string): NativeTransferData { /** * Decode the given ABI-encoded flush tokens data and return parsed fields * - * @param data The data to decode + * @param txJson The txJson with data to decode * @returns parsed transfer data */ -export function decodeFlushTokensData(data: string): FlushTokensData { - if (!data.startsWith(flushForwarderTokensMethodId)) { - throw new BuildTransactionError(`Invalid transfer bytecode: ${data}`); +export function decodeFlushTokensData(txJson: TxData): FlushTokensData { + if (txJson.data.startsWith(flushForwarderTokensMethodId)) { + const [forwarderAddress, tokenAddress] = getRawDecoded( + flushTokensTypes, + getBufferedByteCode(flushForwarderTokensMethodId, txJson.data) + ); + return { + forwarderAddress: addHexPrefix(forwarderAddress as string), + tokenAddress: addHexPrefix(tokenAddress as string), + }; + } else if (txJson.data.startsWith(flushForwarderTokensMethodIdV4)) { + const [tokenAddress] = getRawDecoded( + flushTokensTypesv4, + getBufferedByteCode(flushForwarderTokensMethodIdV4, txJson.data) + ); + if (!txJson.to) { + throw new BuildTransactionError(`Missing to address: ${txJson}`); + } + return { + forwarderAddress: txJson.to, + tokenAddress: addHexPrefix(tokenAddress as string), + }; + } else { + throw new BuildTransactionError(`Invalid transfer bytecode: ${txJson.data}`); } - - const [forwarderAddress, tokenAddress] = getRawDecoded( - flushTokensTypes, - getBufferedByteCode(flushForwarderTokensMethodId, data) - ); - - return { - forwarderAddress: addHexPrefix(forwarderAddress as string), - tokenAddress: addHexPrefix(tokenAddress as string), - }; } /** @@ -484,6 +507,7 @@ const transactionTypesMap = { [v4CreateForwarderMethodId]: TransactionType.AddressInitialization, [sendMultisigMethodId]: TransactionType.Send, [flushForwarderTokensMethodId]: TransactionType.FlushTokens, + [flushForwarderTokensMethodIdV4]: TransactionType.FlushTokens, [flushCoinsMethodId]: TransactionType.FlushCoins, [sendMultisigTokenMethodId]: TransactionType.Send, [LockMethodId]: TransactionType.StakingLock, diff --git a/modules/abstract-eth/src/lib/walletUtil.ts b/modules/abstract-eth/src/lib/walletUtil.ts index bf214dd986..6fd9322e3f 100644 --- a/modules/abstract-eth/src/lib/walletUtil.ts +++ b/modules/abstract-eth/src/lib/walletUtil.ts @@ -8,6 +8,7 @@ export const createForwarderMethodId = '0xa68a76cc'; export const walletInitializationFirstBytes = '0x60606040'; export const recoveryWalletInitializationFirstBytes = '0x60c06040'; export const flushForwarderTokensMethodId = '0x2da03409'; +export const flushForwarderTokensMethodIdV4 = '0x3ef13367'; export const flushCoinsMethodId = '0x6b9f96ea'; export const ERC721SafeTransferTypeMethodId = '0xb88d4fde'; @@ -19,6 +20,7 @@ export const defaultWalletVersion = 0; export const walletSimpleConstructor = ['address[]']; export const createV1WalletTypes = ['address[]', 'bytes32']; export const flushTokensTypes = ['address', 'address']; +export const flushTokensTypesv4 = ['address']; export const flushCoinsTypes = []; export const sendMultiSigTypes = ['address', 'uint', 'bytes', 'uint', 'uint', 'bytes']; diff --git a/modules/sdk-coin-eth/test/unit/transactionBuilder/flushTokens.ts b/modules/sdk-coin-eth/test/unit/transactionBuilder/flushTokens.ts index 9577bf9b45..3e7cba5b75 100644 --- a/modules/sdk-coin-eth/test/unit/transactionBuilder/flushTokens.ts +++ b/modules/sdk-coin-eth/test/unit/transactionBuilder/flushTokens.ts @@ -1,6 +1,13 @@ import should from 'should'; import { TransactionType } from '@bitgo/sdk-core'; -import { ETHTransactionType, Fee, flushForwarderTokensMethodId, KeyPair, Transaction } from '../../../src'; +import { + ETHTransactionType, + Fee, + flushForwarderTokensMethodId, + flushForwarderTokensMethodIdV4, + KeyPair, + Transaction, +} from '../../../src'; import { getBuilder } from '../getBuilder'; describe('Eth Transaction builder flush tokens', function () { @@ -15,6 +22,7 @@ describe('Eth Transaction builder flush tokens', function () { counter?: number; fee?: Fee; key?: KeyPair; + forwarderVersion?: number; } const buildTransaction = async function (details: FlushTokensDetails): Promise { @@ -45,6 +53,10 @@ describe('Eth Transaction builder flush tokens', function () { txBuilder.sign({ key: details.key.getKeys().prv }); } + if (details.forwarderVersion !== undefined) { + txBuilder.forwarderVersion(details.forwarderVersion); + } + return await txBuilder.build(); }; @@ -117,6 +129,27 @@ describe('Eth Transaction builder flush tokens', function () { txJson.data.should.startWith(flushForwarderTokensMethodId); }); + it('a wallet flush forwarder transaction with forwarder Version 4', async () => { + const tx = await buildTransaction({ + fee: { + fee: '10', + gasLimit: '1000', + }, + counter: 0, + forwarderAddress: '0x53b8e91bb3b8f618b5f01004ef108f134f219573', + tokenAddress: '0xbcf935d206ca32929e1b887a07ed240f0d8ccd22', + contractAddress: '0x8f977e912ef500548a0c3be6ddde9899f1199b81', + forwarderVersion: 4, + }); + + tx.type.should.equal(TransactionType.FlushTokens); + const txJson = tx.toJson(); + txJson.gasLimit.should.equal('1000'); + txJson._type.should.equals(ETHTransactionType.LEGACY); + should.equal(txJson.nonce, 0); + txJson.data.should.startWith(flushForwarderTokensMethodIdV4); + }); + it('an unsigned flush transaction from serialized', async () => { const tx = await buildTransaction({ fee: {