Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions modules/abstract-eth/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,10 @@ export interface ForwarderInitializationData {
addressCreationSalt?: string;
feeAddress?: string;
}

export interface FlushERC7984ForwarderTokenData {
forwarderAddress: string;
tokenContractAddress: string;
encryptedHandle: string; // bytes32 hex
parentAddress: string;
}
114 changes: 113 additions & 1 deletion modules/abstract-eth/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from '@bitgo/sdk-core';

import { KeyPair } from './keyPair';
import { ETHTransactionType, Fee, SignatureParts, TxData } from './iface';
import { ETHTransactionType, Fee, FlushERC7984ForwarderTokenData, SignatureParts, TxData } from './iface';
import {
calculateForwarderAddress,
calculateForwarderV1Address,
Expand All @@ -29,6 +29,7 @@ import {
decodeFlushTokensData,
decodeFlushERC721TokensData,
decodeFlushERC1155TokensData,
decodeFlushERC7984ForwarderTokenData,
decodeWalletCreationData,
flushCoinsData,
flushTokensData,
Expand All @@ -42,6 +43,7 @@ import {
getV1WalletInitializationData,
getCreateForwarderParamsAndTypes,
} from './utils';
import { buildFlushERC7984ForwarderTokenCalldata } from './zamaUtils';
import { defaultWalletVersion, walletSimpleConstructor } from './walletUtil';
import { ERC1155TransferBuilder } from './transferBuilders/transferBuilderERC1155';
import { ERC721TransferBuilder } from './transferBuilders/transferBuilderERC721';
Expand Down Expand Up @@ -77,6 +79,11 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
private _tokenAddress: string;
private _tokenId: string;

// FlushERC7984ForwarderToken parameters
private _tokenContractAddress: string;
private _encryptedHandle: string; // bytes32 hex from confidentialBalanceOf
private _parentAddress: string; // where flushed tokens go (wallet base address)

// Send and AddressInitialization transaction specific parameters
protected _transfer: TransferBuilder | ERC721TransferBuilder | ERC1155TransferBuilder | TransferBuilderERC7984;
private _contractAddress: string;
Expand Down Expand Up @@ -161,6 +168,8 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
case TransactionType.ContractCall:
case TransactionType.DecryptionDelegation:
return this.buildGenericContractCallTransaction();
case TransactionType.FlushERC7984ForwarderToken:
return this.buildFlushERC7984ForwarderTokenTransaction();
default:
throw new BuildTransactionError('Unsupported transaction type');
}
Expand Down Expand Up @@ -303,6 +312,18 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
this.setContract(transactionJson.to);
this.data(transactionJson.data);
break;
case TransactionType.FlushERC7984ForwarderToken: {
this.setContract(transactionJson.to);
const erc7984Data: FlushERC7984ForwarderTokenData = decodeFlushERC7984ForwarderTokenData(
transactionJson.data,
transactionJson.to!
);
this.forwarderAddress(erc7984Data.forwarderAddress);
this.tokenContractAddress(erc7984Data.tokenContractAddress);
this.encryptedHandle(erc7984Data.encryptedHandle);
this.parentAddress(erc7984Data.parentAddress);
break;
}
default:
throw new BuildTransactionError('Unsupported transaction type');
// TODO: Add other cases of deserialization
Expand Down Expand Up @@ -454,6 +475,13 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
this.validateContractAddress();
this.validateDataField();
break;
case TransactionType.FlushERC7984ForwarderToken:
this.validateContractAddress();
this.validateForwarderAddress();
this.validateTokenContractAddress();
this.validateEncryptedHandle();
this.validateParentAddress();
break;
default:
throw new BuildTransactionError('Unsupported transaction type');
}
Expand Down Expand Up @@ -510,6 +538,33 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
}
}

/**
* Check if a token contract address for ERC-7984 flush was defined or throw.
*/
private validateTokenContractAddress(): void {
if (!this._tokenContractAddress) {
throw new BuildTransactionError('Invalid transaction: missing tokenContractAddress');
}
}

/**
* Check if an encrypted handle for ERC-7984 flush was defined or throw.
*/
private validateEncryptedHandle(): void {
if (!this._encryptedHandle) {
throw new BuildTransactionError('Invalid transaction: missing encryptedHandle');
}
}

/**
* Check if a parent address for ERC-7984 flush was defined or throw.
*/
private validateParentAddress(): void {
if (!this._parentAddress) {
throw new BuildTransactionError('Invalid transaction: missing parentAddress');
}
}

private setContract(address: string | undefined): void {
if (address === undefined) {
throw new BuildTransactionError('Undefined recipient address');
Expand Down Expand Up @@ -982,4 +1037,61 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
public getWalletVersion(): number {
return this._walletVersion;
}

// region FlushERC7984ForwarderToken builder methods

/**
* Set the ERC-7984 token contract address for forwarder flush transactions.
*
* @param {string} address The ERC-7984 token contract address
*/
tokenContractAddress(address: string): void {
if (!isValidEthAddress(address)) {
throw new BuildTransactionError('Invalid address: ' + address);
}
this._tokenContractAddress = address;
}

/**
* Set the encrypted balance handle (bytes32 hex) for forwarder flush transactions.
* This is the opaque handle returned by confidentialBalanceOf() on the ERC-7984 token contract.
*
* @param {string} handle The bytes32 hex encrypted balance handle
*/
encryptedHandle(handle: string): void {
this._encryptedHandle = handle;
}

/**
* Set the parent address (root wallet) for ERC-7984 forwarder flush transactions.
* The flushed tokens will be transferred to this address.
*
* @param {string} address The parent wallet address
*/
parentAddress(address: string): void {
if (!isValidEthAddress(address)) {
throw new BuildTransactionError('Invalid address: ' + address);
}
this._parentAddress = address;
}

/**
* Build a transaction to flush ERC-7984 tokens from a V4 forwarder to the parent wallet.
* Encodes: callFromParent(tokenContractAddress, 0, confidentialTransfer(parentAddress, encryptedHandle))
*
* @returns {TxData} The Ethereum transaction data
*/
private buildFlushERC7984ForwarderTokenTransaction(): TxData {
if (this._contractAddress !== this._forwarderAddress) {
throw new BuildTransactionError('contractAddress must equal forwarderAddress for FlushERC7984ForwarderToken');
}
const data = buildFlushERC7984ForwarderTokenCalldata(
this._tokenContractAddress,
this._parentAddress,
this._encryptedHandle
);
return this.buildBase(data);
}

// endregion
}
42 changes: 41 additions & 1 deletion modules/abstract-eth/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
ERC1155TransferData,
ERC721TransferData,
FlushTokensData,
FlushERC7984ForwarderTokenData,
NativeTransferData,
SignatureParts,
TokenTransferData,
Expand Down Expand Up @@ -85,9 +86,15 @@ import {
sendMultiSigTypesFirstSigner,
confidentialTransferWithProofMethodId,
confidentialTransferWithProofTypes,
confidentialTransferNoProofMethodId,
} from './walletUtil';
import { EthTransactionData } from './types';
import { delegateForUserDecryptionMethodId } from './zamaUtils';
import {
callFromParentMethodId,
callFromParentTypes,
decodeFlushERC7984ForwarderTokenCalldata,
delegateForUserDecryptionMethodId,
} from './zamaUtils';

/**
* @param network
Expand Down Expand Up @@ -735,6 +742,23 @@ export function decodeConfidentialTransferData(data: string): ConfidentialTransf
};
}

/**
* Decode a FlushERC7984ForwarderToken transaction's calldata into its component parts.
*
* @param data The full calldata hex string (callFromParent wrapping confidentialTransfer)
* @param to The `to` field of the transaction (equals the forwarder address for V4)
* @returns Decoded fields including forwarderAddress, tokenContractAddress, parentAddress, encryptedHandle
*/
export function decodeFlushERC7984ForwarderTokenData(data: string, to: string): FlushERC7984ForwarderTokenData {
const { tokenContractAddress, parentAddress, encryptedHandle } = decodeFlushERC7984ForwarderTokenCalldata(data);
return {
forwarderAddress: to,
tokenContractAddress,
parentAddress,
encryptedHandle,
};
}

/**
* Classify the given transaction data based as a transaction type.
* ETH transactions are defined by the first 8 bytes of the transaction data, also known as the method id
Expand Down Expand Up @@ -765,6 +789,22 @@ export function classifyTransaction(data: string): TransactionType {
}
}

// Peek inside callFromParent to detect FlushERC7984ForwarderToken
if (transactionType === undefined && data.startsWith(callFromParentMethodId)) {
try {
const [, , innerData] = getRawDecoded(
[...callFromParentTypes],
getBufferedByteCode(callFromParentMethodId, data)
);
const innerHex = bufferToHex(innerData as Buffer);
if (innerHex.startsWith(confidentialTransferNoProofMethodId)) {
return TransactionType.FlushERC7984ForwarderToken;
}
} catch {
// Not a confidential flush; fall through to ContractCall
}
}

if (transactionType === undefined) {
transactionType = TransactionType.ContractCall;
}
Expand Down
2 changes: 2 additions & 0 deletions modules/abstract-eth/src/lib/walletUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ export const confidentialTransferWithProofMethodId = '0x2fb74e62';
export const confidentialTransferNoProofMethodId = '0x5bebed7e';
// ABI parameter types for the 3-param version
export const confidentialTransferWithProofTypes = ['address', 'bytes32', 'bytes'];
// ABI parameter types for confidentialTransfer(address, bytes32) — no proof version
export const confidentialTransferNoProofTypes = ['address', 'bytes32'];
82 changes: 81 additions & 1 deletion modules/abstract-eth/src/lib/zamaUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { addHexPrefix, toBuffer } from 'ethereumjs-util';
import { addHexPrefix, bufferToHex, toBuffer } from 'ethereumjs-util';
import EthereumAbi from 'ethereumjs-abi';
import { ethers } from 'ethers';
import { confidentialTransferNoProofMethodId, confidentialTransferNoProofTypes } from './walletUtil';

// ---------------------------------------------------------------------------
// Constants
Expand Down Expand Up @@ -188,3 +189,82 @@ export function decodeTokenAddressesFromDelegationCalldata(calldata: string): st

return tokenAddresses;
}

/**
* Encodes confidentialTransfer(address to, bytes32 encryptedHandle) calldata.
* Uses the no-proof variant (selector 0x5bebed7e) — valid when the caller (forwarder)
* is already ACL-allowed on the handle from when it received the tokens.
*
* @param toAddress Address that will receive the tokens
* @param encryptedHandle bytes32 encrypted balance handle from confidentialBalanceOf
* @returns ABI-encoded calldata hex string (0x-prefixed)
*/
export function buildConfidentialTransferByHandleCalldata(toAddress: string, encryptedHandle: string): string {
const method = Buffer.from(confidentialTransferNoProofMethodId.slice(2), 'hex');
const handleBuffer = toBuffer(encryptedHandle);
const args = EthereumAbi.rawEncode([...confidentialTransferNoProofTypes], [toAddress, handleBuffer]);
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
}

/**
* Encodes the full flush calldata for ERC-7984 forwarder consolidation:
* callFromParent(tokenContractAddress, 0, confidentialTransfer(parentAddress, encryptedHandle))
* The forwarder executes the inner confidentialTransfer with msg.sender = forwarder.
*
* @param tokenContractAddress ERC-7984 token contract address
* @param parentAddress Root wallet address (destination of flushed tokens)
* @param encryptedHandle bytes32 encrypted balance handle from confidentialBalanceOf
* @returns ABI-encoded calldata hex string (0x-prefixed)
*/
export function buildFlushERC7984ForwarderTokenCalldata(
tokenContractAddress: string,
parentAddress: string,
encryptedHandle: string
): string {
const innerCalldata = buildConfidentialTransferByHandleCalldata(parentAddress, encryptedHandle);
return wrapInCallFromParent(tokenContractAddress, innerCalldata);
}

/**
* Decodes a FlushERC7984ForwarderToken calldata.
* Strips the outer callFromParent wrapper and the inner confidentialTransfer.
* Returns { tokenContractAddress, parentAddress, encryptedHandle }.
*
* @param data ABI-encoded flush calldata (0x-prefixed)
* @returns Decoded fields
*/
export function decodeFlushERC7984ForwarderTokenCalldata(data: string): {
tokenContractAddress: string;
parentAddress: string;
encryptedHandle: string;
} {
if (!data.startsWith(callFromParentMethodId)) {
throw new Error(
`Invalid FlushERC7984ForwarderToken calldata: expected callFromParent selector, got ${data.slice(0, 10)}`
);
}

const abiCoder = new ethers.utils.AbiCoder();
const outerDecoded = abiCoder.decode([...callFromParentTypes], '0x' + data.slice(10));
const tokenContractAddress: string = outerDecoded[0];
const innerCalldata: string = ethers.utils.hexlify(outerDecoded[2]);

if (!innerCalldata.startsWith(confidentialTransferNoProofMethodId)) {
throw new Error(
`Invalid FlushERC7984ForwarderToken inner calldata: expected confidentialTransfer selector, got ${innerCalldata.slice(
0,
10
)}`
);
}

const innerDecoded = abiCoder.decode([...confidentialTransferNoProofTypes], '0x' + innerCalldata.slice(10));
const parentAddress: string = innerDecoded[0];
const encryptedHandle: string = bufferToHex(toBuffer(innerDecoded[1]));

return {
tokenContractAddress,
parentAddress,
encryptedHandle,
};
}
Loading
Loading