neo_solidity/ir/expressions/member_access/
address_ops.rs

1fn try_lower_address_balance(
2    inner: &Expression,
3    member: &Identifier,
4    ctx: &mut LoweringContext,
5    instructions: &mut Vec<Instruction>,
6) -> Option<bool> {
7    if member.name != "balance" {
8        return None;
9    }
10
11    if matches!(
12        infer_type_from_expression(inner, ctx),
13        Some(ValueType::Address)
14    ) {
15        if !lower_expression(inner, ctx, instructions) {
16            return Some(false);
17        }
18        instructions.push(Instruction::CallBuiltin {
19            builtin: BuiltinCall::NativeCall {
20                contract: NativeContract::Gas,
21                method: "balanceOf".to_string(),
22            },
23            arg_count: 1,
24        });
25        return Some(true);
26    }
27
28    None
29}
30
31fn try_lower_length_property(
32    inner: &Expression,
33    member: &Identifier,
34    ctx: &mut LoweringContext,
35    instructions: &mut Vec<Instruction>,
36) -> Option<bool> {
37    if member.name != "length" {
38        return None;
39    }
40
41    // Neo doesn't expose EVM bytecode, but many Solidity contracts use
42    // `address.code.length > 0` as a contract-existence check. On Neo N3 we can
43    // approximate this by calling `ContractManagement.isContract(address)`:
44    // - false => non-contract => length 0
45    // - true => contract exists => length 1
46    if let Expression::MemberAccess(_, code_inner, code_member) = inner {
47        if code_member.name == "code"
48            && matches!(
49                infer_type_from_expression(code_inner.as_ref(), ctx),
50                Some(ValueType::Address)
51            )
52        {
53            if !lower_expression(code_inner.as_ref(), ctx, instructions) {
54                return Some(false);
55            }
56
57            instructions.push(Instruction::CallBuiltin {
58                builtin: BuiltinCall::NativeCall {
59                    contract: NativeContract::ContractManagement,
60                    method: "isContract".to_string(),
61                },
62                arg_count: 1,
63            });
64
65            let not_contract_label = ctx.next_label();
66            let end_label = ctx.next_label();
67
68            // JumpIf branches on false, so this jumps when isContract == false.
69            instructions.push(Instruction::JumpIf {
70                target: not_contract_label,
71            });
72            instructions.push(Instruction::PushLiteral(LiteralValue::Integer(
73                BigInt::one(),
74            )));
75            instructions.push(Instruction::Jump { target: end_label });
76            instructions.push(Instruction::Label(not_contract_label));
77            instructions.push(Instruction::PushLiteral(LiteralValue::Integer(
78                BigInt::zero(),
79            )));
80            instructions.push(Instruction::Label(end_label));
81            return Some(true);
82        }
83    }
84
85    if let Expression::Variable(base) = inner {
86        if let Some(state_index) = ctx.state_index_map.get(&base.name) {
87            if matches!(ctx.state_type(*state_index), Some(ValueType::Array(_))) {
88                // Storage array length is stored at the base slot.
89                instructions.push(Instruction::LoadState(*state_index));
90                return Some(true);
91            }
92        }
93    }
94
95    if lower_expression(inner, ctx, instructions) {
96        instructions.push(Instruction::GetSize);
97        return Some(true);
98    }
99
100    Some(false)
101}
102
103fn try_lower_current_key(
104    inner: &Expression,
105    member: &Identifier,
106    ctx: &mut LoweringContext,
107    instructions: &mut Vec<Instruction>,
108) -> Option<bool> {
109    if member.name != "currentKey" {
110        return None;
111    }
112
113    // Neo storage iterators yield [key, value] pairs. Devpacks sometimes model this as
114    // a struct field; extract key via `System.Iterator.Value` + PICKITEM(0).
115    if !lower_expression(inner, ctx, instructions) {
116        return Some(false);
117    }
118    instructions.push(Instruction::CallBuiltin {
119        builtin: BuiltinCall::Syscall("System.Iterator.Value".to_string()),
120        arg_count: 1,
121    });
122    instructions.push(Instruction::PushLiteral(LiteralValue::Integer(
123        BigInt::zero(),
124    )));
125    instructions.push(Instruction::ArrayGet);
126    Some(true)
127}
128
129fn try_lower_current_value(
130    inner: &Expression,
131    member: &Identifier,
132    ctx: &mut LoweringContext,
133    instructions: &mut Vec<Instruction>,
134) -> Option<bool> {
135    if member.name != "currentValue" {
136        return None;
137    }
138
139    // Neo storage iterators yield [key, value] pairs. Devpacks sometimes model this as
140    // a struct field; extract value via `System.Iterator.Value` + PICKITEM(1).
141    if !lower_expression(inner, ctx, instructions) {
142        return Some(false);
143    }
144    instructions.push(Instruction::CallBuiltin {
145        builtin: BuiltinCall::Syscall("System.Iterator.Value".to_string()),
146        arg_count: 1,
147    });
148    instructions.push(Instruction::PushLiteral(LiteralValue::Integer(
149        BigInt::one(),
150    )));
151    instructions.push(Instruction::ArrayGet);
152    Some(true)
153}
154
155fn try_lower_code_property(
156    inner: &Expression,
157    member: &Identifier,
158    ctx: &mut LoweringContext,
159    instructions: &mut Vec<Instruction>,
160) -> Option<bool> {
161    if member.name == "codehash" {
162        if matches!(
163            infer_type_from_expression(inner, ctx),
164            Some(ValueType::Address)
165        ) {
166            // Neo N3 auto-compat: address.codehash → the address itself (script hash)
167            // On Neo, a contract's "code hash" IS its script hash (UInt160), which is
168            // the same as the address. For non-contract addresses, return bytes32(0).
169            eprintln!(
170                "warning: address.codehash auto-mapped to the contract script hash \
171                 on Neo N3. For contract addresses this returns the address itself; \
172                 for non-contract addresses it returns bytes32(0)."
173            );
174            if !lower_expression(inner, ctx, instructions) {
175                return Some(false);
176            }
177            // Duplicate the address for the isContract check
178            instructions.push(Instruction::Dup);
179            instructions.push(Instruction::CallBuiltin {
180                builtin: BuiltinCall::NativeCall {
181                    contract: NativeContract::ContractManagement,
182                    method: "isContract".to_string(),
183                },
184                arg_count: 1,
185            });
186            let not_contract_label = ctx.next_label();
187            let end_label = ctx.next_label();
188            instructions.push(Instruction::JumpIf {
189                target: not_contract_label,
190            });
191            // Is a contract: address IS the script hash — leave it on stack
192            instructions.push(Instruction::Jump { target: end_label });
193            instructions.push(Instruction::Label(not_contract_label));
194            // Not a contract: drop address, push zero bytes
195            instructions.push(Instruction::Drop(ValueType::Any));
196            instructions.push(Instruction::PushLiteral(LiteralValue::ByteArray(
197                vec![0u8; 32],
198            )));
199            instructions.push(Instruction::Label(end_label));
200            return Some(true);
201        }
202        return None;
203    }
204
205    if member.name != "code" {
206        return None;
207    }
208
209    if lower_expression(inner, ctx, instructions) {
210        instructions.push(Instruction::Drop(ValueType::Any));
211        instructions.push(Instruction::PushLiteral(LiteralValue::ByteArray(vec![])));
212        return Some(true);
213    }
214
215    Some(false)
216}