Skip to content

ERC-6492 Predeploy Signature Verification #105

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 2 commits into from
Dec 6, 2024
Merged
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 Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.SmartWallet.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,15 @@ public async Task GetAddress_WithOverride()
public async Task PersonalSign() // This is the only different signing mechanism for smart wallets, also tests isValidSignature
{
var account = await this.GetSmartAccount();

// ERC-6942 Verification
var sig = await account.PersonalSign("Hello, world!");
Assert.NotNull(sig);

// Raw EIP-1271 Verification
await account.ForceDeploy();
var sig2 = await account.PersonalSign("Hello, world!");
Assert.NotNull(sig2);
}

[Fact(Timeout = 120000)]
Expand Down
58 changes: 23 additions & 35 deletions Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,28 +102,12 @@ public static async Task<string> FetchAbi(ThirdwebClient client, string address,
public static async Task<T> Read<T>(ThirdwebContract contract, string method, params object[] parameters)
{
var rpc = ThirdwebRPC.GetRpcInstance(contract.Client, contract.Chain);
var contractRaw = new Contract(null, contract.Abi, contract.Address);

var function = GetFunctionMatchSignature(contractRaw, method, parameters);
if (function == null)
{
if (method.Contains('('))
{
var canonicalSignature = ExtractCanonicalSignature(method);
var selector = Nethereum.Util.Sha3Keccack.Current.CalculateHash(canonicalSignature)[..8];
function = contractRaw.GetFunctionBySignature(selector);
}
else
{
throw new ArgumentException("Method signature not found in contract ABI.");
}
}

var data = function.GetData(parameters);
(var data, var function) = EncodeFunctionCall(contract, method, parameters);
var resultData = await rpc.SendRequestAsync<string>("eth_call", new { to = contract.Address, data }, "latest").ConfigureAwait(false);

if ((typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(List<>)) || typeof(T).IsArray)
{
var contractRaw = new Contract(null, contract.Abi, contract.Address);
var functionAbi = contractRaw.ContractBuilder.ContractABI.FindFunctionABIFromInputData(data);
var decoder = new FunctionCallDecoder();
var outputList = new FunctionCallDecoder().DecodeDefaultData(resultData.HexToBytes(), functionAbi.OutputParameters);
Expand Down Expand Up @@ -168,23 +152,7 @@ public static async Task<T> Read<T>(ThirdwebContract contract, string method, pa
/// <returns>A prepared transaction.</returns>
public static async Task<ThirdwebTransaction> Prepare(IThirdwebWallet wallet, ThirdwebContract contract, string method, BigInteger weiValue, params object[] parameters)
{
var contractRaw = new Contract(null, contract.Abi, contract.Address);
var function = GetFunctionMatchSignature(contractRaw, method, parameters);
if (function == null)
{
if (method.Contains('('))
{
var canonicalSignature = ExtractCanonicalSignature(method);
var selector = Nethereum.Util.Sha3Keccack.Current.CalculateHash(canonicalSignature)[..8];
function = contractRaw.GetFunctionBySignature(selector);
}
else
{
throw new ArgumentException("Method signature not found in contract ABI.");
}
}

var data = function.GetData(parameters);
var data = contract.CreateCallData(method, parameters);
var transaction = new ThirdwebTransactionInput(chainId: contract.Chain)
{
To = contract.Address,
Expand All @@ -210,6 +178,26 @@ public static async Task<ThirdwebTransactionReceipt> Write(IThirdwebWallet walle
return await ThirdwebTransaction.SendAndWaitForTransactionReceipt(thirdwebTx).ConfigureAwait(false);
}

internal static (string callData, Function function) EncodeFunctionCall(ThirdwebContract contract, string method, params object[] parameters)
{
var contractRaw = new Contract(null, contract.Abi, contract.Address);
var function = GetFunctionMatchSignature(contractRaw, method, parameters);
if (function == null)
{
if (method.Contains('('))
{
var canonicalSignature = ExtractCanonicalSignature(method);
var selector = Nethereum.Util.Sha3Keccack.Current.CalculateHash(canonicalSignature)[..8];
function = contractRaw.GetFunctionBySignature(selector);
}
else
{
throw new ArgumentException("Method signature not found in contract ABI.");
}
}
return (function.GetData(parameters), function);
}

/// <summary>
/// Gets a function matching the specified signature from the contract.
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ public static class ThirdwebExtensions
{
#region Common

/// <summary>
/// Returns whether the contract supports the specified interface.
/// </summary>
/// <param name="contract">The contract instance.</param>
/// <param name="interfaceId">The interface ID to check.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the contract supports the interface.</returns>
/// <exception cref="ArgumentNullException"></exception>
public static async Task<bool> SupportsInterface(this ThirdwebContract contract, string interfaceId)
{
if (contract == null)
Expand All @@ -19,6 +26,19 @@ public static async Task<bool> SupportsInterface(this ThirdwebContract contract,
return await ThirdwebContract.Read<bool>(contract, "supportsInterface", interfaceId.HexToBytes());
}

/// <summary>
/// Encodes the function call for the specified method and parameters.
/// </summary>
/// <param name="contract">The contract instance.</param>
/// <param name="method">The method to call.</param>
/// <param name="parameters">The parameters for the method.</param>
/// <returns>The generated calldata.</returns>
public static string CreateCallData(this ThirdwebContract contract, string method, params object[] parameters)
{
(var data, _) = ThirdwebContract.EncodeFunctionCall(contract, method, parameters);
return data;
}

/// <summary>
/// Reads data from the contract using the specified method.
/// </summary>
Expand Down
10 changes: 6 additions & 4 deletions Thirdweb/Thirdweb.RPC/ThirdwebRPC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public async Task<TResponse> SendRequestAsync<TResponse>(string method, params o
{
lock (this._cacheLock)
{
var cacheKey = GetCacheKey(method, parameters);
var cacheKey = GetCacheKey(this._rpcUrl.ToString(), method, parameters);
if (this._cache.TryGetValue(cacheKey, out var cachedItem) && (DateTime.Now - cachedItem.Timestamp) < this._cacheDuration)
{
if (cachedItem.Response is TResponse cachedResponse)
Expand Down Expand Up @@ -121,7 +121,7 @@ public async Task<TResponse> SendRequestAsync<TResponse>(string method, params o
{
lock (this._cacheLock)
{
var cacheKey = GetCacheKey(method, parameters);
var cacheKey = GetCacheKey(this._rpcUrl.ToString(), method, parameters);
this._cache[cacheKey] = (response, DateTime.Now);
}
return response;
Expand All @@ -133,7 +133,7 @@ public async Task<TResponse> SendRequestAsync<TResponse>(string method, params o
var deserializedResponse = JsonConvert.DeserializeObject<TResponse>(JsonConvert.SerializeObject(result));
lock (this._cacheLock)
{
var cacheKey = GetCacheKey(method, parameters);
var cacheKey = GetCacheKey(this._rpcUrl.ToString(), method, parameters);
this._cache[cacheKey] = (deserializedResponse, DateTime.Now);
}
return deserializedResponse;
Expand Down Expand Up @@ -238,10 +238,12 @@ private async Task SendBatchAsync(List<RpcRequest> batch)
}
}

private static string GetCacheKey(string method, params object[] parameters)
private static string GetCacheKey(string rpcUrl, string method, params object[] parameters)
{
var keyBuilder = new StringBuilder();

_ = keyBuilder.Append(rpcUrl);

_ = keyBuilder.Append(method);

foreach (var param in parameters)
Expand Down
6 changes: 6 additions & 0 deletions Thirdweb/Thirdweb.Utils/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ public static class Constants
public const string DEFAULT_FACTORY_ADDRESS_V06 = "0x85e23b94e7F5E9cC1fF78BCe78cfb15B81f0DF00";
public const string DEFAULT_FACTORY_ADDRESS_V07 = "0x4bE0ddfebcA9A5A4a617dee4DeCe99E7c862dceb";

public const string EIP_1271_MAGIC_VALUE = "0x1626ba7e00000000000000000000000000000000000000000000000000000000";
public const string ERC_6492_MAGIC_VALUE = "0x6492649264926492649264926492649264926492649264926492649264926492";
public const string MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
public const string MULTICALL3_ABI =
/*lang=json,strict*/
"[{\"type\":\"function\",\"stateMutability\":\"payable\",\"outputs\":[{\"type\":\"uint256\",\"name\":\"blockNumber\",\"internalType\":\"uint256\"},{\"type\":\"bytes[]\",\"name\":\"returnData\",\"internalType\":\"bytes[]\"}],\"name\":\"aggregate\",\"inputs\":[{\"type\":\"tuple[]\",\"name\":\"calls\",\"internalType\":\"struct Multicall3.Call[]\",\"components\":[{\"type\":\"address\",\"name\":\"target\",\"internalType\":\"address\"},{\"type\":\"bytes\",\"name\":\"callData\",\"internalType\":\"bytes\"}]}]},{\"type\":\"function\",\"stateMutability\":\"payable\",\"outputs\":[{\"type\":\"tuple[]\",\"name\":\"returnData\",\"internalType\":\"struct Multicall3.Result[]\",\"components\":[{\"type\":\"bool\",\"name\":\"success\",\"internalType\":\"bool\"},{\"type\":\"bytes\",\"name\":\"returnData\",\"internalType\":\"bytes\"}]}],\"name\":\"aggregate3\",\"inputs\":[{\"type\":\"tuple[]\",\"name\":\"calls\",\"internalType\":\"struct Multicall3.Call3[]\",\"components\":[{\"type\":\"address\",\"name\":\"target\",\"internalType\":\"address\"},{\"type\":\"bool\",\"name\":\"allowFailure\",\"internalType\":\"bool\"},{\"type\":\"bytes\",\"name\":\"callData\",\"internalType\":\"bytes\"}]}]},{\"type\":\"function\",\"stateMutability\":\"payable\",\"outputs\":[{\"type\":\"tuple[]\",\"name\":\"returnData\",\"internalType\":\"struct Multicall3.Result[]\",\"components\":[{\"type\":\"bool\",\"name\":\"success\",\"internalType\":\"bool\"},{\"type\":\"bytes\",\"name\":\"returnData\",\"internalType\":\"bytes\"}]}],\"name\":\"aggregate3Value\",\"inputs\":[{\"type\":\"tuple[]\",\"name\":\"calls\",\"internalType\":\"struct Multicall3.Call3Value[]\",\"components\":[{\"type\":\"address\",\"name\":\"target\",\"internalType\":\"address\"},{\"type\":\"bool\",\"name\":\"allowFailure\",\"internalType\":\"bool\"},{\"type\":\"uint256\",\"name\":\"value\",\"internalType\":\"uint256\"},{\"type\":\"bytes\",\"name\":\"callData\",\"internalType\":\"bytes\"}]}]},{\"type\":\"function\",\"stateMutability\":\"payable\",\"outputs\":[{\"type\":\"uint256\",\"name\":\"blockNumber\",\"internalType\":\"uint256\"},{\"type\":\"bytes32\",\"name\":\"blockHash\",\"internalType\":\"bytes32\"},{\"type\":\"tuple[]\",\"name\":\"returnData\",\"internalType\":\"struct Multicall3.Result[]\",\"components\":[{\"type\":\"bool\",\"name\":\"success\",\"internalType\":\"bool\"},{\"type\":\"bytes\",\"name\":\"returnData\",\"internalType\":\"bytes\"}]}],\"name\":\"blockAndAggregate\",\"inputs\":[{\"type\":\"tuple[]\",\"name\":\"calls\",\"internalType\":\"struct Multicall3.Call[]\",\"components\":[{\"type\":\"address\",\"name\":\"target\",\"internalType\":\"address\"},{\"type\":\"bytes\",\"name\":\"callData\",\"internalType\":\"bytes\"}]}]},{\"type\":\"function\",\"stateMutability\":\"view\",\"outputs\":[{\"type\":\"uint256\",\"name\":\"basefee\",\"internalType\":\"uint256\"}],\"name\":\"getBasefee\",\"inputs\":[]},{\"type\":\"function\",\"stateMutability\":\"view\",\"outputs\":[{\"type\":\"bytes32\",\"name\":\"blockHash\",\"internalType\":\"bytes32\"}],\"name\":\"getBlockHash\",\"inputs\":[{\"type\":\"uint256\",\"name\":\"blockNumber\",\"internalType\":\"uint256\"}]},{\"type\":\"function\",\"stateMutability\":\"view\",\"outputs\":[{\"type\":\"uint256\",\"name\":\"blockNumber\",\"internalType\":\"uint256\"}],\"name\":\"getBlockNumber\",\"inputs\":[]},{\"type\":\"function\",\"stateMutability\":\"view\",\"outputs\":[{\"type\":\"uint256\",\"name\":\"chainid\",\"internalType\":\"uint256\"}],\"name\":\"getChainId\",\"inputs\":[]},{\"type\":\"function\",\"stateMutability\":\"view\",\"outputs\":[{\"type\":\"address\",\"name\":\"coinbase\",\"internalType\":\"address\"}],\"name\":\"getCurrentBlockCoinbase\",\"inputs\":[]},{\"type\":\"function\",\"stateMutability\":\"view\",\"outputs\":[{\"type\":\"uint256\",\"name\":\"difficulty\",\"internalType\":\"uint256\"}],\"name\":\"getCurrentBlockDifficulty\",\"inputs\":[]},{\"type\":\"function\",\"stateMutability\":\"view\",\"outputs\":[{\"type\":\"uint256\",\"name\":\"gaslimit\",\"internalType\":\"uint256\"}],\"name\":\"getCurrentBlockGasLimit\",\"inputs\":[]},{\"type\":\"function\",\"stateMutability\":\"view\",\"outputs\":[{\"type\":\"uint256\",\"name\":\"timestamp\",\"internalType\":\"uint256\"}],\"name\":\"getCurrentBlockTimestamp\",\"inputs\":[]},{\"type\":\"function\",\"stateMutability\":\"view\",\"outputs\":[{\"type\":\"uint256\",\"name\":\"balance\",\"internalType\":\"uint256\"}],\"name\":\"getEthBalance\",\"inputs\":[{\"type\":\"address\",\"name\":\"addr\",\"internalType\":\"address\"}]},{\"type\":\"function\",\"stateMutability\":\"view\",\"outputs\":[{\"type\":\"bytes32\",\"name\":\"blockHash\",\"internalType\":\"bytes32\"}],\"name\":\"getLastBlockHash\",\"inputs\":[]},{\"type\":\"function\",\"stateMutability\":\"payable\",\"outputs\":[{\"type\":\"tuple[]\",\"name\":\"returnData\",\"internalType\":\"struct Multicall3.Result[]\",\"components\":[{\"type\":\"bool\",\"name\":\"success\",\"internalType\":\"bool\"},{\"type\":\"bytes\",\"name\":\"returnData\",\"internalType\":\"bytes\"}]}],\"name\":\"tryAggregate\",\"inputs\":[{\"type\":\"bool\",\"name\":\"requireSuccess\",\"internalType\":\"bool\"},{\"type\":\"tuple[]\",\"name\":\"calls\",\"internalType\":\"struct Multicall3.Call[]\",\"components\":[{\"type\":\"address\",\"name\":\"target\",\"internalType\":\"address\"},{\"type\":\"bytes\",\"name\":\"callData\",\"internalType\":\"bytes\"}]}]},{\"type\":\"function\",\"stateMutability\":\"payable\",\"outputs\":[{\"type\":\"uint256\",\"name\":\"blockNumber\",\"internalType\":\"uint256\"},{\"type\":\"bytes32\",\"name\":\"blockHash\",\"internalType\":\"bytes32\"},{\"type\":\"tuple[]\",\"name\":\"returnData\",\"internalType\":\"struct Multicall3.Result[]\",\"components\":[{\"type\":\"bool\",\"name\":\"success\",\"internalType\":\"bool\"},{\"type\":\"bytes\",\"name\":\"returnData\",\"internalType\":\"bytes\"}]}],\"name\":\"tryBlockAndAggregate\",\"inputs\":[{\"type\":\"bool\",\"name\":\"requireSuccess\",\"internalType\":\"bool\"},{\"type\":\"tuple[]\",\"name\":\"calls\",\"internalType\":\"struct Multicall3.Call[]\",\"components\":[{\"type\":\"address\",\"name\":\"target\",\"internalType\":\"address\"},{\"type\":\"bytes\",\"name\":\"callData\",\"internalType\":\"bytes\"}]}]}]";
public const string REDIRECT_HTML =
"<html lang=\"en\" style=\"background-color:#050505;color:#fff\"><body style=\"position:relative;display:flex;flex-direction:column;height:100%;width:100%;margin:0;justify-content:center;align-items:center;text-align:center;overflow:hidden\"><div style=\"position:fixed;top:0;left:50%;background-image:radial-gradient(ellipse at center,hsl(260deg 78% 35% / 40%),transparent 60%);width:2400px;height:1400px;transform:translate(-50%,-50%);z-index:-1\"></div><h1>Authentication Complete!</h1><h2>You may close this tab now and return to the game</h2></body></html>";

Expand Down
18 changes: 16 additions & 2 deletions Thirdweb/Thirdweb.Utils/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text;
using System.Text.RegularExpressions;
using ADRaffy.ENSNormalize;
using Nethereum.ABI;
using Nethereum.ABI.EIP712;
using Nethereum.ABI.FunctionEncoding;
using Nethereum.ABI.FunctionEncoding.Attributes;
Expand Down Expand Up @@ -87,8 +88,7 @@ public static byte[] HashPrefixedMessage(this byte[] messageBytes)
/// <returns>The hashed message.</returns>
public static string HashPrefixedMessage(this string message)
{
var signer = new EthereumMessageSigner();
return signer.HashPrefixedMessage(Encoding.UTF8.GetBytes(message)).ToHex(true);
return HashPrefixedMessage(Encoding.UTF8.GetBytes(message)).BytesToHex();
}

/// <summary>
Expand Down Expand Up @@ -1020,4 +1020,18 @@ static void StringifyLargeNumbers(JToken token)

return jObject.ToString();
}

/// <summary>
/// Serializes a signature for use with ERC-6492. The signature must be generated by a signer for an ERC-4337 Account Factory account with counterfactual deployment addresses.
/// </summary>
/// <param name="address">The ERC-4337 Account Factory address</param>
/// <param name="data">Account deployment calldata (if not deployed) for counterfactual verification</param>
/// <param name="signature">The original signature</param>
/// <returns>The serialized signature hex string.</returns>
public static string SerializeErc6492Signature(string address, byte[] data, byte[] signature)
{
var encoder = new ABIEncode();
var encodedParams = encoder.GetABIEncoded(new ABIValue("address", address), new ABIValue("bytes", data), new ABIValue("bytes", signature));
return HexConcat(encodedParams.BytesToHex(), Constants.ERC_6492_MAGIC_VALUE);
}
}
Loading
Loading