Commit 19599c7d authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into dependabot/go_modules/github.com/docker/docker-20.10.24incompatible

parents 4761fede d0ee681f
[submodule "tests"] [submodule "tests"]
path = l2geth/tests/testdata path = l2geth/tests/testdata
url = https://github.com/ethereum/tests url = https://github.com/ethereum/tests
[submodule "packages/contracts-periphery/lib/multicall"]
path = packages/contracts-periphery/lib/multicall
url = https://github.com/mds1/multicall
[submodule "lib/multicall"]
branch = v3.1.0
...@@ -447,7 +447,7 @@ func TestBigL2Txs(gt *testing.T) { ...@@ -447,7 +447,7 @@ func TestBigL2Txs(gt *testing.T) {
require.NoError(t, err) require.NoError(t, err)
gas, err := core.IntrinsicGas(data, nil, false, true, true, false) gas, err := core.IntrinsicGas(data, nil, false, true, true, false)
require.NoError(t, err) require.NoError(t, err)
if gas > engine.l2GasPool.Gas() { if gas > engine.engineApi.RemainingBlockGas() {
break break
} }
tx := types.MustSignNewTx(dp.Secrets.Alice, signer, &types.DynamicFeeTx{ tx := types.MustSignNewTx(dp.Secrets.Alice, signer, &types.DynamicFeeTx{
......
...@@ -3,12 +3,12 @@ package actions ...@@ -3,12 +3,12 @@ package actions
import ( import (
"errors" "errors"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-program/l2/engineapi"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
geth "github.com/ethereum/go-ethereum/eth" geth "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/ethconfig"
...@@ -38,22 +38,10 @@ type L2Engine struct { ...@@ -38,22 +38,10 @@ type L2Engine struct {
rollupGenesis *rollup.Genesis rollupGenesis *rollup.Genesis
// L2 evm / chain // L2 evm / chain
l2Chain *core.BlockChain l2Chain *core.BlockChain
l2Database ethdb.Database l2Signer types.Signer
l2Cfg *core.Genesis
l2Signer types.Signer engineApi *engineapi.L2EngineAPI
// L2 block building data
l2BuildingHeader *types.Header // block header that we add txs to for block building
l2BuildingState *state.StateDB // state used for block building
l2GasPool *core.GasPool // track gas used of ongoing building
pendingIndices map[common.Address]uint64 // per account, how many txs from the pool were already included in the block, since the pool is lagging behind block mining.
l2Transactions []*types.Transaction // collects txs that were successfully included into current block build
l2Receipts []*types.Receipt // collect receipts of ongoing building
l2ForceEmpty bool // when no additional txs may be processed (i.e. when sequencer drift runs out)
l2TxFailed []*types.Transaction // log of failed transactions which could not be included
payloadID engine.PayloadID // ID of payload that is currently being built
failL2RPC error // mock error failL2RPC error // mock error
} }
...@@ -61,6 +49,38 @@ type L2Engine struct { ...@@ -61,6 +49,38 @@ type L2Engine struct {
type EngineOption func(ethCfg *ethconfig.Config, nodeCfg *node.Config) error type EngineOption func(ethCfg *ethconfig.Config, nodeCfg *node.Config) error
func NewL2Engine(t Testing, log log.Logger, genesis *core.Genesis, rollupGenesisL1 eth.BlockID, jwtPath string, options ...EngineOption) *L2Engine { func NewL2Engine(t Testing, log log.Logger, genesis *core.Genesis, rollupGenesisL1 eth.BlockID, jwtPath string, options ...EngineOption) *L2Engine {
n, ethBackend, apiBackend := newBackend(t, genesis, jwtPath, options)
engineApi := engineapi.NewL2EngineAPI(log, apiBackend)
chain := ethBackend.BlockChain()
genesisBlock := chain.Genesis()
eng := &L2Engine{
log: log,
node: n,
eth: ethBackend,
rollupGenesis: &rollup.Genesis{
L1: rollupGenesisL1,
L2: eth.BlockID{Hash: genesisBlock.Hash(), Number: genesisBlock.NumberU64()},
L2Time: genesis.Timestamp,
},
l2Chain: chain,
l2Signer: types.LatestSigner(genesis.Config),
engineApi: engineApi,
}
// register the custom engine API, so we can serve engine requests while having more control
// over sequencing of individual txs.
n.RegisterAPIs([]rpc.API{
{
Namespace: "engine",
Service: eng.engineApi,
Authenticated: true,
},
})
require.NoError(t, n.Start(), "failed to start L2 op-geth node")
return eng
}
func newBackend(t e2eutils.TestingBase, genesis *core.Genesis, jwtPath string, options []EngineOption) (*node.Node, *geth.Ethereum, *engineApiBackend) {
ethCfg := &ethconfig.Config{ ethCfg := &ethconfig.Config{
NetworkId: genesis.Config.ChainID.Uint64(), NetworkId: genesis.Config.ChainID.Uint64(),
Genesis: genesis, Genesis: genesis,
...@@ -89,33 +109,26 @@ func NewL2Engine(t Testing, log log.Logger, genesis *core.Genesis, rollupGenesis ...@@ -89,33 +109,26 @@ func NewL2Engine(t Testing, log log.Logger, genesis *core.Genesis, rollupGenesis
chain := backend.BlockChain() chain := backend.BlockChain()
db := backend.ChainDb() db := backend.ChainDb()
genesisBlock := chain.Genesis() apiBackend := &engineApiBackend{
eng := &L2Engine{ BlockChain: chain,
log: log, db: db,
node: n, genesis: genesis,
eth: backend,
rollupGenesis: &rollup.Genesis{
L1: rollupGenesisL1,
L2: eth.BlockID{Hash: genesisBlock.Hash(), Number: genesisBlock.NumberU64()},
L2Time: genesis.Timestamp,
},
l2Chain: chain,
l2Database: db,
l2Cfg: genesis,
l2Signer: types.LatestSigner(genesis.Config),
} }
// register the custom engine API, so we can serve engine requests while having more control return n, backend, apiBackend
// over sequencing of individual txs. }
n.RegisterAPIs([]rpc.API{
{
Namespace: "engine",
Service: (*L2EngineAPI)(eng),
Authenticated: true,
},
})
require.NoError(t, n.Start(), "failed to start L2 op-geth node")
return eng type engineApiBackend struct {
*core.BlockChain
db ethdb.Database
genesis *core.Genesis
}
func (e *engineApiBackend) Database() ethdb.Database {
return e.db
}
func (e *engineApiBackend) Genesis() *core.Genesis {
return e.genesis
} }
func (s *L2Engine) EthClient() *ethclient.Client { func (s *L2Engine) EthClient() *ethclient.Client {
...@@ -158,39 +171,25 @@ func (e *L2Engine) ActL2RPCFail(t Testing) { ...@@ -158,39 +171,25 @@ func (e *L2Engine) ActL2RPCFail(t Testing) {
// ActL2IncludeTx includes the next transaction from the given address in the block that is being built // ActL2IncludeTx includes the next transaction from the given address in the block that is being built
func (e *L2Engine) ActL2IncludeTx(from common.Address) Action { func (e *L2Engine) ActL2IncludeTx(from common.Address) Action {
return func(t Testing) { return func(t Testing) {
if e.l2BuildingHeader == nil { if e.engineApi.ForcedEmpty() {
t.InvalidAction("not currently building a block, cannot include tx from queue")
return
}
if e.l2ForceEmpty {
e.log.Info("Skipping including a transaction because e.L2ForceEmpty is true") e.log.Info("Skipping including a transaction because e.L2ForceEmpty is true")
// t.InvalidAction("cannot include any sequencer txs")
return return
} }
i := e.pendingIndices[from] i := e.engineApi.PendingIndices(from)
txs, q := e.eth.TxPool().ContentFrom(from) txs, q := e.eth.TxPool().ContentFrom(from)
if uint64(len(txs)) <= i { if uint64(len(txs)) <= i {
t.Fatalf("no pending txs from %s, and have %d unprocessable queued txs from this account", from, len(q)) t.Fatalf("no pending txs from %s, and have %d unprocessable queued txs from this account", from, len(q))
} }
tx := txs[i] tx := txs[i]
if tx.Gas() > e.l2BuildingHeader.GasLimit { err := e.engineApi.IncludeTx(tx, from)
t.Fatalf("tx consumes %d gas, more than available in L2 block %d", tx.Gas(), e.l2BuildingHeader.GasLimit) if errors.Is(err, engineapi.ErrNotBuildingBlock) {
} t.InvalidAction(err.Error())
if tx.Gas() > uint64(*e.l2GasPool) { } else if errors.Is(err, engineapi.ErrUsesTooMuchGas) {
t.InvalidAction("action takes too much gas: %d, only have %d", tx.Gas(), uint64(*e.l2GasPool)) t.InvalidAction("included tx uses too much gas: %v", err)
return } else if err != nil {
} t.Fatalf("include tx: %v", err)
e.pendingIndices[from] = i + 1 // won't retry the tx
e.l2BuildingState.SetTxContext(tx.Hash(), len(e.l2Transactions))
receipt, err := core.ApplyTransaction(e.l2Cfg.Config, e.l2Chain, &e.l2BuildingHeader.Coinbase,
e.l2GasPool, e.l2BuildingState, e.l2BuildingHeader, tx, &e.l2BuildingHeader.GasUsed, *e.l2Chain.GetVMConfig())
if err != nil {
e.l2TxFailed = append(e.l2TxFailed, tx)
t.Fatalf("failed to apply transaction to L2 block (tx %d): %v", len(e.l2Transactions), err)
} }
e.l2Receipts = append(e.l2Receipts, receipt)
e.l2Transactions = append(e.l2Transactions, tx)
} }
} }
......
...@@ -4,6 +4,8 @@ import ( ...@@ -4,6 +4,8 @@ import (
"math/big" "math/big"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-program/l2/engineapi"
"github.com/ethereum-optimism/optimism/op-program/l2/engineapi/test"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/beacon"
"github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/consensus/ethash"
...@@ -187,3 +189,15 @@ func TestL2EngineAPIFail(gt *testing.T) { ...@@ -187,3 +189,15 @@ func TestL2EngineAPIFail(gt *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(gt, sd.L2Cfg.ToBlock().Hash(), head.Hash(), "expecting engine to start at genesis") require.Equal(gt, sd.L2Cfg.ToBlock().Hash(), head.Hash(), "expecting engine to start at genesis")
} }
func TestEngineAPITests(t *testing.T) {
test.RunEngineAPITests(t, func() engineapi.EngineBackend {
jwtPath := e2eutils.WriteDefaultJWT(t)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
sd := e2eutils.Setup(t, dp, defaultAlloc)
n, _, apiBackend := newBackend(t, sd.L2Cfg, jwtPath, nil)
err := n.Start()
require.NoError(t, err)
return apiBackend
})
}
...@@ -98,7 +98,7 @@ func TestL2Sequencer_SequencerDrift(gt *testing.T) { ...@@ -98,7 +98,7 @@ func TestL2Sequencer_SequencerDrift(gt *testing.T) {
// We passed the sequencer drift: we can still keep the old origin, but can't include any txs // We passed the sequencer drift: we can still keep the old origin, but can't include any txs
sequencer.ActL2KeepL1Origin(t) sequencer.ActL2KeepL1Origin(t)
sequencer.ActL2StartBlock(t) sequencer.ActL2StartBlock(t)
require.True(t, engine.l2ForceEmpty, "engine should not be allowed to include anything after sequencer drift is surpassed") require.True(t, engine.engineApi.ForcedEmpty(), "engine should not be allowed to include anything after sequencer drift is surpassed")
} }
// TestL2Sequencer_SequencerOnlyReorg regression-tests a Goerli halt where the sequencer // TestL2Sequencer_SequencerOnlyReorg regression-tests a Goerli halt where the sequencer
......
This diff is collapsed.
...@@ -4,4 +4,6 @@ import "github.com/ethereum/go-ethereum/core/types" ...@@ -4,4 +4,6 @@ import "github.com/ethereum/go-ethereum/core/types"
type NoopTxMetrics struct{} type NoopTxMetrics struct{}
func (*NoopTxMetrics) RecordL1GasFee(*types.Receipt) {} func (*NoopTxMetrics) RecordL1GasFee(*types.Receipt) {}
func (*NoopTxMetrics) RecordGasBumpCount(int) {}
func (*NoopTxMetrics) RecordTxConfirmationLatency(int64) {}
...@@ -10,10 +10,14 @@ import ( ...@@ -10,10 +10,14 @@ import (
type TxMetricer interface { type TxMetricer interface {
RecordL1GasFee(receipt *types.Receipt) RecordL1GasFee(receipt *types.Receipt)
RecordGasBumpCount(times int)
RecordTxConfirmationLatency(latency int64)
} }
type TxMetrics struct { type TxMetrics struct {
TxL1GasFee prometheus.Gauge TxL1GasFee prometheus.Gauge
TxGasBump prometheus.Gauge
LatencyConfirmedTx prometheus.Gauge
} }
var _ TxMetricer = (*TxMetrics)(nil) var _ TxMetricer = (*TxMetrics)(nil)
...@@ -26,9 +30,29 @@ func MakeTxMetrics(ns string, factory metrics.Factory) TxMetrics { ...@@ -26,9 +30,29 @@ func MakeTxMetrics(ns string, factory metrics.Factory) TxMetrics {
Help: "L1 gas fee for transactions in GWEI", Help: "L1 gas fee for transactions in GWEI",
Subsystem: "txmgr", Subsystem: "txmgr",
}), }),
TxGasBump: factory.NewGauge(prometheus.GaugeOpts{
Namespace: ns,
Name: "tx_gas_bump",
Help: "Number of times a transaction gas needed to be bumped before it got included",
Subsystem: "txmgr",
}),
LatencyConfirmedTx: factory.NewGauge(prometheus.GaugeOpts{
Namespace: ns,
Name: "tx_confirmed_latency_ms",
Help: "Latency of a confirmed transaction in milliseconds",
Subsystem: "txmgr",
}),
} }
} }
func (t *TxMetrics) RecordL1GasFee(receipt *types.Receipt) { func (t *TxMetrics) RecordL1GasFee(receipt *types.Receipt) {
t.TxL1GasFee.Set(float64(receipt.EffectiveGasPrice.Uint64() * receipt.GasUsed / params.GWei)) t.TxL1GasFee.Set(float64(receipt.EffectiveGasPrice.Uint64() * receipt.GasUsed / params.GWei))
} }
func (t *TxMetrics) RecordGasBumpCount(times int) {
t.TxGasBump.Set(float64(times))
}
func (t *TxMetrics) RecordTxConfirmationLatency(latency int64) {
t.LatencyConfirmedTx.Set(float64(latency))
}
...@@ -216,6 +216,7 @@ func (m *SimpleTxManager) send(ctx context.Context, tx *types.Transaction) (*typ ...@@ -216,6 +216,7 @@ func (m *SimpleTxManager) send(ctx context.Context, tx *types.Transaction) (*typ
ticker := time.NewTicker(m.cfg.ResubmissionTimeout) ticker := time.NewTicker(m.cfg.ResubmissionTimeout)
defer ticker.Stop() defer ticker.Stop()
bumpCounter := 0
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
...@@ -231,12 +232,14 @@ func (m *SimpleTxManager) send(ctx context.Context, tx *types.Transaction) (*typ ...@@ -231,12 +232,14 @@ func (m *SimpleTxManager) send(ctx context.Context, tx *types.Transaction) (*typ
// Increase the gas price & submit the new transaction // Increase the gas price & submit the new transaction
tx = m.increaseGasPrice(ctx, tx) tx = m.increaseGasPrice(ctx, tx)
wg.Add(1) wg.Add(1)
bumpCounter += 1
go sendTxAsync(tx) go sendTxAsync(tx)
case <-ctx.Done(): case <-ctx.Done():
return nil, ctx.Err() return nil, ctx.Err()
case receipt := <-receiptChan: case receipt := <-receiptChan:
m.metr.RecordGasBumpCount(bumpCounter)
return receipt, nil return receipt, nil
} }
} }
...@@ -251,6 +254,7 @@ func (m *SimpleTxManager) publishAndWaitForTx(ctx context.Context, tx *types.Tra ...@@ -251,6 +254,7 @@ func (m *SimpleTxManager) publishAndWaitForTx(ctx context.Context, tx *types.Tra
cCtx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout) cCtx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout)
defer cancel() defer cancel()
t := time.Now()
err := m.backend.SendTransaction(cCtx, tx) err := m.backend.SendTransaction(cCtx, tx)
sendState.ProcessSendError(err) sendState.ProcessSendError(err)
...@@ -282,6 +286,7 @@ func (m *SimpleTxManager) publishAndWaitForTx(ctx context.Context, tx *types.Tra ...@@ -282,6 +286,7 @@ func (m *SimpleTxManager) publishAndWaitForTx(ctx context.Context, tx *types.Tra
} }
select { select {
case receiptChan <- receipt: case receiptChan <- receipt:
m.metr.RecordTxConfirmationLatency(time.Since(t).Milliseconds())
m.metr.RecordL1GasFee(receipt) m.metr.RecordL1GasFee(receipt)
default: default:
} }
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { Multicall3 } from "multicall/src/Multicall3.sol";
/**
* Just exists so we can compile this contract.
*/
contract MulticallContractCompiler {
}
...@@ -6,40 +6,54 @@ import { ...@@ -6,40 +6,54 @@ import {
ERC721BurnableUpgradeable ERC721BurnableUpgradeable
} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol"; } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol";
import { AttestationStation } from "./AttestationStation.sol"; import { AttestationStation } from "./AttestationStation.sol";
import { OptimistAllowlist } from "./OptimistAllowlist.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
/** /**
* @author Optimism Collective * @author Optimism Collective
* @author Gitcoin * @author Gitcoin
* @title Optimist * @title Optimist
* @notice A Soul Bound Token for real humans only(tm). * @notice A Soul Bound Token for real humans only(tm).
*/ */
contract Optimist is ERC721BurnableUpgradeable, Semver { contract Optimist is ERC721BurnableUpgradeable, Semver {
/**
* @notice Attestation key used by the attestor to attest the baseURI.
*/
bytes32 public constant BASE_URI_ATTESTATION_KEY = bytes32("optimist.base-uri");
/**
* @notice Attestor who attests to baseURI.
*/
address public immutable BASE_URI_ATTESTOR;
/** /**
* @notice Address of the AttestationStation contract. * @notice Address of the AttestationStation contract.
*/ */
AttestationStation public immutable ATTESTATION_STATION; AttestationStation public immutable ATTESTATION_STATION;
/** /**
* @notice Attestor who attests to baseURI and allowlist. * @notice Address of the OptimistAllowlist contract.
*/ */
address public immutable ATTESTOR; OptimistAllowlist public immutable OPTIMIST_ALLOWLIST;
/** /**
* @custom:semver 1.0.0 * @custom:semver 2.0.0
* @param _name Token name. * @param _name Token name.
* @param _symbol Token symbol. * @param _symbol Token symbol.
* @param _attestor Address of the attestor. * @param _baseURIAttestor Address of the baseURI attestor.
* @param _attestationStation Address of the AttestationStation contract. * @param _attestationStation Address of the AttestationStation contract.
* @param _optimistAllowlist Address of the OptimistAllowlist contract
*/ */
constructor( constructor(
string memory _name, string memory _name,
string memory _symbol, string memory _symbol,
address _attestor, address _baseURIAttestor,
AttestationStation _attestationStation AttestationStation _attestationStation,
) Semver(1, 0, 0) { OptimistAllowlist _optimistAllowlist
ATTESTOR = _attestor; ) Semver(2, 0, 0) {
BASE_URI_ATTESTOR = _baseURIAttestor;
ATTESTATION_STATION = _attestationStation; ATTESTATION_STATION = _attestationStation;
OPTIMIST_ALLOWLIST = _optimistAllowlist;
initialize(_name, _symbol); initialize(_name, _symbol);
} }
...@@ -76,7 +90,7 @@ contract Optimist is ERC721BurnableUpgradeable, Semver { ...@@ -76,7 +90,7 @@ contract Optimist is ERC721BurnableUpgradeable, Semver {
string( string(
abi.encodePacked( abi.encodePacked(
ATTESTATION_STATION.attestations( ATTESTATION_STATION.attestations(
ATTESTOR, BASE_URI_ATTESTOR,
address(this), address(this),
bytes32("optimist.base-uri") bytes32("optimist.base-uri")
) )
...@@ -105,17 +119,15 @@ contract Optimist is ERC721BurnableUpgradeable, Semver { ...@@ -105,17 +119,15 @@ contract Optimist is ERC721BurnableUpgradeable, Semver {
} }
/** /**
* @notice Checks whether a given address is allowed to mint the Optimist NFT yet. Since the * @notice Checks OptimistAllowlist to determine whether a given address is allowed to mint
* Optimist NFT will also be used as part of the Citizens House, mints are currently * the Optimist NFT. Since the Optimist NFT will also be used as part of the
* restricted. Eventually anyone will be able to mint. * Citizens House, mints are currently restricted. Eventually anyone will be able
* to mint.
* *
* @return Whether or not the address is allowed to mint yet. * @return Whether or not the address is allowed to mint yet.
*/ */
function isOnAllowList(address _recipient) public view returns (bool) { function isOnAllowList(address _recipient) public view returns (bool) {
return return OPTIMIST_ALLOWLIST.isAllowedToMint(_recipient);
ATTESTATION_STATION
.attestations(ATTESTOR, _recipient, bytes32("optimist.can-mint"))
.length > 0;
} }
/** /**
......
...@@ -16,9 +16,13 @@ remappings = [ ...@@ -16,9 +16,13 @@ remappings = [
'@rari-capital/solmate/=node_modules/@rari-capital/solmate', '@rari-capital/solmate/=node_modules/@rari-capital/solmate',
'forge-std/=node_modules/forge-std/src', 'forge-std/=node_modules/forge-std/src',
'ds-test/=node_modules/ds-test/src', 'ds-test/=node_modules/ds-test/src',
'multicall/=lib/multicall',
'@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/', '@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/',
'@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/', '@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/',
'@eth-optimism/contracts-bedrock/=../../node_modules/@eth-optimism/contracts-bedrock', '@eth-optimism/contracts-bedrock/=../../node_modules/@eth-optimism/contracts-bedrock',
] ]
# The metadata hash can be removed from the bytecode by setting "none" # The metadata hash can be removed from the bytecode by setting "none"
bytecode_hash = "none" bytecode_hash = "none"
libs = ["node_modules", "lib"]
# Required to use `deployCode` to deploy the multicall contract which has incompatible version
fs_permissions = [{ access = "read", path = "./forge-artifacts/Multicall3.sol/Multicall3.json"}]
Subproject commit a1fa0644fa412cd3237ef7081458ecb2ffad7dbe
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment