Skip to content

Commit

Permalink
erc20 proof (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhangchiqing committed Aug 6, 2022
1 parent 972e77b commit ce9e0f1
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 38 deletions.
19 changes: 19 additions & 0 deletions erc20_proof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package main

import (
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)

func GetSlotForMapKey(slotIndexForMap int, keyInMap []byte) [32]byte {
return crypto.Keccak256Hash(
keyInMap,
common.LeftPadBytes(big.NewInt(int64(slotIndexForMap)).Bytes(), 32),
)
}

func GetSlotForERC20TokenHolder(slotIndexForHoldersMap int, tokenHolder common.Address) [32]byte {
return GetSlotForMapKey(slotIndexForHoldersMap, common.LeftPadBytes(tokenHolder[:], 32))
}
144 changes: 144 additions & 0 deletions erc20_proof_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
"math/big"
"net/http"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/require"
)

func TestERC20(t *testing.T) {

erc20Address := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
tokenHolder := common.HexToAddress("0x467d543e5e4e41aeddf3b6d1997350dd9820a173")
slotIndex, result, err := FindBalanceForERC20TokenHolder(erc20Address, tokenHolder, 15245000)
require.NoError(t, err)
fmt.Println(fmt.Sprintf("slot index %v", slotIndex))

err = VerifyStorageProof(result)
require.NoError(t, err)

// convert hex to bigInt
balance := new(big.Int).SetBytes(result.StorageProof[0].Value)

fmt.Println(fmt.Sprintf("the balance of token holder %x for contract %x's %v", tokenHolder, erc20Address, balance))
}

func VerifyStorageProof(result *StorageStateResult) error {
storageHash := result.StorageHash
storageProof := result.StorageProof[0]
value, err := rlp.EncodeToBytes(storageProof.Value)
if err != nil {
return fmt.Errorf("fail to encode value: %w", err)
}
key := common.LeftPadBytes(storageProof.Key, 32)
proofTrie := NewProofDB()
for _, node := range storageProof.Proof {
proofTrie.Put(crypto.Keccak256(node), node)
}

verified, err := VerifyProof(
storageHash.Bytes(), crypto.Keccak256(key), proofTrie)

if err != nil {
return fmt.Errorf("invalid storage proof: %w", err)
}

if !bytes.Equal(verified, value) {
return fmt.Errorf("invalid proof %x != %x", verified, value)
}
return nil
}

func FindBalanceForERC20TokenHolder(contractAddress common.Address, tokenHolder common.Address, blockNumber uint64) (int, *StorageStateResult, error) {
// for i := 0; i < 20; i++ {
for i := 8; i < 10; i++ {
result, err := FindBalanceForERC20TokenHolderAtSlot(contractAddress, tokenHolder, blockNumber, i)
if err != nil {
return 0, nil, err
}

if len(result.StorageProof) == 0 {
continue
}

proof := result.StorageProof[0]

if len(proof.Value) == 0 {
continue
}

return i, result, nil
}
return 0, nil, fmt.Errorf("not found")
}

func FindBalanceForERC20TokenHolderAtSlot(contractAddress common.Address, tokenHolder common.Address, blockNumber uint64, slotIndex int) (*StorageStateResult, error) {
slot := GetSlotForERC20TokenHolder(slotIndex, tokenHolder)
fmt.Println(
fmt.Sprintf("if slot index for map is %v, 0x467d543e5e4e41aeddf3b6d1997350dd9820a173 's token is stored at %x",
slotIndex, slot),
)

result, err := RequestEthGetProof(
contractAddress,
[]hexutil.Bytes{hexutil.Bytes(slot[:])},
15245000,
)
if err != nil {
return nil, fmt.Errorf("could not get proof for token holder %v in contract %v: %w", tokenHolder, contractAddress, err)
}

return result, nil
}

func RequestEthGetProof(contractAddress common.Address, keys []hexutil.Bytes, blockNumber uint64) (*StorageStateResult, error) {

// ▸ curl https://eth-mainnet.g.alchemy.com/v2/ \
// -X POST \
// -H "Content-Type: application/json" \
// -d '{"jsonrpc":"2.0","method":"eth_getProof","params":["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",["0x4065d4ec50c2a4fc400b75cca2760227b773c3e315ed2f2a7784cd505065cb07"], "0xE89D2E"],"id":1}' | jq .

keysData := make([]string, 0, len(keys))
for _, k := range keys {
keysData = append(keysData, k.String())
}
data := map[string]interface{}{
"jsonrpc": "2.0",
"method": "eth_getProof",
"params": []interface{}{
contractAddress.String(), // erc20 token contract address
keysData, // slot for token holder balance
fmt.Sprintf("0x%x", blockNumber), // hex encoded block number
},
}

payload := new(bytes.Buffer)
json.NewEncoder(payload).Encode(data)

resp, err := http.Post(
"https://eth-mainnet.g.alchemy.com/v2/sljmVCoQ7nCZGHYf_3SAvSLpq0zUEhdd",
"application/json",
payload,
)
if err != nil {
return nil, fmt.Errorf("fail to get response: %w", err)
}
defer resp.Body.Close()

var response EthGetProofResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return nil, fmt.Errorf("fail to parse response: %w", err)
}

return &response.Result, nil
}
46 changes: 46 additions & 0 deletions storage_proof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"bytes"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)

type StorageStateResult struct {
Nonce hexutil.Uint64 `json:"nonce"`
Balance *hexutil.Big `json:"balance"`
StorageHash common.Hash `json:"storageHash"`
CodeHash common.Hash `json:"codeHash"`
StorageProof []StorageProof `json:"storageProof"`
AccountProof []hexutil.Bytes `json:"accountProof"`
}

type StorageProof struct {
Key HexNibbles `json:"key"`
Value HexNibbles `json:"value"`
Proof []hexutil.Bytes `json:"proof"`
}

type HexNibbles []byte

func (n HexNibbles) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf("0x%v",
new(big.Int).SetBytes(n).Text(16))), nil
}

func (n *HexNibbles) UnmarshalText(input []byte) error {
input = bytes.TrimPrefix(input, []byte("0x"))
v, ok := new(big.Int).SetString(string(input), 16)
if !ok {
return fmt.Errorf("invalid hex input")
}
*n = v.Bytes()
return nil
}

type EthGetProofResponse struct {
Result StorageStateResult `json:"result"`
}
38 changes: 0 additions & 38 deletions storage_proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,15 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"math/big"
"os"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/require"
)

type StorageStateResult struct {
Nonce hexutil.Uint64 `json:"nonce"`
Balance *hexutil.Big `json:"balance"`
StorageHash common.Hash `json:"storageHash"`
CodeHash common.Hash `json:"codeHash"`
StorageProof []StorageProof `json:"storageProof"`
AccountProof []hexutil.Bytes `json:"accountProof"`
}

type StorageProof struct {
Key HexNibbles `json:"key"`
Value HexNibbles `json:"value"`
Proof []hexutil.Bytes `json:"proof"`
}

type HexNibbles []byte

func (n HexNibbles) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf("0x%v",
new(big.Int).SetBytes(n).Text(16))), nil
}

func (n *HexNibbles) UnmarshalText(input []byte) error {
input = bytes.TrimPrefix(input, []byte("0x"))
v, ok := new(big.Int).SetString(string(input), 16)
if !ok {
return fmt.Errorf("invalid hex input")
}
*n = v.Bytes()
return nil
}

type EthGetProofResponse struct {
Result StorageStateResult `json:"result"`
}

func TestStorageTrie(t *testing.T) {
// slot index
slot0 := common.FromHex("0x0000000000000000000000000000000000000000000000000000000000000000") // 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
Expand Down

0 comments on commit ce9e0f1

Please sign in to comment.