Skip to content

Commit

Permalink
ERC-6492 Predeploy Signature Verification (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xFirekeeper authored Dec 6, 2024
1 parent b75f909 commit 8537bf3
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 79 deletions.
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

0 comments on commit 8537bf3

Please sign in to comment.