Skip to content

Commit 130e719

Browse files
authored
fix: constant substitution should not fail when referencing builtin functions (#127)
1 parent cdddf5e commit 130e719

File tree

7 files changed

+409
-112
lines changed

7 files changed

+409
-112
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
## Unreleased
66
- Add compile-time if/else if/else statements.
77
- Example: `if ([MODE] == 0x01) { 0xAA } else if ([MODE] == 0x02) { 0xBB } else { 0xCC }`
8+
- Fix constant substitution failing when referencing builtin functions (fixes #122).
9+
- Example: `#define constant C1 = __RIGHTPAD(0x)` and `#define constant C2 = [C1]` now works correctly.
10+
- Applies to all builtin functions: `__FUNC_SIG`, `__EVENT_HASH`, `__RIGHTPAD`, `__LEFTPAD`, `__BYTES`.
811

912
## [1.5.1] - 2025-11-04
1013
- Throw error for circular constant dependencies to prevent infinite loops during constant evaluation.

crates/codegen/src/irgen/builtin_function.rs

Lines changed: 7 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::Codegen;
22
use crate::irgen::constants::{constant_gen, evaluate_constant_value};
33
use alloy_primitives::{B256, hex, keccak256};
4+
use huff_neo_utils::builtin_eval::{PadDirection, eval_builtin_bytes, eval_event_hash, eval_function_signature};
45
use huff_neo_utils::bytecode::{BytecodeRes, Bytes, CircularCodeSizeIndices, Jump, Jumps};
56
use huff_neo_utils::bytes_util::{bytes32_to_hex_string, format_even_bytes, pad_n_bytes};
67
use huff_neo_utils::error::{CodegenError, CodegenErrorKind};
@@ -10,7 +11,6 @@ use huff_neo_utils::prelude::{
1011
Argument, AstSpan, BuiltinFunctionArg, BuiltinFunctionCall, BuiltinFunctionKind, Contract, MacroDefinition, MacroInvocation, PushValue,
1112
TableDefinition,
1213
};
13-
use std::fmt::Display;
1414

1515
/// Creates a CodegenError for invalid arguments
1616
fn invalid_arguments_error(msg: impl Into<String>, span: &AstSpan) -> CodegenError {
@@ -139,13 +139,13 @@ pub fn builtin_function_gen<'a>(
139139
}
140140
}
141141
BuiltinFunctionKind::FunctionSignature => {
142-
let push_value = function_signature(contract, bf)?;
142+
let push_value = eval_function_signature(contract, bf)?;
143143
let push_bytes = push_value.to_hex_with_opcode(evm_version);
144144
*offset += push_bytes.len() / 2;
145145
bytes.push((starting_offset, Bytes(push_bytes)));
146146
}
147147
BuiltinFunctionKind::EventHash => {
148-
let push_value = event_hash(contract, bf)?;
148+
let push_value = eval_event_hash(contract, bf)?;
149149
let push_bytes = push_value.to_hex_with_opcode(evm_version);
150150
*offset += push_bytes.len() / 2;
151151
bytes.push((starting_offset, Bytes(push_bytes)));
@@ -231,7 +231,7 @@ pub fn builtin_function_gen<'a>(
231231
bytes.push((starting_offset, Bytes(push_bytes)));
232232
}
233233
BuiltinFunctionKind::Bytes => {
234-
let push_value = builtin_bytes(bf)?;
234+
let push_value = eval_builtin_bytes(bf)?;
235235
let push_bytes = push_value.to_hex_with_opcode(evm_version);
236236
*offset += push_bytes.len() / 2;
237237
bytes.push((starting_offset, Bytes(push_bytes)));
@@ -440,86 +440,15 @@ pub fn tablesize(contract: &Contract, bf: &BuiltinFunctionCall) -> Result<(Table
440440
Ok((ir_table, push_bytes))
441441
}
442442

443-
/// Generates a PushValue for the __EVENT_HASH builtin function
444-
///
445-
/// Returns a PushValue containing the full keccak256 hash (32 bytes) of the event signature.
446-
/// Looks up event definitions in the contract or computes the hash via keccak256.
447-
pub fn event_hash(contract: &Contract, bf: &BuiltinFunctionCall) -> Result<PushValue, CodegenError> {
448-
validate_arg_count(bf, 1, "__EVENT_HASH")?;
449-
let first_arg = extract_single_argument(bf, "__EVENT_HASH")?;
450-
let hash = if let Some(event) = contract.events.iter().find(|e| first_arg.name.as_ref().unwrap().eq(&e.name)) {
451-
event.hash
452-
} else if let Some(s) = &first_arg.name {
453-
keccak256(s).0
454-
} else {
455-
tracing::error!(
456-
target: "codegen",
457-
"MISSING EVENT INTERFACE PASSED TO __EVENT_HASH: \"{}\"",
458-
first_arg.name.as_ref().unwrap()
459-
);
460-
return Err(CodegenError {
461-
kind: CodegenErrorKind::MissingEventInterface(first_arg.name.as_ref().unwrap().to_string()),
462-
span: bf.span.clone_box(),
463-
token: None,
464-
});
465-
};
466-
Ok(PushValue::from(hash))
467-
}
468-
469-
/// Generates a PushValue for the __FUNC_SIG builtin function
470-
///
471-
/// Returns a PushValue containing the 4-byte function selector (first 4 bytes of keccak256 hash).
472-
/// Looks up function/error definitions in the contract or computes the selector via keccak256.
473-
pub fn function_signature(contract: &Contract, bf: &BuiltinFunctionCall) -> Result<PushValue, CodegenError> {
474-
validate_arg_count(bf, 1, "__FUNC_SIG")?;
475-
let first_arg = extract_single_argument(bf, "__FUNC_SIG")?;
476-
let selector = if let Some(func) = contract.functions.iter().find(|f| first_arg.name.as_ref().unwrap().eq(&f.name)) {
477-
func.signature
478-
} else if let Some(error) = contract.errors.iter().find(|e| first_arg.name.as_ref().unwrap().eq(&e.name)) {
479-
error.selector
480-
} else if let Some(s) = &first_arg.name {
481-
keccak256(s)[..4].try_into().unwrap()
482-
} else {
483-
tracing::error!(
484-
target: "codegen",
485-
"MISSING FUNCTION INTERFACE PASSED TO __SIG: \"{}\"",
486-
first_arg.name.as_ref().unwrap()
487-
);
488-
return Err(CodegenError {
489-
kind: CodegenErrorKind::MissingFunctionInterface(first_arg.name.as_ref().unwrap().to_string()),
490-
span: bf.span.clone_box(),
491-
token: None,
492-
});
493-
};
494-
495-
// Left-pad the 4-byte selector to 32 bytes for B256
496-
let mut bytes = [0u8; 32];
497-
bytes[28..32].copy_from_slice(&selector);
498-
Ok(PushValue::from(bytes))
499-
}
500-
501-
#[derive(Debug, Clone, PartialEq)]
502-
pub enum PadDirection {
503-
Left,
504-
Right,
505-
}
506-
impl Display for PadDirection {
507-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508-
match self {
509-
PadDirection::Left => write!(f, "__LEFTPAD"),
510-
PadDirection::Right => write!(f, "__RIGHTPAD"),
511-
}
512-
}
513-
}
514-
515443
/// Generates a PushValue for left or right padding a byte string to 32 bytes
516444
pub fn builtin_pad(contract: &Contract, bf: &BuiltinFunctionCall, direction: PadDirection) -> Result<PushValue, CodegenError> {
517445
validate_arg_count(bf, 1, &direction.to_string())?;
518446
let first_arg = match &bf.args[0] {
519447
BuiltinFunctionArg::Argument(arg) => arg.name.clone().unwrap_or_default(),
520448
BuiltinFunctionArg::BuiltinFunctionCall(inner_call) => match inner_call.kind {
521-
BuiltinFunctionKind::FunctionSignature => function_signature(contract, inner_call)?.to_hex_trimmed(),
522-
BuiltinFunctionKind::Bytes => builtin_bytes(inner_call)?.to_hex_trimmed(),
449+
BuiltinFunctionKind::FunctionSignature => eval_function_signature(contract, inner_call)?.to_hex_trimmed(),
450+
BuiltinFunctionKind::Bytes => eval_builtin_bytes(inner_call)?.to_hex_trimmed(),
451+
BuiltinFunctionKind::EventHash => eval_event_hash(contract, inner_call)?.to_hex_trimmed(),
523452
_ => {
524453
tracing::error!(target: "codegen", "Invalid function call argument type passed to {direction}");
525454
return Err(invalid_arguments_error(format!("Invalid argument type passed to {direction}"), &bf.span));
@@ -554,34 +483,3 @@ pub fn builtin_pad(contract: &Contract, bf: &BuiltinFunctionCall, direction: Pad
554483

555484
Ok(PushValue::new(B256::from(bytes_array)))
556485
}
557-
558-
/// Validates and converts a string argument to a right-padded 32-byte array
559-
fn validate_and_pad_string(bf: &BuiltinFunctionCall) -> Result<[u8; 32], CodegenError> {
560-
validate_arg_count(bf, 1, "__BYTES")?;
561-
let first_arg = match bf.args[0] {
562-
BuiltinFunctionArg::Argument(ref arg) => arg.name.clone().unwrap_or_default(),
563-
_ => {
564-
tracing::error!(target: "codegen", "Invalid argument type passed to __BYTES");
565-
return Err(invalid_arguments_error("Invalid argument type passed to __BYTES", &bf.span));
566-
}
567-
};
568-
569-
if first_arg.is_empty() {
570-
return Err(invalid_arguments_error("Empty string passed to __BYTES", &bf.span));
571-
}
572-
573-
let bytes = first_arg.as_bytes();
574-
if bytes.len() > 32 {
575-
return Err(invalid_arguments_error("Encoded bytes length exceeds 32 bytes", &bf.span));
576-
}
577-
578-
let mut bytes_array = [0u8; 32];
579-
bytes_array[32 - bytes.len()..].copy_from_slice(bytes);
580-
Ok(bytes_array)
581-
}
582-
583-
/// Generates a PushValue for the __BYTES builtin function
584-
pub fn builtin_bytes(bf: &BuiltinFunctionCall) -> Result<PushValue, CodegenError> {
585-
let bytes_array = validate_and_pad_string(bf)?;
586-
Ok(PushValue::new(B256::from(bytes_array)))
587-
}

crates/codegen/src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#![warn(unused_extern_crates)]
44
#![forbid(unsafe_code)]
55

6-
use crate::irgen::builtin_function::{PadDirection, builtin_bytes, builtin_pad, function_signature};
6+
use crate::irgen::builtin_function::builtin_pad;
77
use alloy_primitives::{U256, hex};
88
use huff_neo_utils::ast::huff::*;
99
use huff_neo_utils::ast::span::AstSpan;
@@ -429,9 +429,11 @@ impl Codegen {
429429
/// Generates bytecode for a builtin function call used for constants, code tables, etc.
430430
/// Returns a PushValue that can be converted to bytecode with or without opcode
431431
pub fn gen_builtin_bytecode(contract: &Contract, bf: &BuiltinFunctionCall, span: AstSpan) -> Result<PushValue, CodegenError> {
432+
use huff_neo_utils::builtin_eval::{PadDirection, eval_builtin_bytes, eval_event_hash, eval_function_signature};
432433
match bf.kind {
433-
BuiltinFunctionKind::FunctionSignature => function_signature(contract, bf),
434-
BuiltinFunctionKind::Bytes => builtin_bytes(bf),
434+
BuiltinFunctionKind::FunctionSignature => eval_function_signature(contract, bf),
435+
BuiltinFunctionKind::Bytes => eval_builtin_bytes(bf),
436+
BuiltinFunctionKind::EventHash => eval_event_hash(contract, bf),
435437
BuiltinFunctionKind::LeftPad => builtin_pad(contract, bf, PadDirection::Left),
436438
BuiltinFunctionKind::RightPad => builtin_pad(contract, bf, PadDirection::Right),
437439
_ => Err(CodegenError {
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
mod common;
2+
3+
#[test]
4+
fn test_rightpad_in_constant_reference() {
5+
let source = r#"
6+
#define constant C1 = __RIGHTPAD(0x)
7+
#define constant C2 = [C1]
8+
9+
#define macro MAIN() = takes(0) returns(0) {
10+
[C2]
11+
}
12+
"#;
13+
14+
let bytecode = common::compile_to_bytecode(source).unwrap();
15+
// __RIGHTPAD(0x) produces 32 zero bytes, optimized to PUSH1 0x00
16+
assert_eq!(bytecode, "5f"); // PUSH0 (0x5f) in Shanghai+ or PUSH1 0x00 in earlier versions
17+
}
18+
19+
#[test]
20+
fn test_func_sig_in_constant_reference() {
21+
let source = r#"
22+
#define constant SIG = __FUNC_SIG("transfer(address,uint256)")
23+
#define constant SELECTOR = [SIG]
24+
25+
#define macro MAIN() = takes(0) returns(0) {
26+
[SELECTOR]
27+
}
28+
"#;
29+
30+
let bytecode = common::compile_to_bytecode(source).unwrap();
31+
// transfer(address,uint256) signature = 0xa9059cbb
32+
// PUSH4 = 0x63
33+
assert_eq!(bytecode, "63a9059cbb");
34+
}
35+
36+
#[test]
37+
fn test_event_hash_in_constant_reference() {
38+
let source = r#"
39+
#define constant HASH = __EVENT_HASH("Transfer(address,address,uint256)")
40+
#define constant EVENT_TOPIC = [HASH]
41+
42+
#define macro MAIN() = takes(0) returns(0) {
43+
[EVENT_TOPIC]
44+
}
45+
"#;
46+
47+
let bytecode = common::compile_to_bytecode(source).unwrap();
48+
// Transfer(address,address,uint256) event hash
49+
// keccak256("Transfer(address,address,uint256)") = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
50+
// PUSH32 = 0x7f
51+
assert_eq!(bytecode, "7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef");
52+
}
53+
54+
#[test]
55+
fn test_bytes_in_constant_reference() {
56+
let source = r#"
57+
#define constant B = __BYTES("hello")
58+
#define constant BYTES_VAL = [B]
59+
60+
#define macro MAIN() = takes(0) returns(0) {
61+
[BYTES_VAL]
62+
}
63+
"#;
64+
65+
let bytecode = common::compile_to_bytecode(source).unwrap();
66+
// "hello" as bytes, right-padded to 32 bytes: 0x68656c6c6f (followed by zeros)
67+
// Since it's only 5 bytes, it will be optimized to PUSH5
68+
// PUSH5 = 0x64
69+
assert_eq!(bytecode, "6468656c6c6f");
70+
}
71+
72+
#[test]
73+
fn test_nested_constant_references_with_builtin() {
74+
let source = r#"
75+
#define constant C1 = __FUNC_SIG("test()")
76+
#define constant C2 = [C1]
77+
#define constant C3 = [C2]
78+
79+
#define macro MAIN() = takes(0) returns(0) {
80+
[C3]
81+
}
82+
"#;
83+
84+
let bytecode = common::compile_to_bytecode(source).unwrap();
85+
// test() signature = keccak256("test()")[0..4] = 0xf8a8fd6d
86+
// PUSH4 = 0x63
87+
assert_eq!(bytecode, "63f8a8fd6d");
88+
}
89+
90+
#[test]
91+
fn test_builtin_in_arithmetic_expression() {
92+
let source = r#"
93+
#define constant SIG = __FUNC_SIG("test()")
94+
#define constant OFFSET = [SIG] + 1
95+
96+
#define macro MAIN() = takes(0) returns(0) {
97+
[OFFSET]
98+
}
99+
"#;
100+
101+
let bytecode = common::compile_to_bytecode(source).unwrap();
102+
// test() signature = 0xf8a8fd6d = 4171875693
103+
// 4171875693 + 1 = 4171875694 = 0xf8a8fd6e
104+
// PUSH4 = 0x63
105+
assert_eq!(bytecode, "63f8a8fd6e");
106+
}
107+
108+
#[test]
109+
fn test_leftpad_in_constant_reference() {
110+
let source = r#"
111+
#define constant PADDED = __LEFTPAD(0x42)
112+
#define constant VAL = [PADDED]
113+
114+
#define macro MAIN() = takes(0) returns(0) {
115+
[VAL]
116+
}
117+
"#;
118+
119+
let bytecode = common::compile_to_bytecode(source).unwrap();
120+
// __LEFTPAD(0x42) produces 0x0000...0042 (left-padded to 32 bytes)
121+
// This is just 0x42, which gets optimized to PUSH1
122+
// PUSH1 = 0x60
123+
assert_eq!(bytecode, "6042");
124+
}
125+
126+
#[test]
127+
fn test_rightpad_vs_leftpad() {
128+
let source_right = r#"
129+
#define constant PADDED = __RIGHTPAD(0x42)
130+
#define constant VAL = [PADDED]
131+
132+
#define macro MAIN() = takes(0) returns(0) {
133+
[VAL]
134+
}
135+
"#;
136+
137+
let bytecode_right = common::compile_to_bytecode(source_right).unwrap();
138+
// __RIGHTPAD(0x42) produces 0x4200...0000 (right-padded to 32 bytes)
139+
// This is 0x42 followed by 31 zero bytes
140+
// PUSH32 = 0x7f (full 32 bytes)
141+
assert_eq!(bytecode_right, "7f4200000000000000000000000000000000000000000000000000000000000000");
142+
143+
let source_left = r#"
144+
#define constant PADDED = __LEFTPAD(0x42)
145+
#define constant VAL = [PADDED]
146+
147+
#define macro MAIN() = takes(0) returns(0) {
148+
[VAL]
149+
}
150+
"#;
151+
152+
let bytecode_left = common::compile_to_bytecode(source_left).unwrap();
153+
// __LEFTPAD(0x42) produces 0x0000...0042 (left-padded to 32 bytes)
154+
// Leading zeros are trimmed, so it's just 0x42
155+
// PUSH1 = 0x60
156+
assert_eq!(bytecode_left, "6042");
157+
}
158+
159+
#[test]
160+
fn test_multiple_builtins_in_constants() {
161+
let source = r#"
162+
#define constant FUNC_SIG = __FUNC_SIG("transfer(address,uint256)")
163+
#define constant EVENT_HASH = __EVENT_HASH("Transfer(address,address,uint256)")
164+
#define constant SIG_REF = [FUNC_SIG]
165+
#define constant HASH_REF = [EVENT_HASH]
166+
167+
#define macro MAIN() = takes(0) returns(0) {
168+
[SIG_REF]
169+
[HASH_REF]
170+
}
171+
"#;
172+
173+
let bytecode = common::compile_to_bytecode(source).unwrap();
174+
// transfer(address,uint256) = 0xa9059cbb (PUSH4)
175+
// Transfer(address,address,uint256) = 0xddf252ad... (PUSH32)
176+
assert_eq!(bytecode, "63a9059cbb7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef");
177+
}
178+
179+
#[test]
180+
fn test_error_selector_in_constant_reference() {
181+
let source = r#"
182+
#define error CustomError(uint256)
183+
184+
#define constant ERR = __FUNC_SIG(CustomError)
185+
#define constant ERR_REF = [ERR]
186+
187+
#define macro MAIN() = takes(0) returns(0) {
188+
[ERR_REF]
189+
}
190+
"#;
191+
192+
let bytecode = common::compile_to_bytecode(source).unwrap();
193+
// CustomError(uint256) selector = keccak256("CustomError(uint256)")[0..4] = 0x110b3655
194+
// PUSH4 = 0x63
195+
assert_eq!(bytecode, "63110b3655");
196+
}

0 commit comments

Comments
 (0)