Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 3 additions & 1 deletion src/abi_decode.zig
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ fn decodeValuesAt(data: []const u8, base: usize, types: []const AbiType, allocat
}

var result = try allocator.alloc(AbiValue, n);
var decoded_count: usize = 0;
errdefer {
for (result[0..n]) |*val| {
for (result[0..decoded_count]) |*val| {
freeValue(val, allocator);
}
allocator.free(result);
Expand All @@ -106,6 +107,7 @@ fn decodeValuesAt(data: []const u8, base: usize, types: []const AbiType, allocat
} else {
result[i] = try decodeStaticValue(data[head_offset..][0..32], abi_type, allocator);
}
decoded_count += 1;
}

return result;
Expand Down
74 changes: 14 additions & 60 deletions src/abi_encode.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const std = @import("std");
const uint256_mod = @import("uint256.zig");

const max_tuple_values = 32;

/// Tagged union representing any ABI-encodable value.
pub const AbiValue = union(enum) {
/// Unsigned 256-bit integer (covers uint8 through uint256).
Expand Down Expand Up @@ -54,11 +56,13 @@ pub const AbiValue = union(enum) {
/// Errors during ABI encoding.
pub const EncodeError = error{
OutOfMemory,
TooManyValues,
};

/// Encode a slice of ABI values according to the Solidity ABI specification.
/// Returns the encoded bytes. Caller owns the returned memory.
pub fn encodeValues(allocator: std.mem.Allocator, values: []const AbiValue) EncodeError![]u8 {
if (values.len > max_tuple_values) return error.TooManyValues;
const total = calcEncodedSize(values);
Comment on lines 64 to 66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

The 32-value limit has to recurse into nested collections.

Lines 65 and 76 only cap the outer argument list. A single top-level .array, .tuple, or .fixed_array with 33 children still reaches the fixed-size offsets buffers at Lines 140, 250, and 306, so the encoder will fail inside the new stack-based path instead of returning error.TooManyValues.

Suggested fix
+fn validateValueArity(values: []const AbiValue) EncodeError!void {
+    if (values.len > max_tuple_values) return error.TooManyValues;
+    for (values) |value| switch (value) {
+        .array, .fixed_array, .tuple => |items| try validateValueArity(items),
+        else => {},
+    };
+}
+
 pub fn encodeValues(allocator: std.mem.Allocator, values: []const AbiValue) EncodeError![]u8 {
-    if (values.len > max_tuple_values) return error.TooManyValues;
+    try validateValueArity(values);
     const total = calcEncodedSize(values);
     const buf = try allocator.alloc(u8, total);
@@
 pub fn encodeFunctionCall(allocator: std.mem.Allocator, selector: [4]u8, values: []const AbiValue) EncodeError![]u8 {
-    if (values.len > max_tuple_values) return error.TooManyValues;
+    try validateValueArity(values);
     const total = 4 + calcEncodedSize(values);
     const buf = try allocator.alloc(u8, total);

Also applies to: 75-77, 139-140, 249-250, 305-306

const buf = try allocator.alloc(u8, total);
errdefer allocator.free(buf);
Expand All @@ -69,6 +73,7 @@ pub fn encodeValues(allocator: std.mem.Allocator, values: []const AbiValue) Enco
/// Encode a function call: 4-byte selector followed by ABI-encoded arguments.
/// Returns the encoded bytes. Caller owns the returned memory.
pub fn encodeFunctionCall(allocator: std.mem.Allocator, selector: [4]u8, values: []const AbiValue) EncodeError![]u8 {
if (values.len > max_tuple_values) return error.TooManyValues;
const total = 4 + calcEncodedSize(values);
const buf = try allocator.alloc(u8, total);
errdefer allocator.free(buf);
Expand Down Expand Up @@ -131,7 +136,8 @@ fn encodeValuesInto(allocator: std.mem.Allocator, buf: *std.ArrayList(u8), value

// First pass: calculate tail offsets for dynamic values
// and pre-compute the offset each dynamic value will be at
var offsets: [32]usize = undefined; // max 32 values in a single tuple
std.debug.assert(values.len <= max_tuple_values);
var offsets: [max_tuple_values]usize = undefined;
for (values, 0..) |val, i| {
if (val.isDynamic()) {
offsets[i] = tail_offset;
Expand All @@ -151,7 +157,7 @@ fn encodeValuesInto(allocator: std.mem.Allocator, buf: *std.ArrayList(u8), value
// Third pass: write tail section directly into buf (no temp allocations)
for (values) |val| {
if (val.isDynamic()) {
encodeDynamicValueInto(allocator, buf, val);
encodeDynamicValueInto(buf, val);
}
}
}
Expand Down Expand Up @@ -202,49 +208,8 @@ fn encodeStaticValueNoAlloc(buf: *std.ArrayList(u8), val: AbiValue) void {
}
}

/// Encode a static value directly as a 32-byte word (allocating variant for backward compat).
fn encodeStaticValue(allocator: std.mem.Allocator, buf: *std.ArrayList(u8), val: AbiValue) EncodeError!void {
switch (val) {
.uint256 => |v| {
try writeUint256(allocator, buf, v);
},
.int256 => |v| {
try writeInt256(allocator, buf, v);
},
.address => |v| {
var word: [32]u8 = [_]u8{0} ** 32;
@memcpy(word[12..32], &v);
try buf.appendSlice(allocator, &word);
},
.boolean => |v| {
var word: [32]u8 = [_]u8{0} ** 32;
if (v) word[31] = 1;
try buf.appendSlice(allocator, &word);
},
.fixed_bytes => |v| {
var word: [32]u8 = [_]u8{0} ** 32;
const size: usize = @intCast(v.len);
@memcpy(word[0..size], v.data[0..size]);
try buf.appendSlice(allocator, &word);
},
.fixed_array => |items| {
for (items) |item| {
try encodeStaticValue(allocator, buf, item);
}
},
.tuple => |items| {
for (items) |item| {
try encodeStaticValue(allocator, buf, item);
}
},
else => unreachable,
}
}

/// Encode a dynamic value directly into the output buffer (no temp allocation).
fn encodeDynamicValueInto(allocator: std.mem.Allocator, buf: *std.ArrayList(u8), val: AbiValue) void {
_ = allocator;

fn encodeDynamicValueInto(buf: *std.ArrayList(u8), val: AbiValue) void {
switch (val) {
.bytes => |data| {
writeUint256NoAlloc(buf, @intCast(data.len));
Expand Down Expand Up @@ -281,7 +246,8 @@ fn encodeValuesIntoNoAlloc(buf: *std.ArrayList(u8), values: []const AbiValue) vo
var tail_offset: usize = head_size;

// Calculate offsets for dynamic values
var offsets: [32]usize = undefined;
std.debug.assert(values.len <= max_tuple_values);
var offsets: [max_tuple_values]usize = undefined;
for (values, 0..) |val, i| {
if (val.isDynamic()) {
offsets[i] = tail_offset;
Expand All @@ -301,7 +267,7 @@ fn encodeValuesIntoNoAlloc(buf: *std.ArrayList(u8), values: []const AbiValue) vo
// Write tails
for (values) |val| {
if (val.isDynamic()) {
encodeDynamicValueInto(undefined, buf, val);
encodeDynamicValueInto(buf, val);
}
}
}
Expand Down Expand Up @@ -336,7 +302,8 @@ fn writeValuesDirect(buf: []u8, values: []const AbiValue) void {
}
var tail_offset: usize = head_size;

var offsets: [32]usize = undefined;
std.debug.assert(values.len <= max_tuple_values);
var offsets: [max_tuple_values]usize = undefined;
for (values, 0..) |val, i| {
if (val.isDynamic()) {
offsets[i] = tail_offset;
Expand Down Expand Up @@ -421,19 +388,6 @@ fn writeDynamicValueDirect(buf: []u8, val: AbiValue) usize {
}
}

/// Write a u256 as a big-endian 32-byte word.
fn writeUint256(allocator: std.mem.Allocator, buf: *std.ArrayList(u8), value: u256) EncodeError!void {
const bytes = uint256_mod.toBigEndianBytes(value);
try buf.appendSlice(allocator, &bytes);
}

/// Write an i256 as a big-endian 32-byte two's complement word.
fn writeInt256(allocator: std.mem.Allocator, buf: *std.ArrayList(u8), value: i256) EncodeError!void {
// Two's complement: cast to u256 bit pattern, then write as big-endian.
const unsigned: u256 = @bitCast(value);
try writeUint256(allocator, buf, unsigned);
}

// ============================================================================
// Tests
// ============================================================================
Expand Down
11 changes: 8 additions & 3 deletions src/abi_json.zig
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ fn parseParam(allocator: std.mem.Allocator, obj: std.json.ObjectMap) !AbiParam {
const name_str = jsonGetString(obj, "name") orelse "";
const indexed = jsonGetBool(obj, "indexed") orelse false;

const abi_type = parseType(type_str);
const abi_type = parseType(type_str) orelse return error.UnknownType;

const name = if (name_str.len > 0) try allocator.dupe(u8, name_str) else name_str;
errdefer if (name.len > 0) allocator.free(name);
Expand All @@ -214,7 +214,7 @@ fn parseMutability(obj: std.json.ObjectMap) StateMutability {
}

/// Parse a Solidity type string into an AbiType.
pub fn parseType(type_str: []const u8) AbiType {
pub fn parseType(type_str: []const u8) ?AbiType {
// Handle array suffixes
if (std.mem.endsWith(u8, type_str, "[]")) return .dynamic_array;

Expand Down Expand Up @@ -243,7 +243,7 @@ pub fn parseType(type_str: []const u8) AbiType {
return parseBytesType(type_str) orelse .bytes;
}

return .uint256; // fallback
return null; // unknown type
}

fn parseUintType(type_str: []const u8) ?AbiType {
Expand Down Expand Up @@ -413,6 +413,11 @@ test "parseType - int without bits defaults to int256" {
try std.testing.expectEqual(AbiType.int256, parseType("int"));
}

test "parseType - unknown type returns null" {
try std.testing.expectEqual(@as(?AbiType, null), parseType("foobar"));
try std.testing.expectEqual(@as(?AbiType, null), parseType("custom_type"));
}

test "ContractAbi.fromJson - ERC20 ABI" {
const allocator = std.testing.allocator;
const json =
Expand Down
8 changes: 4 additions & 4 deletions src/chains/chain.zig
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ pub const Chain = struct {
testnet: bool = false,
};

/// Parse a hex address string into a 20-byte address.
/// Works at both comptime and runtime.
pub fn addressFromHex(hex_str: []const u8) Address {
return hex_mod.hexToBytesFixed(20, hex_str) catch unreachable;
/// Parse a hex address string into a 20-byte address at comptime.
/// Compile error if the hex string is invalid.
pub fn addressFromHex(comptime hex_str: []const u8) Address {
return comptime hex_mod.hexToBytesFixed(20, hex_str) catch @compileError("invalid hex address: " ++ hex_str);
}

/// Look up a chain by ID.
Expand Down
4 changes: 2 additions & 2 deletions src/dex/router.zig
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ pub fn findArbOpportunity(hops: []const Pool, max_input: u256) ?ArbOpportunity {
fn quotePool(amount_in: u256, pool: Pool) ?u256 {
switch (pool) {
.v2 => |p| {
const result = v2.getAmountOut(amount_in, p.reserve_in, p.reserve_out, p.fee_numerator, p.fee_denominator);
const result = v2.getAmountOut(amount_in, p.reserve_in, p.reserve_out, p.fee_numerator, p.fee_denominator) orelse return null;
return if (result == 0) null else result;
},
.v3 => |p| {
Expand Down Expand Up @@ -161,7 +161,7 @@ test "quoteExactInput V2 single hop" {
try std.testing.expect(result != null);

// Should match direct V2 calculation
const direct = v2.getAmountOut(1_000_000_000_000_000_000, 100_000_000_000_000_000_000, 200_000_000_000, 997, 1000);
const direct = v2.getAmountOut(1_000_000_000_000_000_000, 100_000_000_000_000_000_000, 200_000_000_000, 997, 1000).?;
try std.testing.expectEqual(direct, result.?);
}

Expand Down
28 changes: 14 additions & 14 deletions src/dex/v2.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ pub const Pair = struct {

/// Compute UniswapV2 getAmountOut with configurable fee, entirely in u64-limb space.
/// Formula: (amountIn * feeNum * reserveOut) / (reserveIn * feeDenom + amountIn * feeNum)
pub fn getAmountOut(amount_in: u256, reserve_in: u256, reserve_out: u256, fee_numerator: u64, fee_denominator: u64) u256 {
pub fn getAmountOut(amount_in: u256, reserve_in: u256, reserve_out: u256, fee_numerator: u64, fee_denominator: u64) ?u256 {
if (amount_in == 0) return 0;
if (reserve_in == 0 or reserve_out == 0) return 0;
if (fee_denominator == 0) return 0;
if (fee_denominator == 0) return null;

const ai = u256ToLimbs(amount_in);
const ri = u256ToLimbs(reserve_in);
Expand All @@ -39,7 +39,7 @@ pub fn getAmountOut(amount_in: u256, reserve_in: u256, reserve_out: u256, fee_nu
const denominator = addLimbs(mulLimbScalar(ri, fee_denominator), amount_in_with_fee);

if (denominator[0] == 0 and denominator[1] == 0 and denominator[2] == 0 and denominator[3] == 0) {
@panic("getAmountOut: denominator is zero (invalid reserves)");
return null;
}

return limbsToU256(divLimbsDirect(numerator, denominator));
Expand Down Expand Up @@ -69,7 +69,7 @@ pub fn getAmountIn(amount_out: u256, reserve_in: u256, reserve_out: u256, fee_nu
const denominator = mulLimbScalar(rd, fee_numerator);

if (denominator[0] == 0 and denominator[1] == 0 and denominator[2] == 0 and denominator[3] == 0) {
@panic("getAmountIn: denominator is zero");
return null;
}

// Uniswap V2 always adds 1 (ceiling)
Expand All @@ -88,7 +88,7 @@ pub fn getAmountsOut(amount_in: u256, path: []const Pair) ?u256 {

var current = amount_in;
for (path) |pair| {
current = getAmountOut(current, pair.reserve_in, pair.reserve_out, pair.fee_numerator, pair.fee_denominator);
current = getAmountOut(current, pair.reserve_in, pair.reserve_out, pair.fee_numerator, pair.fee_denominator) orelse return null;
if (current == 0) return null;
}
return current;
Expand Down Expand Up @@ -124,14 +124,14 @@ pub fn calculateProfit(amount_in: u256, path: []const Pair) ?u256 {
test "getAmountOut known value" {
// 1 ETH in, 100 ETH / 200k USDC pool, 0.3% fee
// Expected: (1e18 * 997 * 200e9) / (100e18 * 1000 + 1e18 * 997) = 1_974_316_068
const v2_result = getAmountOut(1_000_000_000_000_000_000, 100_000_000_000_000_000_000, 200_000_000_000, 997, 1000);
const v2_result = getAmountOut(1_000_000_000_000_000_000, 100_000_000_000_000_000_000, 200_000_000_000, 997, 1000).?;
try std.testing.expectEqual(@as(u256, 1_974_316_068), v2_result);
}

test "getAmountOut zero reserves" {
try std.testing.expectEqual(@as(u256, 0), getAmountOut(1000, 0, 200_000, 997, 1000));
try std.testing.expectEqual(@as(u256, 0), getAmountOut(1000, 100_000, 0, 997, 1000));
try std.testing.expectEqual(@as(u256, 0), getAmountOut(1000, 100_000, 200_000, 997, 0));
try std.testing.expectEqual(@as(?u256, 0), getAmountOut(1000, 0, 200_000, 997, 1000));
try std.testing.expectEqual(@as(?u256, 0), getAmountOut(1000, 100_000, 0, 997, 1000));
try std.testing.expectEqual(@as(?u256, null), getAmountOut(1000, 100_000, 200_000, 997, 0));
}

test "getAmountOut different fees" {
Expand All @@ -140,16 +140,16 @@ test "getAmountOut different fees" {
const reserve_out: u256 = 200_000_000_000;

// PancakeSwap uses 9975/10000 (0.25% fee) vs Uniswap 997/1000 (0.3% fee)
const pancake = getAmountOut(amount_in, reserve_in, reserve_out, 9975, 10000);
const uniswap = getAmountOut(amount_in, reserve_in, reserve_out, 997, 1000);
const pancake = getAmountOut(amount_in, reserve_in, reserve_out, 9975, 10000).?;
const uniswap = getAmountOut(amount_in, reserve_in, reserve_out, 997, 1000).?;

// Lower fee => more output
try std.testing.expect(pancake > uniswap);
}

test "getAmountOut zero input" {
const result = getAmountOut(0, 100_000, 200_000, 997, 1000);
try std.testing.expectEqual(@as(u256, 0), result);
try std.testing.expectEqual(@as(?u256, 0), result);
}

test "getAmountOut result less than reserve" {
Expand All @@ -158,7 +158,7 @@ test "getAmountOut result less than reserve" {
const reserve_out: u256 = 200_000_000_000;

for (amounts) |amount_in| {
const result = getAmountOut(amount_in, reserve_in, reserve_out, 997, 1000);
const result = getAmountOut(amount_in, reserve_in, reserve_out, 997, 1000).?;
try std.testing.expect(result < reserve_out);
}
}
Expand All @@ -168,7 +168,7 @@ test "getAmountIn inverse" {
const reserve_in: u256 = 100_000_000_000_000_000_000;
const reserve_out: u256 = 200_000_000_000;

const output = getAmountOut(amount_in, reserve_in, reserve_out, 997, 1000);
const output = getAmountOut(amount_in, reserve_in, reserve_out, 997, 1000).?;
const recovered_input = getAmountIn(output, reserve_in, reserve_out, 997, 1000) orelse unreachable;

// Due to ceiling division (+1), recovered_input >= amount_in
Expand Down
21 changes: 13 additions & 8 deletions src/eip155.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ pub fn applyEip155(v: u8, chain_id: u64) u256 {

/// Extract the recovery ID (0 or 1) from an EIP-155 encoded v value.
/// Reverses the formula: recovery_id = v - chain_id * 2 - 35
pub fn recoverFromEip155V(v: u256, chain_id: u64) u8 {
pub fn recoverFromEip155V(v: u256, chain_id: u64) ?u8 {
const base: u256 = @as(u256, chain_id) * 2 + 35;
if (v < base) return 0;
if (v < base) return null;
const recovery_id = v - base;
if (recovery_id > 1) return 0;
if (recovery_id > 1) return null;
return @intCast(recovery_id);
}

Expand Down Expand Up @@ -86,21 +86,26 @@ test "applyEip155 - Arbitrum (chain_id=42161)" {
}

test "recoverFromEip155V - Ethereum mainnet" {
try std.testing.expectEqual(@as(u8, 0), recoverFromEip155V(37, 1));
try std.testing.expectEqual(@as(u8, 1), recoverFromEip155V(38, 1));
try std.testing.expectEqual(@as(?u8, 0), recoverFromEip155V(37, 1));
try std.testing.expectEqual(@as(?u8, 1), recoverFromEip155V(38, 1));
}

test "recoverFromEip155V - BSC" {
try std.testing.expectEqual(@as(u8, 0), recoverFromEip155V(147, 56));
try std.testing.expectEqual(@as(u8, 1), recoverFromEip155V(148, 56));
try std.testing.expectEqual(@as(?u8, 0), recoverFromEip155V(147, 56));
try std.testing.expectEqual(@as(?u8, 1), recoverFromEip155V(148, 56));
}

test "recoverFromEip155V - invalid v returns null" {
try std.testing.expectEqual(@as(?u8, null), recoverFromEip155V(5, 1));
try std.testing.expectEqual(@as(?u8, null), recoverFromEip155V(100, 1));
}

test "recoverFromEip155V roundtrip" {
const chain_ids = [_]u64{ 1, 56, 137, 42161, 10, 8453 };
for (chain_ids) |chain_id| {
for ([_]u8{ 0, 1 }) |recovery_id| {
const eip155_v = applyEip155(recovery_id, chain_id);
const recovered = recoverFromEip155V(eip155_v, chain_id);
const recovered = recoverFromEip155V(eip155_v, chain_id).?;
try std.testing.expectEqual(recovery_id, recovered);
}
}
Expand Down
Loading
Loading