-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #151 from Layr-Labs/jb/multicall3
Support `multicall3` in status endpoint
- Loading branch information
Showing
7 changed files
with
298 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
package multicall | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"math" | ||
"strings" | ||
|
||
"github.com/Layr-Labs/eigenpod-proofs-generation/cli/utils" | ||
"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 MultiCallMetaData[T interface{}] struct { | ||
Address common.Address | ||
Data []byte | ||
Deserialize func([]byte) (T, error) | ||
} | ||
|
||
type Multicall3Result struct { | ||
Success bool | ||
ReturnData []byte | ||
} | ||
|
||
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 MulticallContract struct { | ||
Contract *bind.BoundContract | ||
ABI *abi.ABI | ||
Context context.Context | ||
MaxBatchSize uint64 | ||
} | ||
|
||
type ParamMulticall3Call3 struct { | ||
Target common.Address | ||
AllowFailure bool | ||
CallData []byte | ||
} | ||
|
||
// maxBatchSizeBytes - 0: no batching. | ||
func NewMulticallContract(ctx context.Context, eth *ethclient.Client, address *common.Address, maxBatchSizeBytes uint64) (*MulticallContract, 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 address == nil { | ||
// also taken from: https://www.multicall3.com/ -- it's deployed at the same addr on most chains | ||
return common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11") | ||
} | ||
return *address | ||
}() | ||
|
||
return &MulticallContract{MaxBatchSize: maxBatchSizeBytes, Context: ctx, ABI: &parsed, Contract: bind.NewBoundContract(contractAddress, parsed, eth, eth, eth)}, nil | ||
} | ||
|
||
// Call invokes the (constant) contract method with params as input values and | ||
// sets the output to result. The result type might be a single field for simple | ||
// returns, a slice of interfaces for anonymous returns and a struct for named | ||
// returns. | ||
func MultiCall[T any](contractAddress common.Address, abi abi.ABI, deserialize func([]byte) (T, error), method string, params ...interface{}) (*MultiCallMetaData[T], error) { | ||
callData, err := abi.Pack(method, params...) | ||
if err != nil { | ||
return nil, fmt.Errorf("error packing multicall: %s", err.Error()) | ||
} | ||
return &MultiCallMetaData[T]{ | ||
Address: contractAddress, | ||
Data: callData, | ||
Deserialize: deserialize, | ||
}, nil | ||
} | ||
|
||
func DoMultiCall[A any, B any](mc MulticallContract, a *MultiCallMetaData[A], b *MultiCallMetaData[B]) (*A, *B, error) { | ||
res, err := doMultiCallMany(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 DoMultiCallMany[A any](mc MulticallContract, requests ...*MultiCallMetaData[A]) (*[]A, error) { | ||
res, err := doMultiCallMany(mc, utils.Map(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 := utils.Map(res, func(d DeserializedMulticall3Result, i uint64) A { | ||
// force these back to A | ||
return any(d.Value).(A) | ||
}) | ||
return &unwoundResults, nil | ||
} | ||
|
||
/* | ||
* Some RPC providers may limit the amount of calldata you can send in one eth_call, which (for those who have 1000's of validators), means | ||
* you can't just spam one enormous multicall request. | ||
* | ||
* This function checks whether the calldata appended exceeds maxBatchSizeBytes | ||
*/ | ||
func chunkCalls(allCalls []ParamMulticall3Call3, maxBatchSizeBytes int) [][]ParamMulticall3Call3 { | ||
// chunk by the maximum size of calldata, which is 1024 per call. | ||
results := [][]ParamMulticall3Call3{} | ||
currentBatchSize := 0 | ||
currentBatch := []ParamMulticall3Call3{} | ||
|
||
for _, call := range allCalls { | ||
if (currentBatchSize + len(call.CallData)) > maxBatchSizeBytes { | ||
// we can't fit in this batch, so dump the current batch and start a new one | ||
results = append(results, currentBatch) | ||
currentBatchSize = 0 | ||
currentBatch = []ParamMulticall3Call3{} | ||
} | ||
|
||
currentBatch = append(currentBatch, call) | ||
currentBatchSize += len(call.CallData) | ||
} | ||
|
||
// check if we forgot to add the last batch | ||
if len(currentBatch) > 0 { | ||
results = append(results, currentBatch) | ||
} | ||
|
||
return results | ||
} | ||
|
||
func doMultiCallMany(mc MulticallContract, 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, | ||
} | ||
} | ||
|
||
// see if we need to chunk them now | ||
chunkedCalls := chunkCalls(typedCalls, func() int { | ||
if mc.MaxBatchSize == 0 { | ||
return math.MaxInt64 | ||
} else { | ||
return int(mc.MaxBatchSize) | ||
} | ||
}()) | ||
var results = make([]interface{}, len(calls)) | ||
var totalResults = 0 | ||
|
||
for _, multicalls := range chunkedCalls { | ||
var res []interface{} | ||
|
||
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) | ||
|
||
// copy over into master results list | ||
for i := 0; i < len(multicallResults); i++ { | ||
results[totalResults+i] = multicallResults[i] | ||
} | ||
totalResults += len(multicallResults) | ||
} | ||
|
||
// now we should have a bunch of Multicall3Result | ||
outputs := make([]DeserializedMulticall3Result, len(calls)) | ||
for i, call := range calls { | ||
res := results[i].(Multicall3Result) | ||
if res.Success { | ||
if res.ReturnData != nil { | ||
val, err := call.Deserialize(res.ReturnData) | ||
if err != nil { | ||
outputs[i] = DeserializedMulticall3Result{ | ||
Value: err, | ||
Success: false, | ||
} | ||
} else { | ||
outputs[i] = DeserializedMulticall3Result{ | ||
Value: val, | ||
Success: res.Success, | ||
} | ||
} | ||
} else { | ||
outputs[i] = DeserializedMulticall3Result{ | ||
Value: errors.New("no data returned"), | ||
Success: false, | ||
} | ||
} | ||
} else { | ||
outputs[i] = DeserializedMulticall3Result{ | ||
Success: false, | ||
Value: errors.New("call failed"), | ||
} | ||
} | ||
} | ||
|
||
return outputs, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.