From 71b760a16855a375113bfc97954e5dbb2b0d6746 Mon Sep 17 00:00:00 2001 From: Justin Brower Date: Mon, 9 Sep 2024 11:08:42 -0400 Subject: [PATCH] support multicall via codegeneration --- accounts/abi/bind/base.go | 8 ++ accounts/abi/bind/source.go.tpl | 43 ++++++++ accounts/abi/multicall/api.go | 181 ++++++++++++++++++++++++++++++++ accounts/abi/multicall/types.go | 85 +++++++++++++++ accounts/abi/multicall/utils.go | 35 ++++++ 5 files changed, 352 insertions(+) create mode 100644 accounts/abi/multicall/api.go create mode 100644 accounts/abi/multicall/types.go create mode 100644 accounts/abi/multicall/utils.go diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go index c8972a9dff..d888209f2e 100644 --- a/accounts/abi/bind/base.go +++ b/accounts/abi/bind/base.go @@ -120,6 +120,14 @@ type BoundContract struct { filterer ContractFilterer // Event filtering to interact with the blockchain } +func (b *BoundContract) Address() common.Address { + return b.address +} + +func (b *BoundContract) ABI() abi.ABI { + return b.abi +} + // NewBoundContract creates a low level contract interface through which calls // and transactions may be made through. func NewBoundContract(address common.Address, abi abi.ABI, caller ContractCaller, transactor ContractTransactor, filterer ContractFilterer) *BoundContract { diff --git a/accounts/abi/bind/source.go.tpl b/accounts/abi/bind/source.go.tpl index c84862d03b..02a804768c 100644 --- a/accounts/abi/bind/source.go.tpl +++ b/accounts/abi/bind/source.go.tpl @@ -10,6 +10,7 @@ import ( ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/multicall" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -257,6 +258,41 @@ var ( {{end}} } + func (_{{$contract.Type}} *{{$contract.Type}}Caller) {{.Normalized.Name}}Multicall( + {{range .Normalized.Inputs}}{{.Name}} {{bindtype .Type $structs}},{{end}} + ) *multicall.MultiCallMetaData[{{if .Structured}}struct{ {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type $structs}};{{end}} },{{else}}{{range .Normalized.Outputs}}{{bindtype .Type $structs}}]{{end}}{{end}} { + return multicall.Describe( + _{{$contract.Type}}.contract.Address(), + _{{$contract.Type}}.contract.ABI(), + func(data []byte) ({{if .Structured}}struct{ {{range .Normalized.Outputs}}*{{.Name}} {{bindtype .Type $structs}};{{end}} },{{else}}{{range .Normalized.Outputs}}{{bindtype .Type $structs}}{{end}}{{end}}, error) { + out, err := _{{$contract.Type}}.contract.ABI().Unpack("{{.Original.Name}}", data) + if err != nil { + panic(err) + } + {{if .Structured}} + outstruct := new(struct{ {{range .Normalized.Outputs}} {{.Name}} {{bindtype .Type $structs}}; {{end}} }) + if err != nil { + return *outstruct, err + } + {{range $i, $t := .Normalized.Outputs}} + outstruct.{{.Name}} = *abi.ConvertType(out[{{$i}}], new({{bindtype .Type $structs}})).(*{{bindtype .Type $structs}}){{end}} + + return *outstruct, err + {{else}} + if err != nil { + return {{range $i, $_ := .Normalized.Outputs}}*new({{bindtype .Type $structs}}), {{end}} err + } + {{range $i, $t := .Normalized.Outputs}} + out{{$i}} := *abi.ConvertType(out[{{$i}}], new({{bindtype .Type $structs}})).(*{{bindtype .Type $structs}}){{end}} + + return {{range $i, $t := .Normalized.Outputs}}out{{$i}}{{end}}, nil + {{end}} + }, + "{{.Original.Name}}",{{range .Normalized.Inputs}} + {{.Name}},{{end}} + ) + } + // {{.Normalized.Name}} is a free data retrieval call binding the contract method 0x{{printf "%x" .Original.ID}}. // // Solidity: {{.Original.String}} @@ -287,6 +323,13 @@ var ( return _{{$contract.Type}}.Contract.{{.Normalized.Name}}(&_{{$contract.Type}}.TransactOpts {{range $i, $_ := .Normalized.Inputs}}, {{.Name}}{{end}}) } + // {{.Normalized.Name}} is a paid mutator transaction binding the contract method 0x{{printf "%x" .Original.ID}}. + // + // Solidity: {{.Original.String}} + func (_{{$contract.Type}} *{{$contract.Type}}Session) {{.Normalized.Name}}Multicall({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type $structs}} {{end}}) (*types.Transaction, error) { + return _{{$contract.Type}}.Contract.{{.Normalized.Name}}(&_{{$contract.Type}}.TransactOpts {{range $i, $_ := .Normalized.Inputs}}, {{.Name}}{{end}}) + } + // {{.Normalized.Name}} is a paid mutator transaction binding the contract method 0x{{printf "%x" .Original.ID}}. // // Solidity: {{.Original.String}} diff --git a/accounts/abi/multicall/api.go b/accounts/abi/multicall/api.go new file mode 100644 index 0000000000..c34cf4798c --- /dev/null +++ b/accounts/abi/multicall/api.go @@ -0,0 +1,181 @@ +package multicall + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +type MulticallClientOptions struct { + // the address of the multicall3 contract. if unset, defaults to the usual address. + // for sensible values, see: https://www.multicall3.com/deployments + address *common.Address + + // optional: if set, will batch requests to the node. Reasonable values for this include 2048 / 4096 byte chunks. + maxBatchSizeBytes *uint64 +} + +/** + * To use multicall, you will typically- + * - setup a client with `multicall.NewClient()` + * - use the autogenerated bindings on your client (or the manual `multicall.Describe()` to describe calls you want to make) + * - send the calls with `multicall.Perform*()` -- using 3, 4, or many for a variable number of requests. This will dispatch your requests, + * and depending on your settings, may break into multiple eth_call requests. + */ +func NewClient(ctx context.Context, eth *ethclient.Client, opts *MulticallClientOptions) (*MulticallClient, error) { + if eth == nil { + return nil, errors.New("no ethclient passed") + } + + // taken from: https://www.multicall3.com/ + parsed, err := abi.JSON(strings.NewReader(`[{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"aggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes[]","name":"returnData","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bool","name":"allowFailure","type":"bool"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call3[]","name":"calls","type":"tuple[]"}],"name":"aggregate3","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bool","name":"allowFailure","type":"bool"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call3Value[]","name":"calls","type":"tuple[]"}],"name":"aggregate3Value","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"blockAndAggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"getBasefee","outputs":[{"internalType":"uint256","name":"basefee","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"name":"getBlockHash","outputs":[{"internalType":"bytes32","name":"blockHash","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBlockNumber","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getChainId","outputs":[{"internalType":"uint256","name":"chainid","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockCoinbase","outputs":[{"internalType":"address","name":"coinbase","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockDifficulty","outputs":[{"internalType":"uint256","name":"difficulty","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockGasLimit","outputs":[{"internalType":"uint256","name":"gaslimit","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockTimestamp","outputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getEthBalance","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastBlockHash","outputs":[{"internalType":"bytes32","name":"blockHash","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"requireSuccess","type":"bool"},{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"tryAggregate","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bool","name":"requireSuccess","type":"bool"},{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"tryBlockAndAggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"}]`)) + if err != nil { + return nil, fmt.Errorf("error parsing multicall abi: %s", err.Error()) + } + + contractAddress := func() common.Address { + if opts.address == nil { + // also taken from: https://www.multicall3.com/ -- it's deployed at the same addr on most chains + return common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11") + } + return *opts.address + }() + + return &MulticallClient{MaxBatchSize: opts.maxBatchSizeBytes, Context: ctx, ABI: &parsed, Contract: bind.NewBoundContract(contractAddress, parsed, eth, eth, eth)}, nil +} + +func Describe[T any](contractAddress common.Address, abi abi.ABI, deserialize func([]byte) (T, error), method string, params ...interface{}) *MultiCallMetaData[T] { + callData, err := abi.Pack(method, params...) + if err != nil { + panic(fmt.Errorf("error packing multicall: %s", err.Error())) + } + return &MultiCallMetaData[T]{ + Address: contractAddress, + Data: callData, + Deserialize: deserialize, + } +} + +func Perform[A any, B any](mc MulticallClient, a *MultiCallMetaData[A], b *MultiCallMetaData[B]) (*A, *B, error) { + res, err := doMultiCall(mc, a.Raw(), b.Raw()) + if err != nil { + return nil, nil, fmt.Errorf("error performing multicall: %s", err.Error()) + } + return any(res[0].Value).(*A), any(res[1].Value).(*B), nil +} + +func Perform3[A any, B any, C any](mc MulticallClient, a *MultiCallMetaData[A], b *MultiCallMetaData[B], c *MultiCallMetaData[C]) (*A, *B, *C, error) { + res, err := doMultiCall(mc, a.Raw(), b.Raw(), c.Raw()) + if err != nil { + return nil, nil, nil, fmt.Errorf("error performing multicall: %s", err.Error()) + } + return any(res[0].Value).(*A), any(res[1].Value).(*B), any(res[2].Value).(*C), nil +} + +func Perform4[A any, B any, C any, D any](mc MulticallClient, a *MultiCallMetaData[A], b *MultiCallMetaData[B], c *MultiCallMetaData[C], d *MultiCallMetaData[D]) (*A, *B, *C, *D, error) { + res, err := doMultiCall(mc, a.Raw(), b.Raw(), c.Raw(), d.Raw()) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("error performing multicall: %s", err.Error()) + } + return any(res[0].Value).(*A), any(res[1].Value).(*B), any(res[2].Value).(*C), any(res[3].Value).(*D), nil +} + +func Perform5[A any, B any, C any, D any, E any](mc MulticallClient, a *MultiCallMetaData[A], b *MultiCallMetaData[B], c *MultiCallMetaData[C], d *MultiCallMetaData[D], e *MultiCallMetaData[E]) (*A, *B, *C, *D, *E, error) { + res, err := doMultiCall(mc, a.Raw(), b.Raw(), c.Raw(), d.Raw(), e.Raw()) + if err != nil { + return nil, nil, nil, nil, nil, fmt.Errorf("error performing multicall: %s", err.Error()) + } + return any(res[0].Value).(*A), any(res[1].Value).(*B), any(res[2].Value).(*C), any(res[3].Value).(*D), any(res[4].Value).(*E), nil +} + +func PerformMany[A any](mc MulticallClient, requests ...*MultiCallMetaData[A]) (*[]A, error) { + res, err := doMultiCall(mc, mapArray(requests, func(mc *MultiCallMetaData[A], index uint64) RawMulticall { + return mc.Raw() + })...) + if err != nil { + return nil, fmt.Errorf("multicall failed: %s", err.Error()) + } + + // unwind results + unwoundResults := mapArray(res, func(d DeserializedMulticall3Result, i uint64) A { + // force these back to A + return any(d.Value).(A) + }) + return &unwoundResults, nil +} + +func doMultiCall(mc MulticallClient, calls ...RawMulticall) ([]DeserializedMulticall3Result, error) { + typedCalls := make([]ParamMulticall3Call3, len(calls)) + for i, call := range calls { + typedCalls[i] = ParamMulticall3Call3{ + Target: call.Address, + AllowFailure: true, + CallData: call.Data, + } + } + + chunkedCalls := func() [][]ParamMulticall3Call3 { + if mc.MaxBatchSize == nil { + // no batching + return [][]ParamMulticall3Call3{ + typedCalls, + } + } + + return chunkCalls(typedCalls, *mc.MaxBatchSize) + }() + + var results = make([]interface{}, len(calls)) + var totalResults = 0 + + for _, multicalls := range chunkedCalls { + var res []interface{} + // we can't use the generated abi, as we want an eth_call simulation (and aggregate3 is payable, which triggers an eth_sendTransaction in abigen) + err := mc.Contract.Call(&bind.CallOpts{}, &res, "aggregate3", multicalls) + if err != nil { + return nil, fmt.Errorf("aggregate3 failed: %s", err) + } + + multicallResults := *abi.ConvertType(res[0], new([]Multicall3Result)).(*[]Multicall3Result) + for i := 0; i < len(multicallResults); i++ { + results[totalResults+i] = multicallResults[i] + } + totalResults += len(multicallResults) + } + + return mapArray(calls, func(call RawMulticall, i uint64) DeserializedMulticall3Result { + res := results[i].(Multicall3Result) + if res.Success { + if res.ReturnData != nil { + val, err := call.Deserialize(res.ReturnData) + if err != nil { + return DeserializedMulticall3Result{ + Value: err, + Success: false, + } + } else { + return DeserializedMulticall3Result{ + Value: val, + Success: res.Success, + } + } + } else { + return DeserializedMulticall3Result{ + Value: errors.New("no data returned"), + Success: false, + } + } + } else { + return DeserializedMulticall3Result{ + Success: false, + Value: errors.New("call failed"), + } + } + }), nil +} diff --git a/accounts/abi/multicall/types.go b/accounts/abi/multicall/types.go new file mode 100644 index 0000000000..3451539d7b --- /dev/null +++ b/accounts/abi/multicall/types.go @@ -0,0 +1,85 @@ +package multicall + +import ( + "context" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" +) + +type MultiCallMetaData[T interface{}] struct { + Address common.Address + Data []byte + Deserialize func([]byte) (T, error) +} + +type DeserializedMulticall3Result struct { + Success bool + Value any +} + +func (md *MultiCallMetaData[T]) Raw() RawMulticall { + return RawMulticall{ + Address: md.Address, + Data: md.Data, + Deserialize: func(data []byte) (any, error) { + res, err := md.Deserialize(data) + return any(res), err + }, + } +} + +type RawMulticall struct { + Address common.Address + Data []byte + Deserialize func([]byte) (any, error) +} + +type MulticallClient struct { + Contract *bind.BoundContract + ABI *abi.ABI + Context context.Context + MaxBatchSize *uint64 +} + +type Multicall3Result struct { + Success bool + ReturnData []byte +} + +type ParamMulticall3Call3 struct { + Target common.Address + AllowFailure bool + CallData []byte +} + +/* + * Some RPC providers may limit the amount of calldata you can send in one eth_call. This utility + * provides a mechanism for chunking calls by the len(CallData) used. + * + * This function checks whether the calldata appended exceeds maxBatchSizeBytes + */ +func chunkCalls(allCalls []ParamMulticall3Call3, maxBatchSizeBytes uint64) [][]ParamMulticall3Call3 { + results := [][]ParamMulticall3Call3{} + + currentBatchSize := uint64(0) + currentBatch := []ParamMulticall3Call3{} + + for _, call := range allCalls { + if (currentBatchSize + uint64(len(call.CallData))) > maxBatchSizeBytes { + results = append(results, currentBatch) + currentBatchSize = 0 + currentBatch = []ParamMulticall3Call3{} + } + + currentBatch = append(currentBatch, call) + currentBatchSize += uint64(len(call.CallData)) + } + + if len(currentBatch) > 0 { + results = append(results, currentBatch) + } + + return results +} diff --git a/accounts/abi/multicall/utils.go b/accounts/abi/multicall/utils.go new file mode 100644 index 0000000000..045fb55891 --- /dev/null +++ b/accounts/abi/multicall/utils.go @@ -0,0 +1,35 @@ +package multicall + +func mapArray[A any, B any](coll []A, mapper func(i A, index uint64) B) []B { + out := make([]B, len(coll)) + for i, item := range coll { + out[i] = mapper(item, uint64(i)) + } + return out +} + +func filterArray[A any](coll []A, criteria func(i A) bool) []A { + out := []A{} + for _, item := range coll { + if criteria(item) { + out = append(out, item) + } + } + return out +} + +func reduceArray[A any, B any](coll []A, processor func(accum B, next A) B, initialState B) B { + val := initialState + for _, item := range coll { + val = processor(val, item) + } + return val +} + +func flattenArrays[A any](coll [][]A) []A { + out := []A{} + for _, arr := range coll { + out = append(out, arr...) + } + return out +}