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
13 changes: 13 additions & 0 deletions modules/abstract-eth/src/lib/iface.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,3 +175,16 @@ export interface WrapERC7984Data {
/** Underlying amount wrapped (base units, decimal string) */
amount: string;
}

export interface UnwrapERC7984Data {
/** Confidential wrapper contract (tx.to) */
wrapperAddress: string;
/** Source of confidential balance */
from: string;
/** Recipient of released underlying ERC-20 */
to: string;
/** bytes32 encrypted amount handle */
encryptedAmount: string;
/** Encryption input proof */
inputProof: string;
}
104 changes: 103 additions & 1 deletion modules/abstract-eth/src/lib/transactionBuilder.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,7 @@ import {
SignatureParts,
TxData,
WrapERC7984Data,
UnwrapERC7984Data,
} from './iface';
import {
calculateForwarderAddress,
Expand All@@ -38,6 +39,7 @@ import {
decodeFlushERC1155TokensData,
decodeFlushERC7984ForwarderTokenData,
decodeWrapERC7984Data,
decodeUnwrapERC7984Data,
decodeWalletCreationData,
flushCoinsData,
flushTokensData,
Expand All@@ -51,7 +53,7 @@ import {
getV1WalletInitializationData,
getCreateForwarderParamsAndTypes,
} from './utils';
import { buildFlushERC7984ForwarderTokenCalldata, buildWrapCalldata } from './zamaUtils';
import { buildFlushERC7984ForwarderTokenCalldata, buildWrapCalldata, buildUnwrapCalldata } from './zamaUtils';
import { defaultWalletVersion, walletSimpleConstructor } from './walletUtil';
import { ERC1155TransferBuilder } from './transferBuilders/transferBuilderERC1155';
import { ERC721TransferBuilder } from './transferBuilders/transferBuilderERC721';
Expand DownExpand Up@@ -97,6 +99,12 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
private _wrapAmount: string; // underlying amount to wrap (base units)
private _wrapRate: string; // on-chain rate() for uint64 validation

// UnwrapERC7984 parameters
private _unwrapFrom: string;
private _unwrapTo: string;
private _unwrapEncryptedAmount: string;
private _unwrapInputProof: string;

// Send and AddressInitialization transaction specific parameters
protected _transfer: TransferBuilder | ERC721TransferBuilder | ERC1155TransferBuilder | TransferBuilderERC7984;
private _contractAddress: string;
Expand DownExpand Up@@ -185,6 +193,8 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
return this.buildFlushERC7984ForwarderTokenTransaction();
case TransactionType.WrapERC7984:
return this.buildWrapERC7984Transaction();
case TransactionType.UnwrapERC7984:
return this.buildUnwrapERC7984Transaction();
default:
throw new BuildTransactionError('Unsupported transaction type');
}
Expand DownExpand Up@@ -347,6 +357,15 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
// rate is not encoded in calldata; callers that rebuild must set wrapRate() again
break;
}
case TransactionType.UnwrapERC7984: {
this.setContract(transactionJson.to);
const unwrapData: UnwrapERC7984Data = decodeUnwrapERC7984Data(transactionJson.data, transactionJson.to!);
this.unwrapFrom(unwrapData.from);
this.unwrapTo(unwrapData.to);
this.unwrapEncryptedAmount(unwrapData.encryptedAmount);
this.unwrapInputProof(unwrapData.inputProof);
break;
}
default:
throw new BuildTransactionError('Unsupported transaction type');
// TODO: Add other cases of deserialization
Expand DownExpand Up@@ -510,6 +529,13 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
this.validateWrapRecipient();
this.validateWrapAmount();
break;
case TransactionType.UnwrapERC7984:
this.validateContractAddress();
this.validateUnwrapFrom();
this.validateUnwrapTo();
this.validateUnwrapEncryptedAmount();
this.validateUnwrapInputProof();
break;
default:
throw new BuildTransactionError('Unsupported transaction type');
}
Expand DownExpand Up@@ -605,6 +631,30 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
}
}

private validateUnwrapFrom(): void {
if (!this._unwrapFrom) {
throw new BuildTransactionError('Invalid transaction: missing unwrapFrom');
}
}

private validateUnwrapTo(): void {
if (!this._unwrapTo) {
throw new BuildTransactionError('Invalid transaction: missing unwrapTo');
}
}

private validateUnwrapEncryptedAmount(): void {
if (!this._unwrapEncryptedAmount) {
throw new BuildTransactionError('Invalid transaction: missing unwrapEncryptedAmount');
}
}

private validateUnwrapInputProof(): void {
if (!this._unwrapInputProof) {
throw new BuildTransactionError('Invalid transaction: missing unwrapInputProof');
}
}

private setContract(address: string | undefined): void {
if (address === undefined) {
throw new BuildTransactionError('Undefined recipient address');
Expand DownExpand Up@@ -1172,4 +1222,56 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
}

// endregion

// region UnwrapERC7984 builder methods

/**
* Set the unwrap `from` address (source of confidential balance).
*/
unwrapFrom(address: string): void {
if (!isValidEthAddress(address)) {
throw new BuildTransactionError('Invalid address: ' + address);
}
this._unwrapFrom = address;
}

/**
* Set the unwrap `to` address (recipient of released ERC-20).
*/
unwrapTo(address: string): void {
if (!isValidEthAddress(address)) {
throw new BuildTransactionError('Invalid address: ' + address);
}
this._unwrapTo = address;
}

/**
* Set the bytes32 encrypted amount handle for unwrap.
*/
unwrapEncryptedAmount(encryptedAmount: string): void {
this._unwrapEncryptedAmount = encryptedAmount;
}

/**
* Set the encryption input proof for unwrap.
*/
unwrapInputProof(inputProof: string): void {
this._unwrapInputProof = inputProof;
}

/**
* Build an UnwrapERC7984 (unshield phase-1) transaction.
* Does not set gasLimit — WP owns gas.
*/
private buildUnwrapERC7984Transaction(): TxData {
const data = buildUnwrapCalldata(
this._unwrapFrom,
this._unwrapTo,
this._unwrapEncryptedAmount,
this._unwrapInputProof
);
return this.buildBase(data);
}

// endregion
}
21 changes: 21 additions & 0 deletions modules/abstract-eth/src/lib/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,6 +43,7 @@ import {
WalletInitializationData,
ForwarderInitializationData,
WrapERC7984Data,
UnwrapERC7984Data,
} from './iface';
import { KeyPair } from './keyPair';
import {
Expand DownExpand Up@@ -95,8 +96,10 @@ import {
callFromParentTypes,
decodeFlushERC7984ForwarderTokenCalldata,
decodeWrapCalldata,
decodeUnwrapCalldata,
delegateForUserDecryptionMethodId,
wrapMethodId,
unwrapMethodId,
} from './zamaUtils';

/**
Expand DownExpand Up@@ -848,6 +851,23 @@ export function decodeWrapERC7984Data(data: string, to: string): WrapERC7984Data
};
}

/**
* Decode an UnwrapERC7984 transaction's calldata into its component parts.
*
* @param data The unwrap(address,address,bytes32,bytes) calldata hex
* @param to The transaction `to` field (wrapper contract address)
*/
export function decodeUnwrapERC7984Data(data: string, to: string): UnwrapERC7984Data {
const decoded = decodeUnwrapCalldata(data);
return {
wrapperAddress: to,
from: decoded.from,
to: decoded.to,
encryptedAmount: decoded.encryptedAmount,
inputProof: decoded.inputProof,
};
}

/**
* 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 DownExpand Up@@ -934,6 +954,7 @@ const transactionTypesMap = {
// explicitly when building from a known delegation template.
[delegateForUserDecryptionMethodId]: TransactionType.DecryptionDelegation,
[wrapMethodId]: TransactionType.WrapERC7984,
[unwrapMethodId]: TransactionType.UnwrapERC7984,
};

/**
Expand Down
79 changes: 78 additions & 1 deletion modules/abstract-eth/src/lib/zamaUtils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,10 +13,10 @@ export const callFromParentTypes = ['address', 'uint256', 'bytes'] as const;
export const aclMulticallTypes = ['bytes[]'] as const;
export const approveTypes = ['address', 'uint256'] as const;
export const wrapTypes = ['address', 'uint256'] as const;
export const unwrapTypes = ['address', 'address', 'bytes32', 'bytes'] as const;

/** Max value for Solidity `uint64` / ERC-7984 confidential amount domain (`euint64`). */
export const UINT64_MAX = 18446744073709551615n;

/**
* Function selector for ACL.delegateForUserDecryption(address,address,uint64)
* = keccak256('delegateForUserDecryption(address,address,uint64)')[0:4]
Expand DownExpand Up@@ -56,6 +56,13 @@ export const approveMethodId = addHexPrefix(EthereumAbi.methodID('approve', [...
*/
export const wrapMethodId = addHexPrefix(EthereumAbi.methodID('wrap', [...wrapTypes]).toString('hex'));

/**
* Function selector for ERC-7984 unwrap(address,address,bytes32,bytes)
* = keccak256('unwrap(address,address,bytes32,bytes)')[0:4]
* Burns confidential balance and requests release of underlying ERC-20 escrow.
*/
export const unwrapMethodId = addHexPrefix(EthereumAbi.methodID('unwrap', [...unwrapTypes]).toString('hex'));

// ---------------------------------------------------------------------------
// Encoding functions
// ---------------------------------------------------------------------------
Expand DownExpand Up@@ -195,6 +202,76 @@ export function decodeWrapCalldata(data: string): { to: string; amount: string }
};
}

/**
* Encodes ERC-7984 `unwrap(from, to, encryptedAmount, inputProof)` calldata for unshield.
*
* Calldata is sent to the confidential wrapper. Phase-1 unshield is self-directed:
* `from` and `to` are both the wallet base address. `encryptedAmount` is the
* FHE-encrypted burn amount (bytes32 handle) and `inputProof` is the Zama
* encryption proof — both produced by WP `ZamaRelayerService.encryptAmount`.
*
* Does not set gasLimit — WP owns gas.
*
* @param from Source of confidential balance (base address)
* @param to Recipient of released ERC-20 (base address in v1)
* @param encryptedAmount bytes32 encrypted amount handle (0x-prefixed)
* @param inputProof Encryption input proof bytes (0x-prefixed)
* @returns ABI-encoded calldata hex string (0x-prefixed)
* @throws {Error} if addresses or ciphertext fields are invalid
*/
export function buildUnwrapCalldata(from: string, to: string, encryptedAmount: string, inputProof: string): string {
let checksummedFrom: string;
let checksummedTo: string;
try {
checksummedFrom = ethers.utils.getAddress(from);
} catch {
throw new Error(`buildUnwrapCalldata: invalid from address '${from}'`);
}
try {
checksummedTo = ethers.utils.getAddress(to);
} catch {
throw new Error(`buildUnwrapCalldata: invalid to address '${to}'`);
}

if (!ethers.utils.isHexString(encryptedAmount) || ethers.utils.hexDataLength(encryptedAmount) !== 32) {
throw new Error(`buildUnwrapCalldata: encryptedAmount must be a 32-byte hex string, got '${encryptedAmount}'`);
}
if (!ethers.utils.isHexString(inputProof) || ethers.utils.hexDataLength(inputProof) === 0) {
throw new Error(`buildUnwrapCalldata: inputProof must be a non-empty hex string, got '${inputProof}'`);
}

const method = EthereumAbi.methodID('unwrap', [...unwrapTypes]);
const args = EthereumAbi.rawEncode(
[...unwrapTypes],
[checksummedFrom, checksummedTo, toBuffer(encryptedAmount), toBuffer(inputProof)]
);
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
}

/**
* Decodes ERC-7984 `unwrap(from, to, encryptedAmount, inputProof)` calldata.
*
* @param data ABI-encoded unwrap calldata (0x-prefixed)
*/
export function decodeUnwrapCalldata(data: string): {
from: string;
to: string;
encryptedAmount: string;
inputProof: string;
} {
if (!data.toLowerCase().startsWith(unwrapMethodId.toLowerCase())) {
throw new Error(`decodeUnwrapCalldata: expected unwrap selector ${unwrapMethodId}, got ${data.slice(0, 10)}`);
}
const abiCoder = new ethers.utils.AbiCoder();
const decoded = abiCoder.decode([...unwrapTypes], '0x' + data.slice(10));
return {
from: ethers.utils.getAddress(decoded[0]),
to: ethers.utils.getAddress(decoded[1]),
encryptedAmount: ethers.utils.hexlify(decoded[2]),
inputProof: ethers.utils.hexlify(decoded[3]),
};
}

/**
* Encodes a single ACL.delegateForUserDecryption() call.
*
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,3 +5,4 @@ export * from './flushNft';
export * from './decryptionDelegation';
export * from './flushERC7984';
export * from './wrapERC7984';
export * from './unwrapERC7984';
Loading
Loading