Skip to content

Commit 8537bf3

Browse files
authored
ERC-6492 Predeploy Signature Verification (#105)
1 parent b75f909 commit 8537bf3

File tree

8 files changed

+174
-79
lines changed

8 files changed

+174
-79
lines changed

Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.SmartWallet.Tests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,15 @@ public async Task GetAddress_WithOverride()
144144
public async Task PersonalSign() // This is the only different signing mechanism for smart wallets, also tests isValidSignature
145145
{
146146
var account = await this.GetSmartAccount();
147+
148+
// ERC-6942 Verification
147149
var sig = await account.PersonalSign("Hello, world!");
148150
Assert.NotNull(sig);
151+
152+
// Raw EIP-1271 Verification
153+
await account.ForceDeploy();
154+
var sig2 = await account.PersonalSign("Hello, world!");
155+
Assert.NotNull(sig2);
149156
}
150157

151158
[Fact(Timeout = 120000)]

Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -102,28 +102,12 @@ public static async Task<string> FetchAbi(ThirdwebClient client, string address,
102102
public static async Task<T> Read<T>(ThirdwebContract contract, string method, params object[] parameters)
103103
{
104104
var rpc = ThirdwebRPC.GetRpcInstance(contract.Client, contract.Chain);
105-
var contractRaw = new Contract(null, contract.Abi, contract.Address);
106-
107-
var function = GetFunctionMatchSignature(contractRaw, method, parameters);
108-
if (function == null)
109-
{
110-
if (method.Contains('('))
111-
{
112-
var canonicalSignature = ExtractCanonicalSignature(method);
113-
var selector = Nethereum.Util.Sha3Keccack.Current.CalculateHash(canonicalSignature)[..8];
114-
function = contractRaw.GetFunctionBySignature(selector);
115-
}
116-
else
117-
{
118-
throw new ArgumentException("Method signature not found in contract ABI.");
119-
}
120-
}
121-
122-
var data = function.GetData(parameters);
105+
(var data, var function) = EncodeFunctionCall(contract, method, parameters);
123106
var resultData = await rpc.SendRequestAsync<string>("eth_call", new { to = contract.Address, data }, "latest").ConfigureAwait(false);
124107

125108
if ((typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(List<>)) || typeof(T).IsArray)
126109
{
110+
var contractRaw = new Contract(null, contract.Abi, contract.Address);
127111
var functionAbi = contractRaw.ContractBuilder.ContractABI.FindFunctionABIFromInputData(data);
128112
var decoder = new FunctionCallDecoder();
129113
var outputList = new FunctionCallDecoder().DecodeDefaultData(resultData.HexToBytes(), functionAbi.OutputParameters);
@@ -168,23 +152,7 @@ public static async Task<T> Read<T>(ThirdwebContract contract, string method, pa
168152
/// <returns>A prepared transaction.</returns>
169153
public static async Task<ThirdwebTransaction> Prepare(IThirdwebWallet wallet, ThirdwebContract contract, string method, BigInteger weiValue, params object[] parameters)
170154
{
171-
var contractRaw = new Contract(null, contract.Abi, contract.Address);
172-
var function = GetFunctionMatchSignature(contractRaw, method, parameters);
173-
if (function == null)
174-
{
175-
if (method.Contains('('))
176-
{
177-
var canonicalSignature = ExtractCanonicalSignature(method);
178-
var selector = Nethereum.Util.Sha3Keccack.Current.CalculateHash(canonicalSignature)[..8];
179-
function = contractRaw.GetFunctionBySignature(selector);
180-
}
181-
else
182-
{
183-
throw new ArgumentException("Method signature not found in contract ABI.");
184-
}
185-
}
186-
187-
var data = function.GetData(parameters);
155+
var data = contract.CreateCallData(method, parameters);
188156
var transaction = new ThirdwebTransactionInput(chainId: contract.Chain)
189157
{
190158
To = contract.Address,
@@ -210,6 +178,26 @@ public static async Task<ThirdwebTransactionReceipt> Write(IThirdwebWallet walle
210178
return await ThirdwebTransaction.SendAndWaitForTransactionReceipt(thirdwebTx).ConfigureAwait(false);
211179
}
212180

181+
internal static (string callData, Function function) EncodeFunctionCall(ThirdwebContract contract, string method, params object[] parameters)
182+
{
183+
var contractRaw = new Contract(null, contract.Abi, contract.Address);
184+
var function = GetFunctionMatchSignature(contractRaw, method, parameters);
185+
if (function == null)
186+
{
187+
if (method.Contains('('))
188+
{
189+
var canonicalSignature = ExtractCanonicalSignature(method);
190+
var selector = Nethereum.Util.Sha3Keccack.Current.CalculateHash(canonicalSignature)[..8];
191+
function = contractRaw.GetFunctionBySignature(selector);
192+
}
193+
else
194+
{
195+
throw new ArgumentException("Method signature not found in contract ABI.");
196+
}
197+
}
198+
return (function.GetData(parameters), function);
199+
}
200+
213201
/// <summary>
214202
/// Gets a function matching the specified signature from the contract.
215203
/// </summary>

Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ public static class ThirdwebExtensions
99
{
1010
#region Common
1111

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

29+
/// <summary>
30+
/// Encodes the function call for the specified method and parameters.
31+
/// </summary>
32+
/// <param name="contract">The contract instance.</param>
33+
/// <param name="method">The method to call.</param>
34+
/// <param name="parameters">The parameters for the method.</param>
35+
/// <returns>The generated calldata.</returns>
36+
public static string CreateCallData(this ThirdwebContract contract, string method, params object[] parameters)
37+
{
38+
(var data, _) = ThirdwebContract.EncodeFunctionCall(contract, method, parameters);
39+
return data;
40+
}
41+
2242
/// <summary>
2343
/// Reads data from the contract using the specified method.
2444
/// </summary>

Thirdweb/Thirdweb.RPC/ThirdwebRPC.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public async Task<TResponse> SendRequestAsync<TResponse>(string method, params o
7777
{
7878
lock (this._cacheLock)
7979
{
80-
var cacheKey = GetCacheKey(method, parameters);
80+
var cacheKey = GetCacheKey(this._rpcUrl.ToString(), method, parameters);
8181
if (this._cache.TryGetValue(cacheKey, out var cachedItem) && (DateTime.Now - cachedItem.Timestamp) < this._cacheDuration)
8282
{
8383
if (cachedItem.Response is TResponse cachedResponse)
@@ -121,7 +121,7 @@ public async Task<TResponse> SendRequestAsync<TResponse>(string method, params o
121121
{
122122
lock (this._cacheLock)
123123
{
124-
var cacheKey = GetCacheKey(method, parameters);
124+
var cacheKey = GetCacheKey(this._rpcUrl.ToString(), method, parameters);
125125
this._cache[cacheKey] = (response, DateTime.Now);
126126
}
127127
return response;
@@ -133,7 +133,7 @@ public async Task<TResponse> SendRequestAsync<TResponse>(string method, params o
133133
var deserializedResponse = JsonConvert.DeserializeObject<TResponse>(JsonConvert.SerializeObject(result));
134134
lock (this._cacheLock)
135135
{
136-
var cacheKey = GetCacheKey(method, parameters);
136+
var cacheKey = GetCacheKey(this._rpcUrl.ToString(), method, parameters);
137137
this._cache[cacheKey] = (deserializedResponse, DateTime.Now);
138138
}
139139
return deserializedResponse;
@@ -238,10 +238,12 @@ private async Task SendBatchAsync(List<RpcRequest> batch)
238238
}
239239
}
240240

241-
private static string GetCacheKey(string method, params object[] parameters)
241+
private static string GetCacheKey(string rpcUrl, string method, params object[] parameters)
242242
{
243243
var keyBuilder = new StringBuilder();
244244

245+
_ = keyBuilder.Append(rpcUrl);
246+
245247
_ = keyBuilder.Append(method);
246248

247249
foreach (var param in parameters)

Thirdweb/Thirdweb.Utils/Constants.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public static class Constants
1616
public const string DEFAULT_FACTORY_ADDRESS_V06 = "0x85e23b94e7F5E9cC1fF78BCe78cfb15B81f0DF00";
1717
public const string DEFAULT_FACTORY_ADDRESS_V07 = "0x4bE0ddfebcA9A5A4a617dee4DeCe99E7c862dceb";
1818

19+
public const string EIP_1271_MAGIC_VALUE = "0x1626ba7e00000000000000000000000000000000000000000000000000000000";
20+
public const string ERC_6492_MAGIC_VALUE = "0x6492649264926492649264926492649264926492649264926492649264926492";
21+
public const string MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
22+
public const string MULTICALL3_ABI =
23+
/*lang=json,strict*/
24+
"[{\"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\"}]}]}]";
1925
public const string REDIRECT_HTML =
2026
"<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>";
2127

Thirdweb/Thirdweb.Utils/Utils.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Text;
55
using System.Text.RegularExpressions;
66
using ADRaffy.ENSNormalize;
7+
using Nethereum.ABI;
78
using Nethereum.ABI.EIP712;
89
using Nethereum.ABI.FunctionEncoding;
910
using Nethereum.ABI.FunctionEncoding.Attributes;
@@ -87,8 +88,7 @@ public static byte[] HashPrefixedMessage(this byte[] messageBytes)
8788
/// <returns>The hashed message.</returns>
8889
public static string HashPrefixedMessage(this string message)
8990
{
90-
var signer = new EthereumMessageSigner();
91-
return signer.HashPrefixedMessage(Encoding.UTF8.GetBytes(message)).ToHex(true);
91+
return HashPrefixedMessage(Encoding.UTF8.GetBytes(message)).BytesToHex();
9292
}
9393

9494
/// <summary>
@@ -1020,4 +1020,18 @@ static void StringifyLargeNumbers(JToken token)
10201020

10211021
return jObject.ToString();
10221022
}
1023+
1024+
/// <summary>
1025+
/// 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.
1026+
/// </summary>
1027+
/// <param name="address">The ERC-4337 Account Factory address</param>
1028+
/// <param name="data">Account deployment calldata (if not deployed) for counterfactual verification</param>
1029+
/// <param name="signature">The original signature</param>
1030+
/// <returns>The serialized signature hex string.</returns>
1031+
public static string SerializeErc6492Signature(string address, byte[] data, byte[] signature)
1032+
{
1033+
var encoder = new ABIEncode();
1034+
var encodedParams = encoder.GetABIEncoded(new ABIValue("address", address), new ABIValue("bytes", data), new ABIValue("bytes", signature));
1035+
return HexConcat(encodedParams.BytesToHex(), Constants.ERC_6492_MAGIC_VALUE);
1036+
}
10231037
}

0 commit comments

Comments
 (0)