Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/todos #2

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f5ff34c
temporary store for urfave implementation
zeuslawyer Feb 26, 2024
1ef71f0
add cli command for to integer
zeuslawyer Feb 29, 2024
d293304
update tests and correct DecodeHexToBigInt
zeuslawyer Mar 8, 2024
797bf9e
refactor to separte encdec and selector packages
zeuslawyer Mar 8, 2024
d03d96b
abiFromSelector working with tests
zeuslawyer Mar 8, 2024
a267a01
Implement SelectorFromSig and SigFromSelector.
zeuslawyer Mar 8, 2024
585d1c6
improve error message where supplied ABI is not an array
zeuslawyer Mar 8, 2024
d224883
Add commands to calculate selector from sig and sig from selector.
zeuslawyer Mar 11, 2024
4dc5b27
refactor package selector and update tests. isValidSlice() still sitt…
zeuslawyer Mar 15, 2024
28f021d
add new WIP function for calculating events signature from 32 byte to…
zeuslawyer Mar 25, 2024
2359925
add topic hash decoder to CLI commands. Update flags
zeuslawyer Mar 25, 2024
cd6ae35
remove validateIsSlice function
zeuslawyer Mar 25, 2024
c08920b
Remove bug affecting the decodeSelector cmd. Update README
zeuslawyer Mar 25, 2024
c1c171a
rename FuncFromSelector and tidy up naming in other functions
zeuslawyer Apr 21, 2024
ac8d618
interim abi.decode working
zeuslawyer May 14, 2024
625d1ad
Added abi.decode functionality. Updated README.
zeuslawyer May 14, 2024
452d94c
update gitignore to exclude hextool
zeuslawyer May 14, 2024
48b1cc8
Abi.Encode working for basic types, but tests not complete
zeuslawyer May 15, 2024
9ccab11
added tests for panic circumstances and some errors
zeuslawyer May 15, 2024
2bfe810
refactor pkg encdec to split encode and decode
zeuslawyer May 16, 2024
92541ec
add abi.encode cmd
zeuslawyer May 16, 2024
16732b5
added logic to check if ABI JSON is arr or object
zeuslawyer May 21, 2024
17d1a00
updated tests to handle abi from array
zeuslawyer May 21, 2024
34e2c2d
Refactor to reuse selector decoding logic for methods and error signa…
zeuslawyer May 21, 2024
afd6850
Merge branch 'main' into feat/urfave. Bring in GH Actions Yaml.
zeuslawyer May 31, 2024
9fd4639
fix redundant newline for tests and build to pass
zeuslawyer May 31, 2024
5e6f263
update gh action name
zeuslawyer May 31, 2024
8475621
Add TestErrorSignFromSelector. Single test working with error with on…
zeuslawyer Jun 3, 2024
a154fda
add more tests for TestErrorSigFromSelector
zeuslawyer Jun 3, 2024
cbb17fa
update errors for TestFuncFromSelector
zeuslawyer Jun 3, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Go Tests
name: Go Build & Test

on:
# Run tests for pull requests to the main branch
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
*.so
*.dylib

# the tool
hextool

# Test binary, built with `go test -c`
*.test

Expand Down
59 changes: 58 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,61 @@
# Motivation

## Reference/Research
- [Function selectors](https://medium.com/coinmonks/function-selectors-in-solidity-understanding-and-working-with-them-25e07755e976#:~:text=The%20function%20signature%20is%20derived,myFunction(address%2Cuint256)%20.)

- [Function selectors](<https://medium.com/coinmonks/function-selectors-in-solidity-understanding-and-working-with-them-25e07755e976#:~:text=The%20function%20signature%20is%20derived,myFunction(address%2Cuint256)%20.>)

## Commands

1. Help : `hextool --help << or -h>>` will list available commands. `hextool help <COMMAND> ` will print out the flags each command accepts or expects.

2. Hex to Int: `hextool toint --hex 0x0000000000000000000000000000000000000000000000000000000000690208` // 6881800

3. Hex to String `hextool tostring --hex 0x486578746f6f6c204d616b657320457468657265756d204465762045617369657221` // Hextool Makes Ethereum Dev Easier!

4. Retrieve function signature if given a function selector (<b>Note: </b> you must pass a path or url to a valid json object that has an `abi` property on it with an ABI array value). See below for examples.
`hextool funcsig --selector 0xa9059cbb --url https://gist.githubusercontent.com/zeuslawyer/ecec03ff3f50311e510c201de4c076d5/raw/f096531942e922cb3f1d5daa2132f0e476356ced/good-data-erc20.json` // transfer(address,uint256)

or, using a path on your file system
`hextool funcsig --path "/PATH/TO/FILE/erc20.abi.json" --selector 0x095ea7b3` // approve(address,uint256)

<br>
Please examine [the shape of the object](https://gist.githubusercontent.com/zeuslawyer/ecec03ff3f50311e510c201de4c076d5/raw/f096531942e922cb3f1d5daa2132f0e476356ced/good-data-erc20.json) for this to work correctly. The ABI json files produced by Hardhat will work too.
<br>

5. Calculate the function selector from the ABI-specified function signature (excludes the word 'function'): `hextool selector --sig 'transfer(address,uint256)'` // 0xa9059cbb
<br>
<b>Note: </b> The signature must be enclosed in single or double quotes.
<br>

6 Decode a method's selector (4 bytes) into the function signature, given a valid ABI file. `hextool decodeMethodSelector --selector 0xa9059cbb --url "https://gist.githubusercontent.com/zeuslawyer/ecec03ff3f50311e510c201de4c076d5/raw/f096531942e922cb3f1d5daa2132f0e476356ced/good-data-erc20.json"`>

> should return transfer(address,uint256)

7 Decode an error's selector (4 bytes) into the error's signature, given a valid ABI file. `hextool decodeErrorSelector --selector 0x07da6ee6 --url https://raw.githubusercontent.com/Cyfrin/ccip-contracts/main/contracts-ccip/abi/v0.8/Router.json`

> should return InsufficientFeeTokenAmount()

8. Decode the topic hash (on block explorers this shows up as `topic0`). The full 32-byte hexstring is needed, as is a valid ABI. `hextool decodeEvent --topic 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef --url "https://gist.githubusercontent.com/zeuslawyer/ecec03ff3f50311e510c201de4c076d5/raw/f096531942e922cb3f1d5daa2132f0e476356ced/good-data-erc20.json"`

9. Decode abi-encoded hex input with `hextool abi.decode --hex <<abi encoded hex>> --types "<<comma separated types in a string>>"`.
Example:

```
hextool abi.decode --hex "00000000000000000000000000000000000000000000000000000000000003e90000000000000000000000000000000000000000000000000000000000000060000000000000000000000000208aa722aca42399eac5192ee778e4d42f4e5de300000000000000000000000000000000000000000000000000000000000000057a7562696e000000000000000000000000000000000000000000000000000000" --types 'uint16,string,address'
```

Produces `[1001 zubin 0x208AA722Aca42399eaC5192EE778e4D42f4E5De3]`.

10. Abi-encode data (with their corresponding values). `hextool abi.encode --types '<<comma separated types in a string>>' --values '<<comma separated data string>>'`

eg: `hextool abi.encode --types 'uint64,string,address' --values '1981,bananas dude!,0x208AA722Aca42399eaC5192EE778e4D42f4E5De3'`
will produce: `0x00000000000000000000000000000000000000000000000000000000000007bd0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000208aa722aca42399eac5192ee778e4d42f4e5de3000000000000000000000000000000000000000000000000000000000000000d62616e616e617320647564652100000000000000000000000000000000000000`

Try and reverse that with `hextool abi.decode`!

## Other projects for research

https://github.com/umbracle/ethgo/tree/main //wrapper pkg
https://github.com/defiweb/go-eth // wrapper pkg

https://gist.github.com/crazygit/9279a3b26461d7cb03e807a6362ec855 // decoding tx logs, and reading contract ABI from etherscan
79 changes: 36 additions & 43 deletions encdec/decode.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,30 @@
package encdec

import (
"encoding/hex"
"fmt"
"math/big"
"regexp"
"strconv"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)

/*
* Decodes `hex` to a string. `hex` must be prexifed with 0x.
*/
func DecodeHexToString(hex string) string {
bytes := hexutil.MustDecode(hex)
return string(bytes) + "\n" // concat newlines so that returned output in terminal pushes terminal prompt "%" to new line.
}

// func DecodeHexToBigInt(hex string) *big.Int {
// if (len(hex) == 0) || (hex == "0x") || (hex == "0x00") {
// return new(big.Int).SetInt64(0)
// }
// // hexutil requires that integers are encoded using the least amount of digits (no leading zero digits).
// hexWithoutPrefix := hex[2:]
// trimmed := strings.TrimLeft(hexWithoutPrefix, "0")
decodedBytes := hexutil.MustDecode(hex)

// bigInt := hexutil.MustDecodeBig("0x" + trimmed)
// return bigInt
// }
return string(decodedBytes) + "\n" // concat newlines so that returned output in terminal pushes terminal prompt "%" to new line.
}

/*
* Decodes `hex` to a Big Int64. `hex` must be prexifed with 0x.
*/
func DecodeHexToBigInt(hex string) *big.Int {
if hex == "0x" {
if hex == "0x" || len(hex) == 0 {
panic(fmt.Sprintf("%q provided as --hex input", hex))
}

Expand All @@ -40,44 +33,44 @@ func DecodeHexToBigInt(hex string) *big.Int {
}

hexWithoutPrefix := hex[2:]
// hexutil requires that integers are encoded using the least amount of digits (no leading zero digits).
// trimmed := strings.TrimLeft(hexWithoutPrefix, "0")

num, err := strconv.ParseInt(hexWithoutPrefix, 16, 64)
if err != nil {
panic(err)
}
return new(big.Int).SetInt64(num)

// bigInt := hexutil.MustDecodeBig("0x" + trimmed)
// return bigInt
}

// TODO: @zeuslawyer resume here.
func FunctionSelector(funcSig string) string {
/*
* Given a hex string and a tuple of types, decode the hex string to the corresponding types.
* `dataTypes` is a comma-separated string of types. Eg: "string, uint, bool, uint".
* The sequence of types in `dataTypes` must match the sequence of values in the hex string.
*/

func AbiDecode(hexInput string, dataTypes string) []any {
if strings.HasPrefix(hexInput, "0x") {
// Strip out the "0x" prefix
hexInput = hexInput[2:]
}

validateInput := func(sig string) error {
re := regexp.MustCompile(`^(\w+)`) // match the first word in a given string
matches := re.FindStringSubmatch(sig)
b, err := hex.DecodeString(hexInput)
if err != nil {
fmt.Printf("Error decoding hex input: %v", err)
}

if len(matches) < 2 {
return fmt.Errorf("unable to extract function name from signature: %s", sig)
}
if len(b) == 0 {
return []any{} // Empty.
}

// validate signature format
signatureRegex := regexp.MustCompile(`^\w+\([^\)]*\)$`)
if !signatureRegex.MatchString(sig) {
return fmt.Errorf("\n%q is not a valid function signature", sig)
}
var args abi.Arguments = dataTypesToAbiArgs(dataTypes)
values, err := args.Unpack(b)
if err != nil {
fmt.Printf("Error Unpacking hex input: %v", err)

return nil
}

if err := validateInput(funcSig); err != nil {
panic(err)
for _, val := range values {
// print type of the value
fmt.Printf("Decoded value '%v' of type %T\n", val, val)
}

funcSigHash := crypto.Keccak256Hash([]byte(funcSig))
selector := funcSigHash.String()[:10] //4 bytes ==8 characters, plus "0x"
return selector
return values
}
84 changes: 44 additions & 40 deletions encdec/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package encdec

import (
"math/big"
"reflect"
"strconv"
"strings"
"testing"

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

func TestDecodeHexToBigInt(t *testing.T) {
Expand All @@ -20,9 +23,13 @@ func TestDecodeHexToBigInt(t *testing.T) {
want: new(big.Int).SetInt64(4617104),
},
{
name: "Empty",
name: "0x panics",
inputHex: "0x",
panics: true,
},
{
name: "Empty String",
inputHex: "0x",
want: new(big.Int).SetInt64(0),
panics: true,
},
{
Expand Down Expand Up @@ -98,55 +105,52 @@ func TestDecodeHexToString(t *testing.T) {
}
}

func TestFunctionSelector(t *testing.T) {
func TestAbiDecode(t *testing.T) {
tests := []struct {
name string
functionSig string
panics bool
want string // Hex string
name string
inputHex string
dataTypes string
want []any
}{
{
name: "greet",
functionSig: "greet(string)",
want: "0xead710c4", // https://www.evm-function-selector.click/
name: "HappyPath#1-0x prefix",
inputHex: "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a676f2d686578746f6f6c00000000000000000000000000000000000000000000",
dataTypes: "string",
want: []any{"go-hextool"}, // see https://adibas03.github.io/online-ethereum-abi-encoder-decoder/#/decode to get decoded scalar values
},
{
name: "HappyPath#2-multiple scalar types",
inputHex: "0x000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000166f4b60000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000d68617070792074657374696e6700000000000000000000000000000000000000",
dataTypes: "uint, uint256, string",
want: []any{big.NewInt(10), big.NewInt(23524534), "happy testing"},
},
{
name: "HappyPath#3-Prefix Gets Added",
inputHex: "000000000000000000000000000000000000000000000000000000000000002b",
dataTypes: "uint16",
want: []any{uint16(43)},
},
{
name: "basic transfer",
functionSig: "transfer(address,uint256)",
want: "0xa9059cbb", // https://www.evm-function-selector.click/
name: "HappyPath#4-multiple scalars including address",
inputHex: "00000000000000000000000000000000000000000000000000000000000003e90000000000000000000000000000000000000000000000000000000000000060000000000000000000000000208aa722aca42399eac5192ee778e4d42f4e5de300000000000000000000000000000000000000000000000000000000000000057a7562696e000000000000000000000000000000000000000000000000000000",
dataTypes: "uint16, string, address",
want: []any{uint16(1001), "zubin", common.HexToAddress("0x208aa722aca42399eac5192ee778e4d42f4e5de3")},
},
{
name: "bad function",
functionSig: "gibberish",
want: "0xa9059cbb",
panics: true,
name: "HappyPath#5-empty input",
inputHex: "0x",
dataTypes: "address",
want: []any{},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.panics {
defer func() {
if r := recover(); r != nil {
// Check if the panic value is as expected
errorString := r.(error).Error()
wantErrorSubString := "not a valid function signature"

if strings.Contains(errorString, wantErrorSubString) == false {
t.Errorf("Expected panic message to contain: %s, got: %v", wantErrorSubString, errorString)
}
} else {
// The function did not panic as expected
t.Error("Expected the function to panic, but it did not")
}
}()

FunctionSelector(tc.functionSig)
} else {
got := FunctionSelector(tc.functionSig)
if got != tc.want {
t.Errorf("FunctionSelector(%s) = %s, want %s", tc.functionSig, got, tc.want)
}
got := AbiDecode(tc.inputHex, tc.dataTypes)
if len(got) != len(tc.want) {
t.Errorf("%s failing because returned slice have unequal length, %d & %d", tc.name, len(got), len(tc.want))
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("AbiDecode() = %v, want %v", got, tc.want)
}
})
}
Expand Down
Loading
Loading