Skip to content

Commit 1979e1c

Browse files
authored
Merge pull request #6882 from BitGo/nft-flush
feat(abstract-eth): add flush token support for ERC721 and ERC1155
2 parents d2ae887 + 5d4fa35 commit 1979e1c

File tree

11 files changed

+977
-1
lines changed

11 files changed

+977
-1
lines changed

modules/abstract-eth/src/lib/transactionBuilder.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ import {
2727
classifyTransaction,
2828
decodeForwarderCreationData,
2929
decodeFlushTokensData,
30+
decodeFlushERC721TokensData,
31+
decodeFlushERC1155TokensData,
3032
decodeWalletCreationData,
3133
flushCoinsData,
3234
flushTokensData,
35+
flushERC721TokensData,
36+
flushERC1155TokensData,
3337
getAddressInitDataAllForwarderVersions,
3438
getCommon,
3539
getProxyInitcode,
@@ -69,6 +73,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
6973
// flush tokens parameters
7074
private _forwarderAddress: string;
7175
private _tokenAddress: string;
76+
private _tokenId: string;
7277

7378
// Send and AddressInitialization transaction specific parameters
7479
protected _transfer: TransferBuilder | ERC721TransferBuilder | ERC1155TransferBuilder;
@@ -144,6 +149,10 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
144149
return this.buildFlushTokensTransaction();
145150
case TransactionType.FlushCoins:
146151
return this.buildFlushCoinsTransaction();
152+
case TransactionType.FlushERC721:
153+
return this.buildFlushERC721Transaction();
154+
case TransactionType.FlushERC1155:
155+
return this.buildFlushERC1155Transaction();
147156
case TransactionType.SingleSigSend:
148157
return this.buildBase('0x');
149158
case TransactionType.ContractCall:
@@ -236,6 +245,26 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
236245
case TransactionType.FlushCoins:
237246
this.setContract(transactionJson.to);
238247
break;
248+
case TransactionType.FlushERC721:
249+
this.setContract(transactionJson.to);
250+
const erc721Data = decodeFlushERC721TokensData(transactionJson.data, transactionJson.to);
251+
if (erc721Data.forwarderVersion >= 4) {
252+
this.forwarderVersion(erc721Data.forwarderVersion);
253+
}
254+
this.forwarderAddress(erc721Data.forwarderAddress);
255+
this.tokenAddress(erc721Data.tokenAddress);
256+
this.tokenId(erc721Data.tokenId);
257+
break;
258+
case TransactionType.FlushERC1155:
259+
this.setContract(transactionJson.to);
260+
const erc1155Data = decodeFlushERC1155TokensData(transactionJson.data, transactionJson.to);
261+
if (erc1155Data.forwarderVersion >= 4) {
262+
this.forwarderVersion(erc1155Data.forwarderVersion);
263+
}
264+
this.forwarderAddress(erc1155Data.forwarderAddress);
265+
this.tokenAddress(erc1155Data.tokenAddress);
266+
this.tokenId(erc1155Data.tokenId);
267+
break;
239268
case TransactionType.Send:
240269
case TransactionType.SendERC1155:
241270
case TransactionType.SendERC721:
@@ -387,6 +416,21 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
387416
this.validateForwarderAddress();
388417
this.validateTokenAddress();
389418
break;
419+
case TransactionType.FlushERC721:
420+
case TransactionType.FlushERC1155:
421+
this.validateContractAddress();
422+
if (this._forwarderVersion < 4) {
423+
this.validateForwarderAddress();
424+
}
425+
this.validateTokenAddress();
426+
if (!this._tokenId) {
427+
throw new BuildTransactionError(
428+
this._type === TransactionType.FlushERC721
429+
? 'Token ID is required for ERC721 flush'
430+
: 'Token ID is required for ERC1155 flush'
431+
);
432+
}
433+
break;
390434
case TransactionType.SingleSigSend:
391435
// for single sig sends, the contract address is actually the recipient
392436
this.validateContractAddress();
@@ -740,6 +784,15 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
740784
this._tokenAddress = address;
741785
}
742786

787+
/**
788+
* Set the token ID for ERC721/ERC1155 token flush
789+
*
790+
* @param {string} tokenId the token ID to flush
791+
*/
792+
tokenId(tokenId: string): void {
793+
this._tokenId = tokenId;
794+
}
795+
743796
/**
744797
* Build a transaction to flush tokens from a forwarder.
745798
*
@@ -760,6 +813,46 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
760813
private buildFlushCoinsTransaction(): TxData {
761814
return this.buildBase(flushCoinsData());
762815
}
816+
817+
/**
818+
* Build a transaction to flush ERC721 NFTs from a forwarder.
819+
*
820+
* @returns {TxData} The Ethereum transaction data
821+
*/
822+
private buildFlushERC721Transaction(): TxData {
823+
if (!this._tokenAddress) {
824+
throw new BuildTransactionError('Token address is required for ERC721 flush');
825+
}
826+
if (!this._tokenId) {
827+
throw new BuildTransactionError('Token ID is required for ERC721 flush');
828+
}
829+
if (this._forwarderVersion >= 4 && this._contractAddress !== this._forwarderAddress) {
830+
throw new BuildTransactionError('Invalid contract address: ' + this._contractAddress);
831+
}
832+
return this.buildBase(
833+
flushERC721TokensData(this._forwarderAddress, this._tokenAddress, this._tokenId, this._forwarderVersion)
834+
);
835+
}
836+
837+
/**
838+
* Build a transaction to flush ERC1155 tokens from a forwarder.
839+
*
840+
* @returns {TxData} The Ethereum transaction data
841+
*/
842+
private buildFlushERC1155Transaction(): TxData {
843+
if (!this._tokenAddress) {
844+
throw new BuildTransactionError('Token address is required for ERC1155 flush');
845+
}
846+
if (!this._tokenId) {
847+
throw new BuildTransactionError('Token ID is required for ERC1155 flush');
848+
}
849+
if (this._forwarderVersion >= 4 && this._contractAddress !== this._forwarderAddress) {
850+
throw new BuildTransactionError('Invalid contract address: ' + this._contractAddress);
851+
}
852+
return this.buildBase(
853+
flushERC1155TokensData(this._forwarderAddress, this._tokenAddress, this._tokenId, this._forwarderVersion)
854+
);
855+
}
763856
// endregion
764857

765858
// region generic contract call

modules/abstract-eth/src/lib/utils.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ import {
5555
flushCoinsTypes,
5656
flushForwarderTokensMethodId,
5757
flushTokensTypes,
58+
flushERC721ForwarderTokensMethodId,
59+
flushERC721ForwarderTokensMethodIdV4,
60+
flushERC721TokensTypes,
61+
flushERC721TokensTypesv4,
62+
flushERC1155ForwarderTokensMethodId,
63+
flushERC1155ForwarderTokensMethodIdV4,
64+
flushERC1155TokensTypes,
65+
flushERC1155TokensTypesv4,
5866
sendMultisigMethodId,
5967
sendMultisigTokenMethodId,
6068
sendMultiSigTokenTypes,
@@ -209,6 +217,152 @@ export function flushCoinsData(): string {
209217
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
210218
}
211219

220+
/**
221+
* Get the data required to make a flush ERC721 tokens contract call
222+
* @param forwarderAddress - The forwarder address (for v0-v3)
223+
* @param tokenAddress - The ERC721 token contract address
224+
* @param tokenId - The token ID to flush
225+
* @param forwarderVersion - The forwarder version
226+
*/
227+
export function flushERC721TokensData(
228+
forwarderAddress: string,
229+
tokenAddress: string,
230+
tokenId: string,
231+
forwarderVersion: number
232+
): string {
233+
let params: (string | Buffer)[];
234+
let method: Uint8Array;
235+
let args: Uint8Array;
236+
237+
if (forwarderVersion >= 4) {
238+
params = [tokenAddress, tokenId];
239+
method = EthereumAbi.methodID('flushERC721Token', flushERC721TokensTypesv4);
240+
args = EthereumAbi.rawEncode(flushERC721TokensTypesv4, params);
241+
} else {
242+
params = [forwarderAddress, tokenAddress, tokenId];
243+
method = EthereumAbi.methodID('flushERC721ForwarderTokens', flushERC721TokensTypes);
244+
args = EthereumAbi.rawEncode(flushERC721TokensTypes, params);
245+
}
246+
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
247+
}
248+
249+
/**
250+
* Decode the given ABI-encoded flush ERC721 tokens data
251+
* @param data The data to decode
252+
* @param to The to address (contract address for v4+)
253+
* @returns parsed flush data with forwarderAddress, tokenAddress, tokenId and forwarderVersion
254+
*/
255+
export function decodeFlushERC721TokensData(
256+
data: string,
257+
to?: string
258+
): {
259+
forwarderAddress: string;
260+
tokenAddress: string;
261+
tokenId: string;
262+
forwarderVersion: number;
263+
} {
264+
if (data.startsWith(flushERC721ForwarderTokensMethodIdV4)) {
265+
if (!to) {
266+
throw new BuildTransactionError(`Missing to address: ${to}`);
267+
}
268+
const [tokenAddress, tokenId] = getRawDecoded(
269+
flushERC721TokensTypesv4,
270+
getBufferedByteCode(flushERC721ForwarderTokensMethodIdV4, data)
271+
);
272+
return {
273+
forwarderAddress: to,
274+
tokenAddress: addHexPrefix(tokenAddress as string),
275+
tokenId: new BigNumber(bufferToHex(tokenId as Buffer)).toFixed(),
276+
forwarderVersion: 4,
277+
};
278+
} else if (data.startsWith(flushERC721ForwarderTokensMethodId)) {
279+
const [forwarderAddress, tokenAddress, tokenId] = getRawDecoded(
280+
flushERC721TokensTypes,
281+
getBufferedByteCode(flushERC721ForwarderTokensMethodId, data)
282+
);
283+
return {
284+
forwarderAddress: addHexPrefix(forwarderAddress as string),
285+
tokenAddress: addHexPrefix(tokenAddress as string),
286+
tokenId: new BigNumber(bufferToHex(tokenId as Buffer)).toFixed(),
287+
forwarderVersion: 0,
288+
};
289+
}
290+
throw new BuildTransactionError(`Invalid flush ERC721 bytecode: ${data}`);
291+
}
292+
293+
/**
294+
* Get the data required to make a flush ERC1155 tokens contract call
295+
* @param forwarderAddress - The forwarder address (for v0-v3)
296+
* @param tokenAddress - The ERC1155 token contract address
297+
* @param tokenId - The token ID to flush
298+
* @param forwarderVersion - The forwarder version
299+
*/
300+
export function flushERC1155TokensData(
301+
forwarderAddress: string,
302+
tokenAddress: string,
303+
tokenId: string,
304+
forwarderVersion: number
305+
): string {
306+
let params: (string | Buffer)[];
307+
let method: Uint8Array;
308+
let args: Uint8Array;
309+
310+
if (forwarderVersion >= 4) {
311+
params = [tokenAddress, tokenId];
312+
method = EthereumAbi.methodID('flushERC1155Tokens', flushERC1155TokensTypesv4);
313+
args = EthereumAbi.rawEncode(flushERC1155TokensTypesv4, params);
314+
} else {
315+
params = [forwarderAddress, tokenAddress, tokenId];
316+
method = EthereumAbi.methodID('flushERC1155ForwarderTokens', flushERC1155TokensTypes);
317+
args = EthereumAbi.rawEncode(flushERC1155TokensTypes, params);
318+
}
319+
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
320+
}
321+
322+
/**
323+
* Decode the given ABI-encoded flush ERC1155 tokens data
324+
* @param data The data to decode
325+
* @param to The to address (contract address for v4+)
326+
* @returns parsed flush data with forwarderAddress, tokenAddress, tokenId and forwarderVersion
327+
*/
328+
export function decodeFlushERC1155TokensData(
329+
data: string,
330+
to?: string
331+
): {
332+
forwarderAddress: string;
333+
tokenAddress: string;
334+
tokenId: string;
335+
forwarderVersion: number;
336+
} {
337+
if (data.startsWith(flushERC1155ForwarderTokensMethodIdV4)) {
338+
if (!to) {
339+
throw new BuildTransactionError(`Missing to address: ${to}`);
340+
}
341+
const [tokenAddress, tokenId] = getRawDecoded(
342+
flushERC1155TokensTypesv4,
343+
getBufferedByteCode(flushERC1155ForwarderTokensMethodIdV4, data)
344+
);
345+
return {
346+
forwarderAddress: to,
347+
tokenAddress: addHexPrefix(tokenAddress as string),
348+
tokenId: new BigNumber(bufferToHex(tokenId as Buffer)).toFixed(),
349+
forwarderVersion: 4,
350+
};
351+
} else if (data.startsWith(flushERC1155ForwarderTokensMethodId)) {
352+
const [forwarderAddress, tokenAddress, tokenId] = getRawDecoded(
353+
flushERC1155TokensTypes,
354+
getBufferedByteCode(flushERC1155ForwarderTokensMethodId, data)
355+
);
356+
return {
357+
forwarderAddress: addHexPrefix(forwarderAddress as string),
358+
tokenAddress: addHexPrefix(tokenAddress as string),
359+
tokenId: new BigNumber(bufferToHex(tokenId as Buffer)).toFixed(),
360+
forwarderVersion: 0,
361+
};
362+
}
363+
throw new BuildTransactionError(`Invalid flush ERC1155 bytecode: ${data}`);
364+
}
365+
212366
/**
213367
* Returns the create forwarder method calling data
214368
*
@@ -542,6 +696,10 @@ const transactionTypesMap = {
542696
[flushForwarderTokensMethodId]: TransactionType.FlushTokens,
543697
[flushForwarderTokensMethodIdV4]: TransactionType.FlushTokens,
544698
[flushCoinsMethodId]: TransactionType.FlushCoins,
699+
[flushERC721ForwarderTokensMethodId]: TransactionType.FlushERC721,
700+
[flushERC721ForwarderTokensMethodIdV4]: TransactionType.FlushERC721,
701+
[flushERC1155ForwarderTokensMethodId]: TransactionType.FlushERC1155,
702+
[flushERC1155ForwarderTokensMethodIdV4]: TransactionType.FlushERC1155,
545703
[sendMultisigTokenMethodId]: TransactionType.Send,
546704
[LockMethodId]: TransactionType.StakingLock,
547705
[VoteMethodId]: TransactionType.StakingVote,

modules/abstract-eth/src/lib/walletUtil.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export const recoveryWalletInitializationFirstBytes = '0x60c06040';
1010
export const flushForwarderTokensMethodId = '0x2da03409';
1111
export const flushForwarderTokensMethodIdV4 = '0x3ef13367';
1212
export const flushCoinsMethodId = '0x6b9f96ea';
13+
export const flushERC721ForwarderTokensMethodId = '0x5a953d0a';
14+
export const flushERC721ForwarderTokensMethodIdV4 = '0x159e44d7';
15+
export const flushERC1155ForwarderTokensMethodId = '0xe6bd0aa4';
16+
export const flushERC1155ForwarderTokensMethodIdV4 = '0x8972c17c';
1317

1418
export const ERC721SafeTransferTypeMethodId = '0xb88d4fde';
1519
export const ERC1155SafeTransferTypeMethodId = '0xf242432a';
@@ -22,6 +26,10 @@ export const createV1WalletTypes = ['address[]', 'bytes32'];
2226
export const flushTokensTypes = ['address', 'address'];
2327
export const flushTokensTypesv4 = ['address'];
2428
export const flushCoinsTypes = [];
29+
export const flushERC721TokensTypes = ['address', 'address', 'uint256'];
30+
export const flushERC721TokensTypesv4 = ['address', 'uint256'];
31+
export const flushERC1155TokensTypes = ['address', 'address', 'uint256'];
32+
export const flushERC1155TokensTypesv4 = ['address', 'uint256'];
2533

2634
export const sendMultiSigTypes = ['address', 'uint', 'bytes', 'uint', 'uint', 'bytes'];
2735
export const sendMultiSigTypesFirstSigner = ['string', 'address', 'uint', 'bytes', 'uint', 'uint'];

0 commit comments

Comments
 (0)