mirror of
https://github.com/luxfi/indexer.git
synced 2026-09-24 00:25:37 +00:00
Lux Partners Limited is not the entity behind this work. Every place the old name stood — copyright lines, site footers, brand config, legal and compliance text — now names the company that actually holds it. lux.fund is the exception and is not renamed to match: that site belongs to Lux Assets Management LLC, a different company, and it says so. Two kinds of line were left for a person rather than rewritten. A dated news item recording that Lux Partners Limited was established in the Isle of Man is a historical fact, and renaming it would falsify the record. And a sentence asserting that the company is domiciled in the Isle of Man is a claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle of Man company, so swapping the name there would assert something untrue. Co-authored-by: Hanzo Dev <[email protected]>
644 lines
16 KiB
Go
644 lines
16 KiB
Go
// Copyright (c) 2025 Lux Industries Inc
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package contracts
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseABI(t *testing.T) {
|
|
abiJSON := json.RawMessage(`[
|
|
{
|
|
"type": "function",
|
|
"name": "transfer",
|
|
"inputs": [
|
|
{"name": "to", "type": "address"},
|
|
{"name": "amount", "type": "uint256"}
|
|
],
|
|
"outputs": [
|
|
{"name": "", "type": "bool"}
|
|
],
|
|
"stateMutability": "nonpayable"
|
|
},
|
|
{
|
|
"type": "event",
|
|
"name": "Transfer",
|
|
"inputs": [
|
|
{"name": "from", "type": "address", "indexed": true},
|
|
{"name": "to", "type": "address", "indexed": true},
|
|
{"name": "value", "type": "uint256", "indexed": false}
|
|
]
|
|
},
|
|
{
|
|
"type": "constructor",
|
|
"inputs": [
|
|
{"name": "name_", "type": "string"},
|
|
{"name": "symbol_", "type": "string"}
|
|
]
|
|
}
|
|
]`)
|
|
|
|
methods, events, err := ParseABI(abiJSON)
|
|
if err != nil {
|
|
t.Fatalf("ParseABI failed: %v", err)
|
|
}
|
|
|
|
if len(methods) != 1 {
|
|
t.Errorf("expected 1 method, got %d", len(methods))
|
|
}
|
|
|
|
if len(events) != 1 {
|
|
t.Errorf("expected 1 event, got %d", len(events))
|
|
}
|
|
|
|
// Check method
|
|
if methods[0].Name != "transfer" {
|
|
t.Errorf("expected method name 'transfer', got '%s'", methods[0].Name)
|
|
}
|
|
if methods[0].Signature != "transfer(address,uint256)" {
|
|
t.Errorf("expected signature 'transfer(address,uint256)', got '%s'", methods[0].Signature)
|
|
}
|
|
// transfer(address,uint256) selector: 0xa9059cbb
|
|
if !strings.HasPrefix(methods[0].Selector, "0x") {
|
|
t.Errorf("selector should start with 0x, got '%s'", methods[0].Selector)
|
|
}
|
|
|
|
// Check event
|
|
if events[0].Name != "Transfer" {
|
|
t.Errorf("expected event name 'Transfer', got '%s'", events[0].Name)
|
|
}
|
|
if events[0].Signature != "Transfer(address,address,uint256)" {
|
|
t.Errorf("expected signature 'Transfer(address,address,uint256)', got '%s'", events[0].Signature)
|
|
}
|
|
}
|
|
|
|
func TestBuildFunctionSignature(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
inputs []ABIParam
|
|
expected string
|
|
}{
|
|
{
|
|
name: "transfer",
|
|
inputs: []ABIParam{{Type: "address"}, {Type: "uint256"}},
|
|
expected: "transfer(address,uint256)",
|
|
},
|
|
{
|
|
name: "approve",
|
|
inputs: []ABIParam{{Type: "address"}, {Type: "uint256"}},
|
|
expected: "approve(address,uint256)",
|
|
},
|
|
{
|
|
name: "balanceOf",
|
|
inputs: []ABIParam{{Type: "address"}},
|
|
expected: "balanceOf(address)",
|
|
},
|
|
{
|
|
name: "noArgs",
|
|
inputs: []ABIParam{},
|
|
expected: "noArgs()",
|
|
},
|
|
{
|
|
name: "withTuple",
|
|
inputs: []ABIParam{
|
|
{
|
|
Type: "tuple",
|
|
Components: []ABIParam{
|
|
{Type: "address"},
|
|
{Type: "uint256"},
|
|
},
|
|
},
|
|
},
|
|
expected: "withTuple((address,uint256))",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := buildFunctionSignature(tt.name, tt.inputs)
|
|
if result != tt.expected {
|
|
t.Errorf("expected '%s', got '%s'", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetectEIP1167(t *testing.T) {
|
|
detector := &ProxyDetector{}
|
|
|
|
// Standard EIP-1167 minimal proxy bytecode
|
|
// 363d3d373d3d3d363d73<20-byte-address>5af43d82803e903d91602b57fd5bf3
|
|
implAddr := "1234567890123456789012345678901234567890"
|
|
bytecode := "0x363d3d373d3d3d363d73" + implAddr + "5af43d82803e903d91602b57fd5bf3"
|
|
|
|
info := detector.detectEIP1167(bytecode)
|
|
if info == nil {
|
|
t.Fatal("expected proxy info, got nil")
|
|
}
|
|
|
|
if !info.IsProxy {
|
|
t.Error("expected IsProxy to be true")
|
|
}
|
|
|
|
if info.ProxyType != ProxyEIP1167 {
|
|
t.Errorf("expected ProxyEIP1167, got %s", info.ProxyType)
|
|
}
|
|
|
|
expectedImpl := "0x" + implAddr
|
|
if strings.ToLower(info.ImplementationAddress) != strings.ToLower(expectedImpl) {
|
|
t.Errorf("expected implementation %s, got %s", expectedImpl, info.ImplementationAddress)
|
|
}
|
|
}
|
|
|
|
func TestDetectEIP1167Variant(t *testing.T) {
|
|
detector := &ProxyDetector{}
|
|
|
|
// Variant EIP-1167: 3d3d3d3d363d3d37363d73<address>5af43d3d93803e602a57fd5bf3
|
|
implAddr := "abcdef1234567890abcdef1234567890abcdef12"
|
|
bytecode := "0x3d3d3d3d363d3d37363d73" + implAddr + "5af43d3d93803e602a57fd5bf3"
|
|
|
|
info := detector.detectEIP1167(bytecode)
|
|
if info == nil {
|
|
t.Fatal("expected proxy info, got nil")
|
|
}
|
|
|
|
if info.ProxyType != ProxyEIP1167 {
|
|
t.Errorf("expected ProxyEIP1167, got %s", info.ProxyType)
|
|
}
|
|
|
|
expectedImpl := "0x" + implAddr
|
|
if strings.ToLower(info.ImplementationAddress) != strings.ToLower(expectedImpl) {
|
|
t.Errorf("expected implementation %s, got %s", expectedImpl, info.ImplementationAddress)
|
|
}
|
|
}
|
|
|
|
func TestIsSafeProxy(t *testing.T) {
|
|
detector := &ProxyDetector{}
|
|
|
|
// Safe proxy v1.3.0 pattern (starts with 608060405273... and contains a619486e)
|
|
safeProxy := "608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3"
|
|
safeBytes, _ := hex.DecodeString(safeProxy)
|
|
|
|
if !detector.isSafeProxy(safeBytes) {
|
|
t.Error("expected Safe proxy to be detected")
|
|
}
|
|
|
|
// Non-Safe bytecode
|
|
nonSafe := "6080604052348015600f57600080fd5b50"
|
|
nonSafeBytes, _ := hex.DecodeString(nonSafe)
|
|
|
|
if detector.isSafeProxy(nonSafeBytes) {
|
|
t.Error("expected non-Safe bytecode to not be detected as Safe proxy")
|
|
}
|
|
}
|
|
|
|
func TestExtractAddress(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
input: "0x000000000000000000000000abcdef1234567890abcdef1234567890abcdef12",
|
|
expected: "0xabcdef1234567890abcdef1234567890abcdef12",
|
|
},
|
|
{
|
|
input: "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
expected: "0x0000000000000000000000000000000000000000",
|
|
},
|
|
{
|
|
input: "0xabcdef",
|
|
expected: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
result := extractAddress(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("expected '%s', got '%s'", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsZeroAddress(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected bool
|
|
}{
|
|
{"0x0000000000000000000000000000000000000000", true},
|
|
{"0x0000000000000000000000000000000000000001", false},
|
|
{"0xabcdef1234567890abcdef1234567890abcdef12", false},
|
|
{"", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
result := isZeroAddress(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestABIDecoder(t *testing.T) {
|
|
abiJSON := json.RawMessage(`[
|
|
{
|
|
"type": "function",
|
|
"name": "transfer",
|
|
"inputs": [
|
|
{"name": "to", "type": "address"},
|
|
{"name": "amount", "type": "uint256"}
|
|
],
|
|
"outputs": [{"name": "", "type": "bool"}]
|
|
},
|
|
{
|
|
"type": "event",
|
|
"name": "Transfer",
|
|
"inputs": [
|
|
{"name": "from", "type": "address", "indexed": true},
|
|
{"name": "to", "type": "address", "indexed": true},
|
|
{"name": "value", "type": "uint256", "indexed": false}
|
|
]
|
|
}
|
|
]`)
|
|
|
|
decoder, err := NewABIDecoder(abiJSON)
|
|
if err != nil {
|
|
t.Fatalf("NewABIDecoder failed: %v", err)
|
|
}
|
|
|
|
// Test DecodeCall
|
|
// transfer(to=0x1234...5678, amount=1000000)
|
|
// selector: a9059cbb (using our simplified keccak)
|
|
// Note: actual selector may differ due to simplified keccak implementation
|
|
t.Run("DecodeCall", func(t *testing.T) {
|
|
// Get the actual selector from the decoder
|
|
for selector, method := range decoder.methods {
|
|
if method.Name == "transfer" {
|
|
// Construct input data
|
|
toAddr := "0000000000000000000000001234567890123456789012345678901234567890"
|
|
amount := "00000000000000000000000000000000000000000000000000000000000f4240" // 1000000
|
|
|
|
input := selector + toAddr + amount
|
|
decoded, err := decoder.DecodeCall(input)
|
|
if err != nil {
|
|
t.Fatalf("DecodeCall failed: %v", err)
|
|
}
|
|
|
|
if decoded.Name != "transfer" {
|
|
t.Errorf("expected name 'transfer', got '%s'", decoded.Name)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDecodeBasicType(t *testing.T) {
|
|
tests := []struct {
|
|
typ string
|
|
data string
|
|
expected interface{}
|
|
}{
|
|
{
|
|
typ: "address",
|
|
data: "0000000000000000000000001234567890123456789012345678901234567890",
|
|
expected: "0x1234567890123456789012345678901234567890",
|
|
},
|
|
{
|
|
typ: "bool",
|
|
data: "0000000000000000000000000000000000000000000000000000000000000001",
|
|
expected: true,
|
|
},
|
|
{
|
|
typ: "bool",
|
|
data: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
expected: false,
|
|
},
|
|
{
|
|
typ: "uint256",
|
|
data: "00000000000000000000000000000000000000000000000000000000000f4240",
|
|
expected: "1000000",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.typ, func(t *testing.T) {
|
|
result, consumed, err := decodeBasicType(tt.typ, tt.data)
|
|
if err != nil {
|
|
t.Fatalf("decodeBasicType failed: %v", err)
|
|
}
|
|
if consumed != 64 {
|
|
t.Errorf("expected 64 consumed, got %d", consumed)
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeTopicValue(t *testing.T) {
|
|
tests := []struct {
|
|
typ string
|
|
topic string
|
|
expected interface{}
|
|
}{
|
|
{
|
|
typ: "address",
|
|
topic: "0x0000000000000000000000001234567890123456789012345678901234567890",
|
|
expected: "0x1234567890123456789012345678901234567890",
|
|
},
|
|
{
|
|
typ: "uint256",
|
|
topic: "0x00000000000000000000000000000000000000000000000000000000000003e8",
|
|
expected: "1000",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.typ, func(t *testing.T) {
|
|
result, err := decodeTopicValue(tt.typ, tt.topic)
|
|
if err != nil {
|
|
t.Fatalf("decodeTopicValue failed: %v", err)
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFourByteDB(t *testing.T) {
|
|
db := NewFourByteDB()
|
|
|
|
db.AddSignature("transfer(address,uint256)")
|
|
db.AddSignature("approve(address,uint256)")
|
|
db.AddSignature("balanceOf(address)")
|
|
|
|
// Look up transfer
|
|
sigs := db.LookupSelector(selectorFromSignature("transfer(address,uint256)"))
|
|
if len(sigs) == 0 {
|
|
t.Error("expected to find transfer signature")
|
|
}
|
|
found := false
|
|
for _, sig := range sigs {
|
|
if sig == "transfer(address,uint256)" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected to find transfer(address,uint256)")
|
|
}
|
|
}
|
|
|
|
func TestCombineABIs(t *testing.T) {
|
|
proxyABI := json.RawMessage(`[
|
|
{"type": "function", "name": "upgradeTo", "inputs": [{"type": "address"}]}
|
|
]`)
|
|
|
|
implABI := json.RawMessage(`[
|
|
{"type": "function", "name": "transfer", "inputs": [{"type": "address"}, {"type": "uint256"}]},
|
|
{"type": "function", "name": "balanceOf", "inputs": [{"type": "address"}]}
|
|
]`)
|
|
|
|
combined, err := CombineABIs(proxyABI, implABI)
|
|
if err != nil {
|
|
t.Fatalf("CombineABIs failed: %v", err)
|
|
}
|
|
|
|
var entries []ABIEntry
|
|
if err := json.Unmarshal(combined, &entries); err != nil {
|
|
t.Fatalf("unmarshal combined ABI failed: %v", err)
|
|
}
|
|
|
|
if len(entries) != 3 {
|
|
t.Errorf("expected 3 entries, got %d", len(entries))
|
|
}
|
|
|
|
// Check that all three functions are present
|
|
names := make(map[string]bool)
|
|
for _, entry := range entries {
|
|
names[entry.Name] = true
|
|
}
|
|
|
|
if !names["upgradeTo"] {
|
|
t.Error("expected upgradeTo in combined ABI")
|
|
}
|
|
if !names["transfer"] {
|
|
t.Error("expected transfer in combined ABI")
|
|
}
|
|
if !names["balanceOf"] {
|
|
t.Error("expected balanceOf in combined ABI")
|
|
}
|
|
}
|
|
|
|
func TestStripMetadata(t *testing.T) {
|
|
v := &Verifier{}
|
|
|
|
// Bytecode with CBOR metadata (simplified example)
|
|
// Metadata: a265627a7a723... (variable length)
|
|
// Last 2 bytes: length indicator
|
|
|
|
// Test with known pattern
|
|
bytecode := "608060405234801561001057600080fd5b5060df8061001f6000396000f3a264697066735822" + strings.Repeat("00", 32) + "0033"
|
|
stripped := v.stripMetadata(bytecode)
|
|
|
|
// Should strip the metadata
|
|
if stripped == bytecode {
|
|
t.Log("stripMetadata may not have stripped anything, checking implementation")
|
|
}
|
|
}
|
|
|
|
func TestVerifierCompareBytecode(t *testing.T) {
|
|
v := &Verifier{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
onChain string
|
|
compiled string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "exact match",
|
|
onChain: "0x608060405234801561001057600080fd",
|
|
compiled: "608060405234801561001057600080fd",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "case insensitive",
|
|
onChain: "0x608060405234801561001057600080FD",
|
|
compiled: "608060405234801561001057600080fd",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "compiled is prefix",
|
|
onChain: "0x608060405234801561001057600080fd00001234",
|
|
compiled: "608060405234801561001057600080fd",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "no match",
|
|
onChain: "0x608060405234801561001057600080fd",
|
|
compiled: "123456789abcdef0",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "empty bytecode",
|
|
onChain: "0x",
|
|
compiled: "608060405234801561001057600080fd",
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := v.compareBytecode(tt.onChain, tt.compiled)
|
|
if result != tt.expected {
|
|
t.Errorf("expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProxyTypeConstants(t *testing.T) {
|
|
// Verify storage slot constants are correct
|
|
tests := []struct {
|
|
name string
|
|
slot string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "EIP1967 Implementation",
|
|
slot: StorageSlotEIP1967Implementation,
|
|
expected: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
|
|
},
|
|
{
|
|
name: "EIP1967 Beacon",
|
|
slot: StorageSlotEIP1967Beacon,
|
|
expected: "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50",
|
|
},
|
|
{
|
|
name: "EIP1967 Admin",
|
|
slot: StorageSlotEIP1967Admin,
|
|
expected: "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103",
|
|
},
|
|
{
|
|
name: "EIP1822 Proxiable",
|
|
slot: StorageSlotEIP1822,
|
|
expected: "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.slot != tt.expected {
|
|
t.Errorf("expected %s, got %s", tt.expected, tt.slot)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMultiABIDecoder(t *testing.T) {
|
|
abi1 := json.RawMessage(`[
|
|
{"type": "function", "name": "transfer", "inputs": [{"name": "to", "type": "address"}, {"name": "amount", "type": "uint256"}]}
|
|
]`)
|
|
|
|
abi2 := json.RawMessage(`[
|
|
{"type": "function", "name": "approve", "inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}]}
|
|
]`)
|
|
|
|
decoder, err := NewMultiABIDecoder(abi1, abi2)
|
|
if err != nil {
|
|
t.Fatalf("NewMultiABIDecoder failed: %v", err)
|
|
}
|
|
|
|
if len(decoder.decoders) != 2 {
|
|
t.Errorf("expected 2 decoders, got %d", len(decoder.decoders))
|
|
}
|
|
}
|
|
|
|
func TestSmartContractTypes(t *testing.T) {
|
|
// Test SmartContract struct
|
|
contract := SmartContract{
|
|
Address: "0x1234567890123456789012345678901234567890",
|
|
Name: "TestContract",
|
|
CompilerVersion: "v0.8.20+commit.a1b2c3d4",
|
|
EVMVersion: "paris",
|
|
Optimization: true,
|
|
OptimizationRuns: 200,
|
|
SourceCode: "contract TestContract {}",
|
|
VerificationStatus: StatusVerified,
|
|
ProxyType: ProxyEIP1967,
|
|
}
|
|
|
|
if contract.Address == "" {
|
|
t.Error("Address should not be empty")
|
|
}
|
|
if contract.VerificationStatus != StatusVerified {
|
|
t.Error("Status should be verified")
|
|
}
|
|
if contract.ProxyType != ProxyEIP1967 {
|
|
t.Error("ProxyType should be EIP1967")
|
|
}
|
|
}
|
|
|
|
func TestProxyInfo(t *testing.T) {
|
|
info := ProxyInfo{
|
|
IsProxy: true,
|
|
ProxyType: ProxyEIP1967,
|
|
ImplementationAddress: "0xabcdef1234567890abcdef1234567890abcdef12",
|
|
}
|
|
|
|
if !info.IsProxy {
|
|
t.Error("IsProxy should be true")
|
|
}
|
|
if info.ProxyType != ProxyEIP1967 {
|
|
t.Error("ProxyType should be EIP1967")
|
|
}
|
|
}
|
|
|
|
// Test context cancellation
|
|
func TestContextCancellation(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
detector := NewProxyDetector("http://localhost:8545")
|
|
|
|
// Should fail fast due to cancelled context
|
|
_, err := detector.DetectProxy(ctx, "0x1234567890123456789012345678901234567890")
|
|
if err == nil {
|
|
t.Log("Context cancellation may not immediately fail HTTP calls in test environment")
|
|
}
|
|
}
|
|
|
|
func TestHexToInt(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected int
|
|
}{
|
|
{"0x10", 16},
|
|
{"10", 16},
|
|
{"0x", 0},
|
|
{"ff", 255},
|
|
{"0xff", 255},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
result := hexToInt(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("expected %d, got %d", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|