-
Notifications
You must be signed in to change notification settings - Fork 303
feat: add decryption delegation support for zama #8765
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
124 changes: 124 additions & 0 deletions
124
modules/abstract-eth/src/lib/decryptionDelegationBuilder.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| import { buildMulticallDelegationCalldata, wrapInCallFromParent } from './zamaUtils'; | ||
|
|
||
| /** | ||
| * Parameters for building a Zama ERC-7984 decryption delegation transaction. | ||
| */ | ||
| export interface DecryptionDelegationBuilderParams { | ||
| /** Address of the Zama ACL contract on the target network. */ | ||
| aclContractAddress: string; | ||
|
|
||
| /** | ||
| * BitGo enterprise viewing key address that receives decryption rights. | ||
| */ | ||
| delegateAddress: string; | ||
|
|
||
| /** | ||
| * ERC-7984 token contract addresses to delegate for. | ||
| * One or more addresses — always encoded as ACL.multicall([delegateForUserDecryption x N]). | ||
| * Pass a single address for single-token delegation; the multicall wrapper is always used | ||
| * for a consistent transaction structure regardless of token count. | ||
| */ | ||
| tokenContractAddresses: string[]; | ||
|
|
||
| /** | ||
| * Delegation expiry as a Unix timestamp (seconds). | ||
| * Recommended: Math.floor(Date.now() / 1000) + 365 * 86400 (1 year) | ||
| */ | ||
| expiryTimestamp: number; | ||
|
|
||
| /** | ||
| * Optional forwarder contract address. | ||
| * | ||
| * When set, the delegation calldata is wrapped in a | ||
| * ForwarderV4.callFromParent(aclContractAddress, 0, delegationCalldata) call, | ||
| * so that the forwarder itself becomes msg.sender (and therefore the delegator) | ||
| * in the ACL call. | ||
| * | ||
| * Only the parentAddress (root wallet) may call callFromParent — | ||
| * this is enforced by the forwarder's onlyParent modifier. | ||
| * | ||
| * Leave undefined when the root wallet is delegating directly. | ||
| */ | ||
| forwarderAddress?: string; | ||
| } | ||
|
|
||
| /** | ||
| * The wallet-type-agnostic output of DecryptionDelegationBuilder.build(). | ||
| * | ||
| * WP is responsible for routing this to the correct signing path: | ||
| * - MPC (TSS): submit as a raw transaction {to, data, value=0} | ||
| * - Multisig root: sendMultiSig(walletContract, to, 0, data, expiry, seqId, sig) | ||
| * - Multisig forwarder: sendMultiSig(walletContract, forwarder, 0, callFromParentData, expiry, seqId, sig) | ||
| */ | ||
| export interface DecryptionDelegationTxRequest { | ||
| /** | ||
| * Transaction recipient: | ||
| * - ACL contract address when delegating from root wallet directly | ||
| * - Forwarder address when wrapping in callFromParent | ||
| */ | ||
| to: string; | ||
|
|
||
| /** ABI-encoded calldata for the decryption delegation operation. */ | ||
| data: string; | ||
|
|
||
| /** Always '0' — decryption delegation transactions carry no ETH value. */ | ||
| value: string; | ||
| } | ||
|
|
||
| /** | ||
| * Builder for Zama ERC-7984 ACL decryption delegation transactions. | ||
| * | ||
| * Grants BitGo's enterprise viewing key the right to decrypt ERC-7984 token | ||
| * balances on behalf of the wallet owner via ACL.delegateForUserDecryption(). | ||
| * | ||
| * Produces a DecryptionDelegationTxRequest that works for both MPC and multisig | ||
| * wallets. Always uses ACL.multicall() regardless of token count, giving WP a | ||
| * consistent transaction structure to handle. | ||
| * | ||
| * Two scenarios: | ||
| * 1. Root wallet → ACL.multicall([delegateForUserDecryption x N]) sent directly to ACL | ||
| * 2. Forwarder → callFromParent(ACL, 0, multicall([...])) sent to forwarder contract | ||
| * | ||
| * Usage: | ||
| * const req = new DecryptionDelegationBuilder().build({ | ||
| * aclContractAddress: '0xf0Ff...', | ||
| * delegateAddress: enterpriseViewingKey, | ||
| * tokenContractAddresses: [tokenAddress], // one or more tokens | ||
| * expiryTimestamp: Math.floor(Date.now() / 1000) + 365 * 86400, | ||
| * }); | ||
| */ | ||
| export class DecryptionDelegationBuilder { | ||
| /** | ||
| * Build the decryption delegation transaction request. | ||
| * | ||
| * @param params Decryption delegation parameters | ||
| * @returns DecryptionDelegationTxRequest containing {to, data, value} ready for WP signing | ||
| * @throws Error if tokenContractAddresses is empty | ||
| */ | ||
| build(params: DecryptionDelegationBuilderParams): DecryptionDelegationTxRequest { | ||
| const { aclContractAddress, delegateAddress, tokenContractAddresses, expiryTimestamp, forwarderAddress } = params; | ||
|
|
||
| if (tokenContractAddresses.length === 0) { | ||
| throw new Error('DecryptionDelegationBuilder: tokenContractAddresses must not be empty'); | ||
| } | ||
|
|
||
| // Always encode as ACL.multicall([delegateForUserDecryption x N]) for a consistent | ||
| // transaction structure regardless of whether one or many tokens are delegated. | ||
| const innerCalldata = buildMulticallDelegationCalldata(delegateAddress, tokenContractAddresses, expiryTimestamp); | ||
|
|
||
| // Optionally wrap in callFromParent for forwarder delegation | ||
| if (forwarderAddress !== undefined) { | ||
| return { | ||
| to: forwarderAddress, | ||
| data: wrapInCallFromParent(aclContractAddress, innerCalldata), | ||
| value: '0', | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| to: aclContractAddress, | ||
| data: innerCalldata, | ||
| value: '0', | ||
| }; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| import { addHexPrefix, toBuffer } from 'ethereumjs-util'; | ||
| import EthereumAbi from 'ethereumjs-abi'; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Constants | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| // ABI parameter type arrays | ||
| export const delegateForUserDecryptionTypes = ['address', 'address', 'uint64'] as const; | ||
| export const callFromParentTypes = ['address', 'uint256', 'bytes'] as const; | ||
| export const aclMulticallTypes = ['bytes[]'] as const; | ||
|
|
||
| /** | ||
| * Function selector for ACL.delegateForUserDecryption(address,address,uint64) | ||
| * = keccak256('delegateForUserDecryption(address,address,uint64)')[0:4] | ||
| */ | ||
| export const delegateForUserDecryptionMethodId = addHexPrefix( | ||
| EthereumAbi.methodID('delegateForUserDecryption', [...delegateForUserDecryptionTypes]).toString('hex') | ||
| ); | ||
|
|
||
| /** | ||
| * Function selector for ACL.multicall(bytes[]) | ||
| * = keccak256('multicall(bytes[])')[0:4] | ||
| * ACL inherits OpenZeppelin MulticallUpgradeable — preserves msg.sender via delegatecall. | ||
| */ | ||
| export const aclMulticallMethodId = addHexPrefix( | ||
| EthereumAbi.methodID('multicall', [...aclMulticallTypes]).toString('hex') | ||
| ); | ||
|
|
||
| /** | ||
| * Function selector for ForwarderV4.callFromParent(address,uint256,bytes) | ||
| * = keccak256('callFromParent(address,uint256,bytes)')[0:4] | ||
| */ | ||
| export const callFromParentMethodId = addHexPrefix( | ||
| EthereumAbi.methodID('callFromParent', [...callFromParentTypes]).toString('hex') | ||
| ); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Encoding functions | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * Encodes a single ACL.delegateForUserDecryption() call. | ||
| * | ||
| * Grants `delegateAddress` the right to decrypt ERC-7984 token balances on | ||
| * behalf of the calling address (msg.sender) for the specified token contract. | ||
| * | ||
| * @param delegateAddress BitGo enterprise viewing key address | ||
| * @param tokenContractAddress ERC-7984 token contract address | ||
| * @param expiryTimestamp Unix seconds; recommended: Math.floor(Date.now()/1000) + 365*86400 | ||
| * @returns ABI-encoded calldata hex string (0x-prefixed) | ||
| */ | ||
| export function buildDelegationCalldata( | ||
| delegateAddress: string, | ||
| tokenContractAddress: string, | ||
| expiryTimestamp: number | ||
| ): string { | ||
| const method = EthereumAbi.methodID('delegateForUserDecryption', [...delegateForUserDecryptionTypes]); | ||
| const args = EthereumAbi.rawEncode( | ||
| [...delegateForUserDecryptionTypes], | ||
| [delegateAddress, tokenContractAddress, expiryTimestamp] | ||
| ); | ||
| return addHexPrefix(Buffer.concat([method, args]).toString('hex')); | ||
| } | ||
|
|
||
| /** | ||
| * Encodes N delegateForUserDecryption calls batched inside ACL.multicall(). | ||
| * | ||
| * Produces a single TX that grants delegation for all specified token contracts. | ||
| * Requires tokenContractAddresses.length >= 1. | ||
| * Note: DecryptionDelegationBuilder always uses this function (even for a single token) | ||
| * to keep the transaction shape consistent regardless of token count. | ||
| * | ||
| * @param delegateAddress BitGo enterprise viewing key address | ||
| * @param tokenContractAddresses Array of ERC-7984 token contract addresses | ||
| * @param expiryTimestamp Unix seconds | ||
| * @returns ABI-encoded calldata hex string (0x-prefixed) | ||
| */ | ||
| export function buildMulticallDelegationCalldata( | ||
| delegateAddress: string, | ||
| tokenContractAddresses: string[], | ||
| expiryTimestamp: number | ||
| ): string { | ||
| if (tokenContractAddresses.length === 0) { | ||
| throw new Error('buildMulticallDelegationCalldata: tokenContractAddresses must not be empty'); | ||
| } | ||
|
|
||
| // Build each inner delegateForUserDecryption call as raw bytes | ||
| const innerCalls: Buffer[] = tokenContractAddresses.map((tokenAddress) => { | ||
| const innerMethod = EthereumAbi.methodID('delegateForUserDecryption', [...delegateForUserDecryptionTypes]); | ||
| const innerArgs = EthereumAbi.rawEncode( | ||
| [...delegateForUserDecryptionTypes], | ||
| [delegateAddress, tokenAddress, expiryTimestamp] | ||
| ); | ||
| return Buffer.concat([innerMethod, innerArgs]); | ||
| }); | ||
|
MohammedRyaan786 marked this conversation as resolved.
|
||
|
|
||
| // Encode outer multicall(bytes[]) | ||
| const outerMethod = EthereumAbi.methodID('multicall', [...aclMulticallTypes]); | ||
| const outerArgs = EthereumAbi.rawEncode([...aclMulticallTypes], [innerCalls]); | ||
| return addHexPrefix(Buffer.concat([outerMethod, outerArgs]).toString('hex')); | ||
| } | ||
|
|
||
| /** | ||
| * Wraps calldata in a ForwarderV4.callFromParent(target, 0, data) call. | ||
| * | ||
| * Used when a forwarder contract must be msg.sender for an external contract | ||
| * call — for example, when the forwarder itself needs to call | ||
| * ACL.delegateForUserDecryption() so that its own balance can be decrypted. | ||
| * | ||
| * Only the parentAddress (root wallet) is allowed to call callFromParent | ||
| * (enforced by the forwarder's onlyParent modifier). | ||
| * | ||
| * @param targetAddress Address of the contract the forwarder will call (e.g. ACL) | ||
| * @param calldata ABI-encoded inner calldata (e.g. from buildDelegationCalldata) | ||
| * @returns ABI-encoded callFromParent calldata hex string (0x-prefixed) | ||
| */ | ||
| export function wrapInCallFromParent(targetAddress: string, calldata: string): string { | ||
| const method = EthereumAbi.methodID('callFromParent', [...callFromParentTypes]); | ||
| const args = EthereumAbi.rawEncode( | ||
| [...callFromParentTypes], | ||
| [ | ||
| targetAddress, | ||
| 0, // value: no ETH transfer | ||
| toBuffer(calldata), // inner calldata as bytes | ||
| ] | ||
| ); | ||
| return addHexPrefix(Buffer.concat([method, args]).toString('hex')); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.