Skip to main content

Nethereum.ABI

NuGet: Nethereum.ABI | Source: src/Nethereum.ABI/

Nethereum.ABI

Encoding and decoding of ABI Types, functions, events of Ethereum contracts

NuGet

Overview​

Nethereum.ABI is the core package for Ethereum's Application Binary Interface (ABI) encoding and decoding in .NET. It provides comprehensive support for encoding function calls, decoding function outputs, processing event logs, EIP-712 typed data signing, and handling all Solidity data types including complex structures like tuples and dynamic arrays.

This package is fundamental to all smart contract interactions in Nethereum, as it translates between .NET objects and the binary format Ethereum uses for contract communication.

Installation​

dotnet add package Nethereum.ABI

Dependencies​

Nethereum:

  • Nethereum.Hex - Hexadecimal encoding/decoding
  • Nethereum.Util - Keccak-256 hashing and utilities

Key Concepts​

Application Binary Interface (ABI)​

The ABI is a JSON specification that describes:

  • Functions: Input parameters, output parameters, and function signatures
  • Events: Indexed and non-indexed parameters for log filtering and decoding
  • Errors: Custom error types and their parameters
  • Types: Complete type system including elementary types, arrays, structs (tuples)

Function Encoding​

Function calls are encoded as:

  1. Function Selector: First 4 bytes of Keccak-256 hash of the function signature
  2. Encoded Parameters: ABI-encoded input parameters (32-byte aligned)

Event Decoding​

Events are stored in transaction logs with:

  • Topics: Up to 4 indexed parameters (including event signature hash)
  • Data: Non-indexed parameters (ABI-encoded)

EIP-712 Typed Data​

Structured data hashing and signing standard that enables:

  • Human-readable signatures: Users can see what they're signing
  • Domain separation: Prevents replay attacks across different dApps
  • Typed structs: Support for complex nested data structures
  • Used by: MetaMask, Permit (EIP-2612), Uniswap, OpenSea, and many more

Type System​

Nethereum.ABI supports all Solidity types:

  • Elementary: uint256, int256, address, bool, bytes, bytesN, string
  • Fixed Arrays: uint256[20], address[5]
  • Dynamic Arrays: uint256[], string[]
  • Tuples: Complex structs with nested components

Quick Start​

Basic Function Encoding​

using Nethereum.ABI.FunctionEncoding;
using Nethereum.ABI.Model;

// Create encoder
var functionCallEncoder = new FunctionCallEncoder();

// Define function signature and parameters
var sha3Signature = "c6888fa1"; // First 8 hex chars of Keccak-256("functionName(paramTypes)")
var parameters = new[]
{
new Parameter("address", "recipient"),
new Parameter("uint256", "amount")
};

// Encode function call
string encoded = functionCallEncoder.EncodeRequest(
sha3Signature,
parameters,
"0x1234567890abcdef1234567890abcdef12345678", // recipient
1000000000000000000 // amount (1 ETH in wei)
);
// Result: "0xc6888fa10000000000000000000000001234567890abcdef1234567890abcdef123456780000000000000000000000000000000000000000000000000de0b6b3a7640000"

Decoding Function Output​

// From test: FunctionEncodingTests.cs
var functionCallDecoder = new FunctionCallDecoder();

var outputParameters = new[]
{
new ParameterOutput()
{
Parameter = new Parameter("uint[]", "numbers")
{
DecodedType = typeof(List<int>)
}
}
};

var result = functionCallDecoder.DecodeOutput(
"0x0000000000000000000000000000000000000000000000000000000000000020" +
"0000000000000000000000000000000000000000000000000000000000000003" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000001" +
"0000000000000000000000000000000000000000000000000000000000000002",
outputParameters
);

var numbers = (List<int>)result[0].Result;
// numbers: [0, 1, 2]

Usage Examples​

Example 1: Encoding Multiple Types Including Dynamic Strings​

From test: FunctionEncodingTests.cs:125

using Nethereum.ABI.FunctionEncoding;
using Nethereum.ABI.Model;

var functionCallEncoder = new FunctionCallEncoder();
var sha3Signature = "c6888fa1";

var inputsParameters = new[]
{
new Parameter("string", "greeting"),
new Parameter("uint[20]", "numbers"),
new Parameter("string", "farewell")
};

var array = new uint[20];
for (uint i = 0; i < 20; i++)
array[i] = i + 234567;

string encoded = functionCallEncoder.EncodeRequest(
sha3Signature,
inputsParameters,
"hello", // Dynamic string (pointer to data)
array, // Fixed-size array (inline)
"world" // Dynamic string (pointer to data)
);

// Result starts with function selector, followed by:
// - Pointer to "hello" data
// - 20 uint256 values inline
// - Pointer to "world" data
// - Actual "hello" string data
// - Actual "world" string data

Example 2: Attribute-Based Function Encoding​

From test: FunctionAttributeEncodingTests.cs:55-61 (ShouldEncodeInt, backed by FunctionIntInput at line 96)

EncodeRequest<T>(T functionInput, string sha3Signature) takes the signature explicitly, so the input class only needs [Parameter] attributes — it does not need [Function] or a FunctionMessage base:

using Nethereum.ABI.FunctionEncoding;
using Nethereum.ABI.FunctionEncoding.Attributes;

public class MultiplyFunctionInput
{
[Parameter("uint256", "a", 1)]
public int A { get; set; }
}

var input = new MultiplyFunctionInput { A = 69 };
var encoder = new FunctionCallEncoder();
string encoded = encoder.EncodeRequest(input, "c6888fa1");

// Result: "0xc6888fa10000000000000000000000000000000000000000000000000000000000000045"
// 69 decimal = 0x45 hex, padded to 32 bytes

Example 3: Decoding Event Topics (Transfer Event)​

From test: EventTopicDecoderTests.cs:13

using Nethereum.ABI.FunctionEncoding;
using Nethereum.ABI.FunctionEncoding.Attributes;
using System.Numerics;

[Event("Transfer")]
public class TransferEvent
{
[Parameter("address", "_from", 1, indexed: true)]
public string From { get; set; }

[Parameter("address", "_to", 2, indexed: true)]
public string To { get; set; }

[Parameter("uint256", "_value", 3, indexed: true)]
public BigInteger Value { get; set; }
}

var topics = new[]
{
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", // Event signature
"0x0000000000000000000000000000000000000000000000000000000000000000", // from (zero address)
"0x000000000000000000000000c14934679e71ef4d18b6ae927fe2b953c7fd9b91", // to
"0x0000000000000000000000000000000000000000000000400000402000000001" // value
};

var data = "0x"; // No non-indexed data

var transferEvent = new TransferEvent();
new EventTopicDecoder().DecodeTopics(transferEvent, topics, data);

// transferEvent.From: "0x0000000000000000000000000000000000000000"
// transferEvent.To: "0xc14934679e71ef4d18b6ae927fe2b953c7fd9b91"
// transferEvent.Value: 1180591691223594434561

Example 4: Simple ABI Encoding with ABIEncode​

From test: AbiEncodeTests.cs:12

using Nethereum.ABI;
using Nethereum.Hex.HexConvertors.Extensions;

var abiEncode = new ABIEncode();

// Encode multiple values with explicit types
byte[] encoded = abiEncode.GetABIEncoded(
new ABIValue("string", "hello"),
new ABIValue("int", 69),
new ABIValue("string", "world")
);

string hexResult = encoded.ToHex(true);
// Result: "0x0000000000000000000000000000000000000000000000000000000000000060..."
// Includes pointers to dynamic data and the actual string data

Example 5: Encoding with Parameter Attributes​

From test: AbiEncodeTests.cs:34

using Nethereum.ABI;
using Nethereum.ABI.FunctionEncoding.Attributes;

public class TestParamsInput
{
[Parameter("string", 1)]
public string First { get; set; }

[Parameter("int256", 2)]
public int Second { get; set; }

[Parameter("string", 3)]
public string Third { get; set; }
}

var abiEncode = new ABIEncode();
var input = new TestParamsInput
{
First = "hello",
Second = 69,
Third = "world"
};

byte[] encoded = abiEncode.GetABIParamsEncoded(input);
// Automatically encodes based on Parameter attributes

Example 6: Deserializing Contract ABI JSON​

From test: FunctionAttributeEncodingTests.cs:14

using Nethereum.ABI.ABIDeserialisation;
using System.Linq;

var abi = @"[
{
""constant"": false,
""inputs"": [{""name"": ""a"", ""type"": ""uint256""}],
""name"": ""multiply"",
""outputs"": [{""name"": ""d"", ""type"": ""uint256""}],
""type"": ""function""
}
]";

var deserializer = new ABIJsonDeserialiser();
var contract = deserializer.DeserialiseContract(abi);

var multiplyFunction = contract.Functions.FirstOrDefault(x => x.Name == "multiply");
// multiplyFunction.Sha3Signature: "c6888fa1"
// multiplyFunction.Constant: false
// multiplyFunction.InputParameters[0].Type: "uint256"

Example 7: Complex Tuples with Nested Arrays​

From test: AbiDeserialiseTuplesTests.cs:22

using Nethereum.ABI.ABIDeserialisation;
using System.Linq;

// Complex ABI with nested tuple containing array of tuples
var abi = @"[{
""constant"": false,
""inputs"": [{
""components"": [
{""name"": ""id"", ""type"": ""uint256""},
{
""components"": [
{""name"": ""id"", ""type"": ""uint256""},
{""name"": ""productId"", ""type"": ""uint256""},
{""name"": ""quantity"", ""type"": ""uint256""}
],
""name"": ""lineItem"",
""type"": ""tuple[]""
},
{""name"": ""customerId"", ""type"": ""uint256""}
],
""name"": ""purchaseOrder"",
""type"": ""tuple""
}],
""name"": ""SetPurchaseOrder"",
""outputs"": [],
""type"": ""function""
}]";

var contractAbi = new ABIJsonDeserialiser().DeserialiseContract(abi);
var functionABI = contractAbi.Functions.FirstOrDefault(e => e.Name == "SetPurchaseOrder");

// Function signature includes full tuple structure
// functionABI.Sha3Signature: "0cc400bd"

Example 8: Encoding Individual Types​

From test: FunctionEncodingTests.cs:79-107

using Nethereum.ABI.FunctionEncoding;
using Nethereum.ABI.Model;

var encoder = new FunctionCallEncoder();
var signature = "c6888fa1";

// Address encoding
var addressParam = new[] { new Parameter("address", "recipient") };
string encodedAddress = encoder.EncodeRequest(
signature,
addressParam,
"0x1234567890abcdef1234567890abcdef12345678"
);
// Result: "0xc6888fa10000000000000000000000001234567890abcdef1234567890abcdef12345678"

// Boolean encoding
var boolParam = new[] { new Parameter("bool", "flag") };
string encodedBool = encoder.EncodeRequest(signature, boolParam, true);
// Result: "0xc6888fa10000000000000000000000000000000000000000000000000000000000000001"

// Integer encoding
var intParam = new[] { new Parameter("int", "number") };
string encodedInt = encoder.EncodeRequest(signature, intParam, 69);
// Result: "0xc6888fa10000000000000000000000000000000000000000000000000000000000000045"
// Note: 69 decimal = 0x45 hex

Example 9: EIP-712 Typed Data with Simple Structs​

From test: Eip712TypedDataSignerSimpleScenarioTest.cs:66

using Nethereum.ABI.EIP712;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Signer;
using Nethereum.Signer.EIP712;
using System.Collections.Generic;

// Define your domain-specific structs
[Struct("Person")]
public class Person
{
[Parameter("string", "name", 1)]
public string Name { get; set; }

[Parameter("address[]", "wallets", 2)]
public List<string> Wallets { get; set; }
}

[Struct("Mail")]
public class Mail
{
[Parameter("tuple", "from", 1, "Person")]
public Person From { get; set; }

[Parameter("tuple[]", "to", 2, "Person[]")]
public List<Person> To { get; set; }

[Parameter("string", "contents", 3)]
public string Contents { get; set; }
}

// Create typed data definition
var typedData = new TypedData<Domain>
{
Domain = new Domain
{
Name = "Ether Mail",
Version = "1",
ChainId = 1,
VerifyingContract = "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
Types = MemberDescriptionFactory.GetTypesMemberDescription(typeof(Domain), typeof(Mail), typeof(Person)),
PrimaryType = nameof(Mail),
};

// Create message
var mail = new Mail
{
From = new Person
{
Name = "Cow",
Wallets = new List<string>
{
"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"
}
},
To = new List<Person>
{
new Person
{
Name = "Bob",
Wallets = new List<string>
{
"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57",
"0xB0B0b0b0b0b0B000000000000000000000000000"
}
}
},
Contents = "Hello, Bob!"
};

// Set the message
typedData.SetMessage(mail);

// Sign using private key
var signer = new Eip712TypedDataSigner();
var key = new EthECKey("94e001d6adf3a3275d5dd45971c2a5f6637d3e9c51f9693f2e678f649e164fa5");
string signature = signer.SignTypedDataV4(mail, typedData, key);
// signature: "0x943393c998ab7e067d2875385e2218c9b3140f563694267ac9f6276a9fcc53e1..."

// Recover signer address from signature
string recoveredAddress = signer.RecoverFromSignatureV4(mail, typedData, signature);
// recoveredAddress matches key.GetPublicAddress()

Example 10: EIP-712 Typed Data from JSON​

From test: Eip712TypedDataSignerTest.cs:107

using Nethereum.ABI.EIP712;
using Nethereum.Signer;
using Nethereum.Signer.EIP712;

// EIP-712 typed data as JSON (MetaMask format)
var typedDataJson = @"{
'domain': {
'chainId': 1,
'name': 'Ether Mail',
'verifyingContract': '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
'version': '1'
},
'message': {
'contents': 'Hello, Bob!',
'from': {
'name': 'Cow',
'wallets': [
'0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
'0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF'
]
},
'to': [{
'name': 'Bob',
'wallets': [
'0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
'0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57',
'0xB0B0b0b0b0b0B000000000000000000000000000'
]
}]
},
'primaryType': 'Mail',
'types': {
'EIP712Domain': [
{'name': 'name', 'type': 'string'},
{'name': 'version', 'type': 'string'},
{'name': 'chainId', 'type': 'uint256'},
{'name': 'verifyingContract', 'type': 'address'}
],
'Mail': [
{'name': 'from', 'type': 'Person'},
{'name': 'to', 'type': 'Person[]'},
{'name': 'contents', 'type': 'string'}
],
'Person': [
{'name': 'name', 'type': 'string'},
{'name': 'wallets', 'type': 'address[]'}
]
}
}";

// Deserialize and encode
var rawTypedData = TypedDataRawJsonConversion.DeserialiseJsonToRawTypedData(typedDataJson);
var signer = new Eip712TypedDataSigner();
byte[] encodedTypedData = signer.EncodeTypedDataRaw(rawTypedData);

// Sign directly from JSON — there is no SignTypedDataV4(TypedDataRaw, EthECKey) overload;
// signing from a raw JSON payload goes through the string overload instead
var key = new EthECKey("94e001d6adf3a3275d5dd45971c2a5f6637d3e9c51f9693f2e678f649e164fa5");
string signature = signer.SignTypedDataV4(typedDataJson, key);

Example 11: EIP-712 with Complex Nested Structures​

From test: EIP712TypeDataSignatureMultipleComplexInnerObjects.cs:13

using Nethereum.ABI.EIP712;
using Nethereum.ABI.FunctionEncoding.Attributes;
using System.Collections.Generic;
using System.Numerics;

[Struct("UsageLimit")]
public class UsageLimit
{
[Parameter("uint8", "limitType", 1)]
public byte LimitType { get; set; }

[Parameter("uint256", "limit", 2)]
public BigInteger Limit { get; set; }

[Parameter("uint256", "period", 3)]
public BigInteger Period { get; set; }
}

[Struct("Constraint")]
public class Constraint
{
[Parameter("uint8", "condition", 1)]
public byte Condition { get; set; }

[Parameter("uint64", "index", 2)]
public ulong Index { get; set; }

[Parameter("bytes32", "refValue", 3)]
public byte[] RefValue { get; set; }
}

[Struct("CallSpec")]
public class CallSpec
{
[Parameter("address", "target", 1)]
public string Target { get; set; }

[Parameter("bytes4", "selector", 2)]
public byte[] Selector { get; set; }

[Parameter("uint256", "maxValuePerUse", 3)]
public BigInteger MaxValuePerUse { get; set; }

[Parameter("tuple", "valueLimit", 4, structTypeName: "UsageLimit")]
public UsageLimit ValueLimit { get; set; }

[Parameter("tuple[]", "constraints", 5, structTypeName: "Constraint[]")]
public List<Constraint> Constraints { get; set; }
}

[Struct("SessionSpec")]
public class SessionSpec
{
[Parameter("address", "signer", 1)]
public string Signer { get; set; }

[Parameter("uint256", "expiresAt", 2)]
public BigInteger ExpiresAt { get; set; }

[Parameter("tuple[]", "callPolicies", 3, structTypeName: "CallSpec[]")]
public List<CallSpec> CallPolicies { get; set; }
}

// This demonstrates deep nesting: SessionSpec contains array of CallSpec,
// each CallSpec contains UsageLimit struct and array of Constraint structs
// Perfect for complex DeFi protocols, account abstraction, session keys, etc.

Example 12: EIP-712 Encoding and Hashing​

From test: Eip712TypedDataEncoder.cs

using Nethereum.ABI.EIP712;
using Nethereum.Hex.HexConvertors.Extensions;

var encoder = new Eip712TypedDataEncoder();

// Encode from typed data
var mail = new Mail { /* ... */ };
var typedData = new TypedData<Domain> { /* ... */ };
byte[] encoded = encoder.EncodeTypedData(mail, typedData);

// Encode and hash in one operation (for signing)
byte[] hash = encoder.EncodeAndHashTypedData(mail, typedData);

// Encode directly from JSON
string json = /* EIP-712 JSON */;
byte[] encodedFromJson = encoder.EncodeTypedData(json);
byte[] hashFromJson = encoder.EncodeAndHashTypedData(json);

// The hash is what gets signed with ECDSA
string hashHex = hash.ToHex(true);

Example 13: Error Handling for Invalid Parameters​

From test: FunctionEncodingTests.cs:161

using Nethereum.ABI.FunctionEncoding;
using Nethereum.ABI.Model;

var encoder = new FunctionCallEncoder();
var signature = "c6888fa1";
var parameters = new[] { new Parameter("address", "_address1") };

try
{
string encoded = encoder.EncodeRequest(signature, parameters, (object)null);
}
catch (AbiEncodingException ex)
{
// ex.Message: "An error occurred encoding abi value. Order: '1', Type: 'address',
// Value: 'null'. Ensure the value is valid for the abi type."
Console.WriteLine(ex.Message);
}

Example 14: Uniswap Permit2 EIP-712 Types​

The Nethereum.ABI.EIP712.Permit2 namespace provides the Permit2 EIP-712 signing types and their TypedData definitions (Permit2TypedData). Signing itself is done by PermitSigner in Nethereum.Signer.EIP712; the on-chain call tuples live in Nethereum.Contracts.Standards.Permit2.

Signing types (each matches a Permit2 typehash):

  • TokenPermissions — token, amount
  • PermitDetails — token, amount (uint160), expiration/nonce (uint48)
  • PermitSingle / PermitBatch — AllowanceTransfer: details, spender, sigDeadline
  • PermitTransferFromWithSpender / PermitBatchTransferFromWithSpender — SignatureTransfer: permitted, spender, nonce, deadline
using Nethereum.ABI.EIP712.Permit2;

var typedData = Permit2TypedData.GetPermitTransferFromTypeDefinition(chainId, permit2Address);
// PrimaryType "PermitTransferFrom", domain name "Permit2"

Why a separate ...WithSpender type: for SignatureTransfer, Permit2 hashes the caller (msg.sender) into the typehash's spender field, so the signed message must include spender even though the on-chain PermitTransferFrom argument tuple (in Nethereum.Contracts.Standards.Permit2) does not. AllowanceTransfer's PermitSingle/PermitBatch carry spender as a real field, so they are used unchanged for both signing and the on-chain call.

API Reference​

Core Classes​

ABIEncode​

  • GetABIEncoded(params ABIValue[] abiValues) - Encode values with explicit types
  • GetABIEncoded(params object[] values) - Encode values with automatic type detection
  • GetABIParamsEncoded<T>(T input) - Encode object using Parameter attributes
  • GetABIEncodedPacked(params ABIValue[] abiValues) - Packed encoding (no padding)
  • GetSha3ABIEncoded(...) - Encode and hash in one operation

FunctionCallEncoder​

  • EncodeRequest(string sha3Signature, Parameter[] parameters, params object[] values) - Encode function call
  • EncodeRequest<T>(T functionInput, string sha3Signature) - Encode using attributes

FunctionCallDecoder​

  • DecodeOutput(string output, params ParameterOutput[] outputParameters) - Decode function return values
  • DecodeFunctionOutput<T>(string output) - Decode using attributes

EventTopicDecoder​

  • DecodeTopics<T>(T eventDTO, object[] topics, string data) - Decode event log into an existing typed object instance
  • DecodeTopics<T>(object[] topics, string data) - Decode event log into a new typed object (where T : new())

ABIJsonDeserialiser​

  • DeserialiseContract(string abi) - Parse contract ABI JSON
  • DeserialiseContract(JArray abi) - Parse from JArray
  • Produces ContractABI with Functions, Events, Errors, Constructor

Eip712TypedDataEncoder​

  • EncodeTypedData<T, TDomain>(T message, TypedData<TDomain> typedData) - Encode typed data with message
  • EncodeTypedData(string json) - Encode from EIP-712 JSON
  • EncodeAndHashTypedData(...) - Encode and hash for signing
  • EncodeTypedDataRaw(TypedDataRaw typedData) - Low-level encoding

Eip712TypedDataSigner (in Nethereum.Signer.EIP712)​

  • SignTypedDataV4<T, TDomain>(T message, TypedData<TDomain> typedData, EthECKey key) - Sign EIP-712 data
  • RecoverFromSignatureV4<T, TDomain>(T message, TypedData<TDomain> typedData, string signature) - Recover signer
  • SignTypedDataV4(string json, EthECKey key) - Sign directly from EIP-712 JSON

Encoding Attributes​

  • [Function("name")] - Mark class as function definition
  • [Event("name")] - Mark class as event definition
  • [Struct("name")] - Mark class as EIP-712 struct
  • [Parameter("type", "name", order, indexed)] - Mark property as parameter
  • [FunctionOutput] - Mark class can be used for output decoding

EIP-712 Classes​

  • TypedData<TDomain> - Typed data with domain separation
  • Domain - EIP-712 domain (name, version, chainId, verifyingContract); DomainWithSalt extends it with an optional salt field
  • MemberDescription - Type member definition (name, type)
  • MemberDescriptionFactory - Generate type descriptions from .NET types
  • MemberValue - Runtime value for encoding
  • TypedDataRaw - Raw typed data without generics

Example 15: Smart Contract Revert Error Handling​

using Nethereum.ABI.FunctionEncoding;

var decoder = new FunctionCallDecoder();

// Detect and decode standard revert errors (Error(string))
// The output data from a failed call starts with 0x08c379a0
if (ErrorFunction.IsErrorData(outputData))
{
ErrorFunction error = decoder.DecodeFunctionError(outputData);
Console.WriteLine(error.Message); // e.g. "Insufficient balance"
}

// ThrowIfErrorOnOutput checks the data and throws SmartContractRevertException
// if it contains a standard Error(string) revert
try
{
decoder.ThrowIfErrorOnOutput(outputData);
}
catch (SmartContractRevertException ex)
{
Console.WriteLine(ex.RevertMessage); // The revert reason string
Console.WriteLine(ex.EncodedData); // The raw hex data
}

Example 16: Custom Solidity Error Decoding​

using Nethereum.ABI.FunctionEncoding;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.ABI.Model;
using System.Numerics;

// Define a custom Solidity error: error InsufficientBalance(address account, uint256 balance)
[Error("InsufficientBalance")]
public class InsufficientBalanceError
{
[Parameter("address", "account", 1)]
public string Account { get; set; }

[Parameter("uint256", "balance", 2)]
public BigInteger Balance { get; set; }
}

var decoder = new FunctionCallDecoder();

// Decode custom error using the typed class
var errorData = "0x..."; // revert data from failed transaction
var decoded = (InsufficientBalanceError)decoder.DecodeError(
typeof(InsufficientBalanceError), errorData);
// decoded.Account: "0x1234..."
// decoded.Balance: 500

// Alternatively, decode using ErrorABI from a deserialized contract
var contract = new ABIJsonDeserialiser().DeserialiseContract(abiJson);
ErrorABI errorAbi = contract.FindErrorABI(errorSignature);
if (errorAbi != null)
{
var parameters = errorAbi.DecodeErrorDataToDefault(errorData);
// parameters[0].Result, parameters[1].Result, etc.
}

Decoding Untrusted ABI Data​

Decoders, the JSON ABI parser and the signature parser are safe to point at bytes or text from a node, a peer or a user.

  • Declared sizes are checked against the bytes that exist. An array that declares more elements than the remaining data can hold throws Exception("Insufficient data length to decode Array...") before anything is allocated (the size and offset arithmetic is done in 64 bits).
  • Head offsets are validated. An offset past the end of the data throws, and two head offsets that point at the same element throw Exception("Duplicate offset in encoded data..."). Each dynamic element is sliced from its offset to the next greater distinct offset, so decoding is linear in the data size. The same rules apply to arrays, tuples and ParameterDecoder.DecodeOutput.
  • Nesting is limited to 32 levels for signature strings (parentheses) and array types (brackets); beyond that ABIStringSignatureDeserialiser and ABIType.CreateABIType throw ArgumentException. A JSON ABI document may nest 68 JSON levels (2 * 32 + 4); beyond that ABIJsonDeserialiser throws a JsonException.
  • Signature and bytecode parsing is a linear scan. ABIStringSignatureDeserialiser and ByteCodeSwarmExtractor no longer use regular expressions, so a crafted signature or bytecode string cannot cause quadratic backtracking.
var tuple = new string('(', 40) + "uint256" + new string(')', 40);
Assert.Throws<ArgumentException>(() => new ABIStringSignatureDeserialiser().ExtractContractABI("function f(" + tuple + ")"));
Assert.Throws<ArgumentException>(() => ABIType.CreateABIType("uint256" + string.Concat(Enumerable.Repeat("[]", 40))));

var declaringTooMany = (Word(1 << 26) + Word(1)).HexToByteArray();
Assert.ThrowsAny<Exception>(() => ABIType.CreateABIType("uint256[]").Decode<List<BigInteger>>(declaringTooMany));

var sameOffsetTwice = (Word(2) + Word(0x40) + Word(0x40) + Word(1) + "61".PadRight(64, '0')).HexToByteArray();
Assert.ThrowsAny<Exception>(() => ABIType.CreateABIType("string[]").Decode<List<string>>(sameOffsetTwice));

Backed by FoundationsAbiDocExampleTests.Given_ASignatureOrArrayTypeNestedPastThirtyTwoLevels_When_Parsed_Then_ArgumentExceptionIsThrown, FoundationsAbiDocExampleTests.Given_AJsonAbiNestedFarPastTheLimit_When_Deserialised_Then_AJsonExceptionIsThrown, FoundationsAbiDocExampleTests.Given_AnArrayDeclaring67MillionElementsInTwoWords_When_Decoded_Then_TheDecoderRefusesBeforeAllocating, FoundationsAbiDocExampleTests.Given_ADynamicArrayWhoseTwoHeadOffsetsPointAtTheSameElement_When_Decoded_Then_TheDuplicateOffsetIsRefused and FoundationsAbiDocExampleTests.Given_AWellFormedStringArray_When_Decoded_Then_TheElementsAreReturned.

EIP-712 encoding rules​

Eip712TypedDataEncoder encodes by the declared types, as go-ethereum's apitypes does:

  • A type is a struct if and only if it is a key in types, so a struct named Point1 or Mybytes4 encodes as a struct. Dependencies are collected with a visited set, primary type first and the rest sorted by name, so mutually recursive types (legal under EIP-712) encode. Empty member names or types, a member that references its own type directly, and unknown member types throw ArgumentException.
  • Values must match the declared type: an address is 40 hex digits with or without 0x, or 20 bytes (AddressUtil.AddressEmptyAsHex, "0x0", is the zero address); bytesN is exactly N bytes from 0x-prefixed even-length hex or a byte array; bytes is hex or bytes, never null; intN / uintN accept decimal or 0x hex strings, integral numbers or CLR integers and are range-checked for the declared width (ArgumentException when out of range); bool must be a bool.
  • Message and domain keys that the types do not declare are ignored, as MetaMask's eth-sig-util V4 does; a declared field that is missing throws ArgumentException.
  • Encoding does not write to the TypedData<TDomain> argument, and the domain is rebuilt from Domain on every encode: a domain changed after the first encode is the domain signed.
  • TypedDataRaw.GetChainIdFromDomain() reads long, int, BigInteger and decimal or 0x-hex string chain ids, and returns null when the domain declares none.

where json declares Person with a friends member of type Person[]:

var encoded = Eip712TypedDataSigner.Current.EncodeTypedData(json);

var raw = TypedDataRawJsonConversion.DeserialiseJsonToRawTypedData(jsonWithChainId);
BigInteger? chainId = raw.GetChainIdFromDomain();

Backed by FoundationsEip712DocExampleTests.Given_ATypeThatReferencesItselfThroughAnArray_When_Encoded_Then_TheHashIsStable, FoundationsEip712DocExampleTests.Given_ATypeThatReferencesItselfDirectlyOrAnUnknownType_When_Encoded_Then_ArgumentExceptionIsThrown, FoundationsEip712DocExampleTests.Given_AnAddressWithAndWithoutThePrefixAndAnOutOfRangeInteger_When_Encoded_Then_TheAddressFormsMatchAndTheIntegerIsRefused, FoundationsEip712DocExampleTests.Given_AMessageWithAnExtraKeyAndAMessageMissingADeclaredKey_When_Encoded_Then_TheExtraKeyIsIgnoredAndTheMissingKeyIsRefused, FoundationsEip712DocExampleTests.Given_DomainsWithNumberDecimalAndHexChainIds_When_TheChainIdIsRead_Then_EachFormGivesTheSameChainId and Eip712DomainChangedAfterEncodeTests.Given_a_signed_typed_data_When_a_new_Domain_is_assigned_and_signed_again_Then_the_signature_is_for_the_new_domain (in Nethereum.Signer.UnitTests).

  • Nethereum.Hex - Hexadecimal encoding used throughout ABI operations
  • Nethereum.Util - Keccak-256 hashing for function/event signatures
  • Nethereum.Signer - EIP-712 signing and signature recovery
  • Nethereum.Contracts - High-level contract interaction built on ABI encoding
  • Nethereum.RPC - JSON-RPC calls that use ABI-encoded data

Important Notes​

Function Signature Calculation​

Function signatures are the first 4 bytes of Keccak-256 hash of the canonical function signature:

Keccak256("transfer(address,uint256)") → 0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
Function selector: 0xa9059cbb (first 4 bytes)

Event Signature Calculation​

Event signatures are the full 32 bytes of Keccak-256 hash of the canonical event signature:

Keccak256("Transfer(address,address,uint256)") → 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
Event topic[0]: Full hash (identifies the event)

EIP-712 Domain Separation​

The domain separator prevents signature replay attacks across:

  • Different contracts (via verifyingContract address)
  • Different chains (via chainId)
  • Different versions (via version)
  • Different applications (via name)

Formula: Keccak256(encodeData("EIP712Domain", domain))

EIP-712 Structured Data Hash​

Final hash signed by user:

Keccak256("\x19\x01" + domainSeparator + hashStruct(message))

Where:

  • \x19\x01 is the version byte for structured data
  • domainSeparator is the hash of the domain
  • hashStruct(message) is the hash of the primary message type

Dynamic vs Static Types​

  • Static types (uint256, address, bool, bytesN, fixed arrays): Encoded inline
  • Dynamic types (string, bytes, dynamic arrays, tuples with dynamic members): Encoded as pointer + data

Type Canonicalization​

When calculating signatures, types must be canonical:

  • uint → uint256
  • int → int256
  • No spaces: transfer(address,uint256) not transfer(address, uint256)

Indexed Event Parameters​

  • Maximum 3 indexed parameters per event (topic[0] is always event signature)
  • Indexed parameters are searchable/filterable in logs
  • Non-indexed parameters are cheaper and stored in data field

EIP-712 Use Cases​

  • MetaMask Sign-In: Authenticate users without gas
  • EIP-2612 Permit: Gasless token approvals (USDC, DAI, UNI)
  • OpenSea/NFT Marketplaces: Off-chain order signing
  • Uniswap Permit2: Advanced approval management
  • Account Abstraction: Session keys and delegated operations
  • DAO Voting: Off-chain vote collection with on-chain execution

Playground Examples​

Runnable examples available at Nethereum Playground:

Human-Readable ABI:

  • Example 1069 - Deployment, calls, and transactions using human-readable ABI format

ABI Encoding:

Resources​