Skip to content

Commit

Permalink
[Native] ZkSync Account Abstraction Support (#194)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xFirekeeper authored Jun 3, 2024
1 parent 963f06e commit 596bc41
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Net.Http;
using System.Threading.Tasks;
using Nethereum.JsonRpc.Client.RpcMessages;
using Nethereum.RPC.Eth.DTOs;
using Newtonsoft.Json;

namespace Thirdweb.AccountAbstraction
Expand Down Expand Up @@ -69,6 +70,25 @@ string entryPoint
}
}

public static async Task<ZkPaymasterDataResponse> ZkPaymasterData(string paymasterUrl, string apiKey, string bundleId, object requestId, TransactionInput txInput)
{
var response = await BundlerRequest(paymasterUrl, apiKey, bundleId, requestId, "zk_paymasterData", txInput);
try
{
return JsonConvert.DeserializeObject<ZkPaymasterDataResponse>(response.Result.ToString());
}
catch
{
return new ZkPaymasterDataResponse() { paymaster = null, paymasterInput = null };
}
}

public static async Task<ZkBroadcastTransactionResponse> ZkBroadcastTransaction(string paymasterUrl, string apiKey, string bundleId, object requestId, object txInput)
{
var response = await BundlerRequest(paymasterUrl, apiKey, bundleId, requestId, "zk_broadcastTransaction", txInput);
return JsonConvert.DeserializeObject<ZkBroadcastTransactionResponse>(response.Result.ToString());
}

// Request

private static async Task<RpcResponseMessage> BundlerRequest(string url, string apiKey, string bundleId, object requestId, string method, params object[] args)
Expand Down
122 changes: 122 additions & 0 deletions Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/SmartWallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using Thirdweb.Wallets;
using Nethereum.Signer;
using System.Security.Cryptography;
using Newtonsoft.Json.Linq;

namespace Thirdweb.AccountAbstraction
{
Expand Down Expand Up @@ -50,6 +51,7 @@ public class SmartWallet
public bool IsDeploying => _deploying;

private readonly ThirdwebSDK _sdk;
private bool IsZkSync => _sdk.Session.ChainId == 300 || _sdk.Session.ChainId == 324;

public SmartWallet(IThirdwebWallet personalWallet, ThirdwebSDK sdk)
{
Expand All @@ -72,6 +74,13 @@ internal async Task Initialize(string smartWalletOverride = null)
if (_initialized)
return;

if (IsZkSync)
{
Accounts = new List<string>() { await GetPersonalAddress() };
_initialized = true;
return;
}

var predictedAccount =
smartWalletOverride
?? (
Expand All @@ -95,18 +104,33 @@ internal async Task Initialize(string smartWalletOverride = null)

internal async Task UpdateDeploymentStatus()
{
if (IsZkSync)
{
_deployed = true;
return;
}

var web3 = Utils.GetWeb3(_sdk.Session.ChainId, _sdk.Session.Options.clientId, _sdk.Session.Options.bundleId);
var bytecode = await web3.Eth.GetCode.SendRequestAsync(Accounts[0]);
_deployed = bytecode != "0x";
}

internal async Task<TransactionResult> SetPermissionsForSigner(SignerPermissionRequest signerPermissionRequest, byte[] signature)
{
if (IsZkSync)
{
throw new NotImplementedException("SetPermissionsForSigner is not supported on zkSync");
}
return await TransactionManager.ThirdwebWrite(_sdk, Accounts[0], new SetPermissionsForSignerFunction() { Req = signerPermissionRequest, Signature = signature });
}

internal async Task ForceDeploy()
{
if (IsZkSync)
{
return;
}

if (_deployed)
return;

Expand All @@ -118,6 +142,11 @@ internal async Task ForceDeploy()

internal async Task<bool> VerifySignature(byte[] hash, byte[] signature)
{
if (IsZkSync)
{
throw new NotImplementedException("VerifySignature is not supported on zkSync");
}

try
{
var verifyRes = await TransactionManager.ThirdwebRead<AccountContract.IsValidSignatureFunction, AccountContract.IsValidSignatureOutputDTO>(
Expand All @@ -136,6 +165,11 @@ internal async Task<bool> VerifySignature(byte[] hash, byte[] signature)

internal async Task<(byte[] initCode, BigInteger gas)> GetInitCode()
{
if (IsZkSync)
{
throw new NotImplementedException("GetInitCode is not supported on zkSync");
}

if (_deployed)
return (new byte[] { }, 0);

Expand All @@ -153,13 +187,25 @@ internal async Task<RpcResponseMessage> Request(RpcRequestMessage requestMessage

if (requestMessage.Method == "eth_signTransaction")
{
if (IsZkSync)
{
throw new NotImplementedException("eth_signTransaction is not supported on zkSync");
}

var parameters = JsonConvert.DeserializeObject<object[]>(JsonConvert.SerializeObject(requestMessage.RawParameters));
var txInput = JsonConvert.DeserializeObject<TransactionInput>(JsonConvert.SerializeObject(parameters[0]));
var partialUserOp = await SignTransactionAsUserOp(txInput, requestMessage.Id);
return new RpcResponseMessage(requestMessage.Id, JsonConvert.SerializeObject(EncodeUserOperation(partialUserOp)));
}
else if (requestMessage.Method == "eth_sendTransaction")
{
if (IsZkSync)
{
var paramList = JsonConvert.DeserializeObject<List<object>>(JsonConvert.SerializeObject(requestMessage.RawParameters));
var transactionInput = JsonConvert.DeserializeObject<TransactionInput>(JsonConvert.SerializeObject(paramList[0]));
var hash = await SendZkSyncAATransaction(transactionInput);
return new RpcResponseMessage(requestMessage.Id, hash);
}
return await CreateUserOpAndSend(requestMessage);
}
else if (requestMessage.Method == "eth_chainId")
Expand Down Expand Up @@ -188,6 +234,82 @@ internal async Task<RpcResponseMessage> Request(RpcRequestMessage requestMessage
}
}

private async Task<string> SendZkSyncAATransaction(TransactionInput transactionInput)
{
var transaction = new Transaction(_sdk, transactionInput);
var web3 = Utils.GetWeb3(_sdk.Session.ChainId, _sdk.Session.Options.clientId, _sdk.Session.Options.bundleId);

if (transactionInput.Nonce == null)
{
var nonce = await web3.Client.SendRequestAsync<HexBigInteger>(method: "eth_getTransactionCount", route: null, paramList: new object[] { Accounts[0], "latest" });
_ = transaction.SetNonce(nonce.Value.ToString());
}

var feeData = await web3.Client.SendRequestAsync<JToken>(method: "zks_estimateFee", route: null, paramList: new object[] { transactionInput, "latest" });
var maxFee = feeData["max_fee_per_gas"].ToObject<HexBigInteger>().Value * 10 / 5;
var maxPriorityFee = feeData["max_priority_fee_per_gas"].ToObject<HexBigInteger>().Value * 10 / 5;
var gasPerPubData = feeData["gas_per_pubdata_limit"].ToObject<HexBigInteger>().Value;
var gasLimit = feeData["gas_limit"].ToObject<HexBigInteger>().Value * 10 / 5;

if (_sdk.Session.Options.smartWalletConfig?.gasless == true)
{
var pmDataResult = await BundlerClient.ZkPaymasterData(
_sdk.Session.Options.smartWalletConfig?.paymasterUrl,
_sdk.Session.Options.clientId,
_sdk.Session.Options.bundleId,
1,
transactionInput
);

var zkTx = new AccountAbstraction.ZkSyncAATransaction
{
TxType = 0x71,
From = new HexBigInteger(transaction.Input.From).Value,
To = new HexBigInteger(transaction.Input.To).Value,
GasLimit = gasLimit,
GasPerPubdataByteLimit = gasPerPubData,
MaxFeePerGas = maxFee,
MaxPriorityFeePerGas = maxPriorityFee,
Paymaster = new HexBigInteger(pmDataResult.paymaster).Value,
Nonce = transaction.Input.Nonce.Value,
Value = transaction.Input.Value?.Value ?? 0,
Data = transaction.Input.Data?.HexToByteArray() ?? new byte[0],
FactoryDeps = new List<byte[]>(),
PaymasterInput = pmDataResult.paymasterInput?.HexToByteArray() ?? new byte[0]
};

var zkTxSigned = await EIP712.GenerateSignature_ZkSyncTransaction(_sdk, "zkSync", "2", _sdk.Session.ChainId, zkTx);

// Match bundler ZkTransactionInput type without recreating
var zkBroadcastResult = await BundlerClient.ZkBroadcastTransaction(
_sdk.Session.Options.smartWalletConfig?.paymasterUrl,
_sdk.Session.Options.clientId,
_sdk.Session.Options.bundleId,
1,
new
{
nonce = zkTx.Nonce.ToString(),
from = zkTx.From,
to = zkTx.To,
gas = zkTx.GasLimit.ToString(),
gasPrice = string.Empty,
value = zkTx.Value.ToString(),
data = Utils.ByteArrayToHexString(zkTx.Data),
maxFeePerGas = zkTx.MaxFeePerGas.ToString(),
maxPriorityFeePerGas = zkTx.MaxPriorityFeePerGas.ToString(),
chainId = _sdk.Session.ChainId.ToString(),
signedTransaction = zkTxSigned,
paymaster = pmDataResult.paymaster,
}
);
return zkBroadcastResult.transactionHash;
}
else
{
throw new NotImplementedException("ZkSync Smart Wallet transactions are not supported without gasless mode");
}
}

private async Task<EntryPointContract.UserOperation> SignTransactionAsUserOp(TransactionInput transactionInput, object requestId = null)
{
requestId ??= SmartWalletClient.GenerateRpcId();
Expand Down
57 changes: 57 additions & 0 deletions Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/Types.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Collections.Generic;
using System.Numerics;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.RPC.Eth.DTOs;

namespace Thirdweb.AccountAbstraction
Expand Down Expand Up @@ -37,4 +40,58 @@ public class ThirdwebGetUserOperationGasPriceResponse
public string maxFeePerGas { get; set; }
public string maxPriorityFeePerGas { get; set; }
}

public class ZkPaymasterDataResponse
{
public string paymaster { get; set; }
public string paymasterInput { get; set; }
}

public class ZkBroadcastTransactionResponse
{
public string transactionHash { get; set; }
}

[Struct("Transaction")]
public class ZkSyncAATransaction
{
[Parameter("uint256", "txType", 1)]
public virtual BigInteger TxType { get; set; }

[Parameter("uint256", "from", 2)]
public virtual BigInteger From { get; set; }

[Parameter("uint256", "to", 3)]
public virtual BigInteger To { get; set; }

[Parameter("uint256", "gasLimit", 4)]
public virtual BigInteger GasLimit { get; set; }

[Parameter("uint256", "gasPerPubdataByteLimit", 5)]
public virtual BigInteger GasPerPubdataByteLimit { get; set; }

[Parameter("uint256", "maxFeePerGas", 6)]
public virtual BigInteger MaxFeePerGas { get; set; }

[Parameter("uint256", "maxPriorityFeePerGas", 7)]
public virtual BigInteger MaxPriorityFeePerGas { get; set; }

[Parameter("uint256", "paymaster", 8)]
public virtual BigInteger Paymaster { get; set; }

[Parameter("uint256", "nonce", 9)]
public virtual BigInteger Nonce { get; set; }

[Parameter("uint256", "value", 10)]
public virtual BigInteger Value { get; set; }

[Parameter("bytes", "data", 11)]
public virtual byte[] Data { get; set; }

[Parameter("bytes32[]", "factoryDeps", 12)]
public virtual List<byte[]> FactoryDeps { get; set; }

[Parameter("bytes", "paymasterInput", 13)]
public virtual byte[] PaymasterInput { get; set; }
}
}
73 changes: 71 additions & 2 deletions Assets/Thirdweb/Core/Scripts/EIP712.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@
using TokenERC1155Contract = Thirdweb.Contracts.TokenERC1155.ContractDefinition;
using MinimalForwarder = Thirdweb.Contracts.Forwarder.ContractDefinition;
using AccountContract = Thirdweb.Contracts.Account.ContractDefinition;
using System;
using System.Collections.Generic;
using Nethereum.Model;
using Nethereum.Hex.HexConvertors.Extensions;
using Nethereum.RLP;
using System.Linq;

namespace Thirdweb
{
public static class EIP712
{
/// SIGNATURE GENERATION ///
#region Signature Generation

public async static Task<string> GenerateSignature_MinimalForwarder(
ThirdwebSDK sdk,
Expand Down Expand Up @@ -147,6 +153,16 @@ public async static Task<string> GenerateSignature_SmartAccount_AccountMessage(T
return await sdk.Wallet.SignTypedDataV4(accountMessage, typedData);
}

public static async Task<string> GenerateSignature_ZkSyncTransaction(ThirdwebSDK sdk, string domainName, string version, BigInteger chainId, AccountAbstraction.ZkSyncAATransaction transaction)
{
var typedData = GetTypedDefinition_ZkSyncTransaction(domainName, version, chainId);
var signatureHex = await sdk.Wallet.SignTypedDataV4(transaction, typedData);
var signatureRaw = EthECDSASignatureFactory.ExtractECDSASignature(signatureHex);
return SerializeEip712(transaction, signatureRaw, chainId);
}

#endregion

#region Typed Data Definitions

public static TypedData<Domain> GetTypedDefinition_TokenERC20(string domainName, string version, BigInteger chainId, string verifyingContract)
Expand Down Expand Up @@ -244,10 +260,63 @@ public static TypedData<Domain> GetTypedDefinition_SmartAccount_AccountMessage(s
PrimaryType = nameof(AccountMessage),
};
}
}

public static TypedData<DomainWithNameVersionAndChainId> GetTypedDefinition_ZkSyncTransaction(string domainName, string version, BigInteger chainId)
{
return new TypedData<DomainWithNameVersionAndChainId>
{
Domain = new DomainWithNameVersionAndChainId
{
Name = domainName,
Version = version,
ChainId = chainId,
},
Types = MemberDescriptionFactory.GetTypesMemberDescription(typeof(DomainWithNameVersionAndChainId), typeof(AccountAbstraction.ZkSyncAATransaction)),
PrimaryType = "Transaction",
};
}

#endregion

#region Helpers

private static string SerializeEip712(AccountAbstraction.ZkSyncAATransaction transaction, EthECDSASignature signature, BigInteger chainId)
{
if (chainId == 0)
{
throw new ArgumentException("Chain ID must be provided for EIP712 transactions!");
}

var fields = new List<byte[]>
{
transaction.Nonce == 0 ? new byte[0] : transaction.Nonce.ToByteArray(isUnsigned: true, isBigEndian: true),
transaction.MaxPriorityFeePerGas == 0 ? new byte[0] : transaction.MaxPriorityFeePerGas.ToByteArray(isUnsigned: true, isBigEndian: true),
transaction.MaxFeePerGas.ToByteArray(isUnsigned: true, isBigEndian: true),
transaction.GasLimit.ToByteArray(isUnsigned: true, isBigEndian: true),
transaction.To.ToByteArray(isUnsigned: true, isBigEndian: true),
transaction.Value == 0 ? new byte[0] : transaction.Value.ToByteArray(isUnsigned: true, isBigEndian: true),
transaction.Data == null ? new byte[0] : transaction.Data,
};

fields.Add(signature.IsVSignedForYParity() ? new byte[] { 0x1b } : new byte[] { 0x1c });
fields.Add(signature.R);
fields.Add(signature.S);

fields.Add(chainId.ToByteArray(isUnsigned: true, isBigEndian: true));
fields.Add(transaction.From.ToByteArray(isUnsigned: true, isBigEndian: true));

// Add meta
fields.Add(transaction.GasPerPubdataByteLimit.ToByteArray(isUnsigned: true, isBigEndian: true));
fields.Add(new byte[] { }); // TODO: FactoryDeps
fields.Add(signature.CreateStringSignature().HexToByteArray());
// add array of rlp encoded paymaster/paymasterinput
fields.Add(RLP.EncodeElement(transaction.Paymaster.ToByteArray(isUnsigned: true, isBigEndian: true)).Concat(RLP.EncodeElement(transaction.PaymasterInput)).ToArray());

return "0x71" + RLP.EncodeDataItemsAsElementOrListAndCombineAsList(fields.ToArray(), new int[] { 13, 15 }).ToHex();
}

#endregion
}

public partial class AccountMessage : AccountMessageBase { }

Expand Down
Loading

0 comments on commit 596bc41

Please sign in to comment.