Commit e045f582 authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat[contracts]: add sequencer fee wallet (#1029)

* wip: first draft of the fee wallet

* add fee wallet to dump

* rename to sequencer vault

* add L1 fee wallet to geth config

* add unit tests

* fix geth linting error

* add a basic integration test

* fix broken integration test

* add test for correct storage slot

* add integration test for fee withdrawal

* fix typo in integration tests

* fix a bug bin integration tests

* Update OVM_SequencerFeeVault.sol

* fix bug in contract tests

* chore: add changeset

* fix bug in contract tests
parent baacda34
---
'@eth-optimism/integration-tests': patch
'@eth-optimism/l2geth': patch
'@eth-optimism/contracts': patch
---
Adds new SequencerFeeVault contract to store generated fees
import chai, { expect } from 'chai'
import chaiAsPromised from 'chai-as-promised'
chai.use(chaiAsPromised)
import { BigNumber, utils } from 'ethers'
import { OptimismEnv } from './shared/env'
/* Imports: External */
import { BigNumber, Contract, utils } from 'ethers'
import { TxGasLimit, TxGasPrice } from '@eth-optimism/core-utils'
import { predeploys, getContractInterface } from '@eth-optimism/contracts'
/* Imports: Internal */
import { OptimismEnv } from './shared/env'
import { Direction } from './shared/watcher-utils'
describe('Fee Payment Integration Tests', async () => {
let env: OptimismEnv
const other = '0x1234123412341234123412341234123412341234'
let env: OptimismEnv
before(async () => {
env = await OptimismEnv.new()
})
let ovmSequencerFeeVault: Contract
before(async () => {
ovmSequencerFeeVault = new Contract(
predeploys.OVM_SequencerFeeVault,
getContractInterface('OVM_SequencerFeeVault'),
env.l2Wallet
)
})
it(`Should return a gasPrice of ${TxGasPrice.toString()} wei`, async () => {
const gasPrice = await env.l2Wallet.getGasPrice()
expect(gasPrice).to.deep.eq(TxGasPrice)
......@@ -36,6 +51,9 @@ describe('Fee Payment Integration Tests', async () => {
it('Paying a nonzero but acceptable gasPrice fee', async () => {
const amount = utils.parseEther('0.5')
const balanceBefore = await env.l2Wallet.getBalance()
const feeVaultBalanceBefore = await env.l2Wallet.provider.getBalance(
ovmSequencerFeeVault.address
)
expect(balanceBefore.gt(amount))
const tx = await env.ovmEth.transfer(other, amount)
......@@ -43,10 +61,53 @@ describe('Fee Payment Integration Tests', async () => {
expect(receipt.status).to.eq(1)
const balanceAfter = await env.l2Wallet.getBalance()
const feeVaultBalanceAfter = await env.l2Wallet.provider.getBalance(
ovmSequencerFeeVault.address
)
const expectedFeePaid = tx.gasPrice.mul(tx.gasLimit)
// The fee paid MUST be the receipt.gasUsed, and not the tx.gasLimit
// https://github.com/ethereum-optimism/optimism/blob/0de7a2f9c96a7c4860658822231b2d6da0fefb1d/packages/contracts/contracts/optimistic-ethereum/OVM/accounts/OVM_ECDSAContractAccount.sol#L103
expect(balanceBefore.sub(balanceAfter)).to.be.deep.eq(
tx.gasPrice.mul(tx.gasLimit).add(amount)
expect(balanceBefore.sub(balanceAfter)).to.deep.equal(
expectedFeePaid.add(amount)
)
// Make sure the fee was transferred to the vault.
expect(feeVaultBalanceAfter.sub(feeVaultBalanceBefore)).to.deep.equal(
expectedFeePaid
)
})
it('should not be able to withdraw fees before the minimum is met', async () => {
await expect(ovmSequencerFeeVault.withdraw()).to.be.rejected
})
it('should be able to withdraw fees back to L1 once the minimum is met', async () => {
const l1FeeWallet = await ovmSequencerFeeVault.l1FeeWallet()
const balanceBefore = await env.l1Wallet.provider.getBalance(l1FeeWallet)
// Transfer the minimum required to withdraw.
await env.ovmEth.transfer(
ovmSequencerFeeVault.address,
await ovmSequencerFeeVault.MIN_WITHDRAWAL_AMOUNT()
)
const vaultBalance = await env.ovmEth.balanceOf(
ovmSequencerFeeVault.address
)
// Submit the withdrawal.
const withdrawTx = await ovmSequencerFeeVault.withdraw({
gasPrice: 0, // Need a gasprice of 0 or the balances will include the fee paid during this tx.
})
// Wait for the withdrawal to be relayed to L1.
await env.waitForXDomainTransaction(withdrawTx, Direction.L2ToL1)
// Balance difference should be equal to old L2 balance.
const balanceAfter = await env.l1Wallet.provider.getBalance(l1FeeWallet)
expect(balanceAfter.sub(balanceBefore)).to.deep.equal(
BigNumber.from(vaultBalance)
)
})
})
......@@ -58,7 +58,7 @@ export class OptimismEnv {
// fund the user if needed
const balance = await l2Wallet.getBalance()
if (balance.isZero()) {
await fundUser(watcher, gateway, utils.parseEther('10'))
await fundUser(watcher, gateway, utils.parseEther('20'))
}
const ovmEth = getOvmEth(l2Wallet)
......
......@@ -50,6 +50,7 @@ $ USING_OVM=true ./build/bin/geth \
--eth1.chainid $LAYER1_CHAIN_ID \
--eth1.l1gatewayaddress $ETH1_L1_GATEWAY_ADDRESS \
--eth1.l1crossdomainmessengeraddress $ETH1_L1_CROSS_DOMAIN_MESSENGER_ADDRESS \
--eth1.l1feewalletaddress $ETH1_L1_FEE_WALLET_ADDRESS \
--eth1.addressresolveraddress $ETH1_ADDRESS_RESOLVER_ADDRESS \
--eth1.ctcdeploymentheight $CTC_DEPLOY_HEIGHT \
--eth1.syncservice \
......
......@@ -153,6 +153,7 @@ var (
utils.Eth1SyncServiceEnable,
utils.Eth1CanonicalTransactionChainDeployHeightFlag,
utils.Eth1L1CrossDomainMessengerAddressFlag,
utils.Eth1L1FeeWalletAddressFlag,
utils.Eth1ETHGatewayAddressFlag,
utils.Eth1ChainIdFlag,
utils.RollupClientHttpFlag,
......
......@@ -68,6 +68,7 @@ var AppHelpFlagGroups = []flagGroup{
utils.Eth1SyncServiceEnable,
utils.Eth1CanonicalTransactionChainDeployHeightFlag,
utils.Eth1L1CrossDomainMessengerAddressFlag,
utils.Eth1L1FeeWalletAddressFlag,
utils.Eth1ETHGatewayAddressFlag,
utils.Eth1ChainIdFlag,
utils.RollupClientHttpFlag,
......
......@@ -822,6 +822,12 @@ var (
Value: "0x0000000000000000000000000000000000000000",
EnvVar: "ETH1_L1_CROSS_DOMAIN_MESSENGER_ADDRESS",
}
Eth1L1FeeWalletAddressFlag = cli.StringFlag{
Name: "eth1.l1feewalletaddress",
Usage: "Address of the L1 wallet that will collect fees",
Value: "0x0000000000000000000000000000000000000000",
EnvVar: "ETH1_L1_FEE_WALLET_ADDRESS",
}
Eth1ETHGatewayAddressFlag = cli.StringFlag{
Name: "eth1.l1ethgatewayaddress",
Usage: "Deployment address of the Ethereum gateway",
......@@ -1148,6 +1154,10 @@ func setEth1(ctx *cli.Context, cfg *rollup.Config) {
addr := ctx.GlobalString(Eth1L1CrossDomainMessengerAddressFlag.Name)
cfg.L1CrossDomainMessengerAddress = common.HexToAddress(addr)
}
if ctx.GlobalIsSet(Eth1L1FeeWalletAddressFlag.Name) {
addr := ctx.GlobalString(Eth1L1FeeWalletAddressFlag.Name)
cfg.L1FeeWalletAddress = common.HexToAddress(addr)
}
if ctx.GlobalIsSet(Eth1ETHGatewayAddressFlag.Name) {
addr := ctx.GlobalString(Eth1ETHGatewayAddressFlag.Name)
cfg.L1ETHGatewayAddress = common.HexToAddress(addr)
......@@ -1777,10 +1787,11 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
gasLimit = params.GenesisGasLimit
}
xdomainAddress := cfg.Rollup.L1CrossDomainMessengerAddress
l1FeeWalletAddress := cfg.Rollup.L1FeeWalletAddress
addrManagerOwnerAddress := cfg.Rollup.AddressManagerOwnerAddress
l1ETHGatewayAddress := cfg.Rollup.L1ETHGatewayAddress
stateDumpPath := cfg.Rollup.StateDumpPath
cfg.Genesis = core.DeveloperGenesisBlock(uint64(ctx.GlobalInt(DeveloperPeriodFlag.Name)), developer.Address, xdomainAddress, l1ETHGatewayAddress, addrManagerOwnerAddress, stateDumpPath, chainID, gasLimit)
cfg.Genesis = core.DeveloperGenesisBlock(uint64(ctx.GlobalInt(DeveloperPeriodFlag.Name)), developer.Address, xdomainAddress, l1ETHGatewayAddress, addrManagerOwnerAddress, l1FeeWalletAddress, stateDumpPath, chainID, gasLimit)
if !ctx.GlobalIsSet(MinerGasPriceFlag.Name) && !ctx.GlobalIsSet(MinerLegacyGasPriceFlag.Name) {
cfg.Miner.GasPrice = big.NewInt(1)
}
......
......@@ -98,7 +98,7 @@ func newTester(t *testing.T, confOverride func(*eth.Config)) *tester {
t.Fatalf("failed to create node: %v", err)
}
ethConf := &eth.Config{
Genesis: core.DeveloperGenesisBlock(15, common.Address{}, common.Address{}, common.Address{}, common.Address{}, "", nil, 12000000),
Genesis: core.DeveloperGenesisBlock(15, common.Address{}, common.Address{}, common.Address{}, common.Address{}, common.Address{}, "", nil, 12000000),
Miner: miner.Config{
Etherbase: common.HexToAddress(testAddress),
},
......
......@@ -69,6 +69,7 @@ type Genesis struct {
// OVM Specific, used to initialize the l1XDomainMessengerAddress
// in the genesis state
L1FeeWalletAddress common.Address `json:"-"`
L1CrossDomainMessengerAddress common.Address `json:"-"`
AddressManagerOwnerAddress common.Address `json:"-"`
L1ETHGatewayAddress common.Address `json:"-"`
......@@ -266,7 +267,7 @@ func (g *Genesis) configOrDefault(ghash common.Hash) *params.ChainConfig {
}
// ApplyOvmStateToState applies the initial OVM state to a state object.
func ApplyOvmStateToState(statedb *state.StateDB, stateDump *dump.OvmDump, l1XDomainMessengerAddress common.Address, l1ETHGatewayAddress common.Address, addrManagerOwnerAddress common.Address, chainID *big.Int, gasLimit uint64) {
func ApplyOvmStateToState(statedb *state.StateDB, stateDump *dump.OvmDump, l1XDomainMessengerAddress common.Address, l1ETHGatewayAddress common.Address, addrManagerOwnerAddress common.Address, l1FeeWalletAddress common.Address, chainID *big.Int, gasLimit uint64) {
if len(stateDump.Accounts) == 0 {
return
}
......@@ -330,6 +331,13 @@ func ApplyOvmStateToState(statedb *state.StateDB, stateDump *dump.OvmDump, l1XDo
maxTxGasLimitValue := common.BytesToHash(new(big.Int).SetUint64(gasLimit).Bytes())
statedb.SetState(ExecutionManager.Address, maxTxGasLimitSlot, maxTxGasLimitValue)
}
OVM_SequencerFeeVault, ok := stateDump.Accounts["OVM_SequencerFeeVault"]
if ok {
log.Info("Setting l1FeeWallet in OVM_SequencerFeeVault", "wallet", l1FeeWalletAddress.Hex())
l1FeeWalletSlot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000")
l1FeeWalletValue := common.BytesToHash(l1FeeWalletAddress.Bytes())
statedb.SetState(OVM_SequencerFeeVault.Address, l1FeeWalletSlot, l1FeeWalletValue)
}
}
// ToBlock creates the genesis block and writes state of a genesis specification
......@@ -342,7 +350,7 @@ func (g *Genesis) ToBlock(db ethdb.Database) *types.Block {
if vm.UsingOVM {
// OVM_ENABLED
ApplyOvmStateToState(statedb, g.Config.StateDump, g.L1CrossDomainMessengerAddress, g.L1ETHGatewayAddress, g.AddressManagerOwnerAddress, g.ChainID, g.GasLimit)
ApplyOvmStateToState(statedb, g.Config.StateDump, g.L1CrossDomainMessengerAddress, g.L1ETHGatewayAddress, g.AddressManagerOwnerAddress, g.L1FeeWalletAddress, g.ChainID, g.GasLimit)
}
for addr, account := range g.Alloc {
......@@ -469,7 +477,7 @@ func DefaultGoerliGenesisBlock() *Genesis {
}
// DeveloperGenesisBlock returns the 'geth --dev' genesis block.
func DeveloperGenesisBlock(period uint64, faucet, l1XDomainMessengerAddress common.Address, l1ETHGatewayAddress common.Address, addrManagerOwnerAddress common.Address, stateDumpPath string, chainID *big.Int, gasLimit uint64) *Genesis {
func DeveloperGenesisBlock(period uint64, faucet, l1XDomainMessengerAddress common.Address, l1ETHGatewayAddress common.Address, addrManagerOwnerAddress common.Address, l1FeeWalletAddress common.Address, stateDumpPath string, chainID *big.Int, gasLimit uint64) *Genesis {
// Override the default period to the user requested one
config := *params.AllCliqueProtocolChanges
config.Clique.Period = period
......@@ -525,6 +533,7 @@ func DeveloperGenesisBlock(period uint64, faucet, l1XDomainMessengerAddress comm
common.BytesToAddress([]byte{8}): {Balance: big.NewInt(1)}, // ECPairing
},
L1CrossDomainMessengerAddress: l1XDomainMessengerAddress,
L1FeeWalletAddress: l1FeeWalletAddress,
AddressManagerOwnerAddress: addrManagerOwnerAddress,
L1ETHGatewayAddress: l1ETHGatewayAddress,
ChainID: config.ChainID,
......
......@@ -23,6 +23,7 @@ type Config struct {
// HTTP endpoint of the data transport layer
RollupClientHttp string
L1CrossDomainMessengerAddress common.Address
L1FeeWalletAddress common.Address
AddressManagerOwnerAddress common.Address
L1ETHGatewayAddress common.Address
GasPriceOracleAddress common.Address
......
......@@ -33,6 +33,7 @@ CLI Arguments:
--eth1.chainid - eth1 chain id
--eth1.ctcdeploymentheight - eth1 ctc deploy height
--eth1.l1crossdomainmessengeraddress - eth1 l1 xdomain messenger address
--eth1.l1feewalletaddress - eth l1 fee wallet address
--rollup.statedumppath - http path to the initial state dump
--rollup.clienthttp - rollup client http
--rollup.pollinterval - polling interval for the rollup client
......@@ -127,6 +128,15 @@ while (( "$#" )); do
exit 1
fi
;;
--eth1.l1feewalletaddress)
if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then
ETH1_L1_FEE_WALLET_ADDRESS="$2"
shift 2
else
echo "Error: Argument for $1 is missing" >&2
exit 1
fi
;;
--eth1.l1ethgatewayaddress)
if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then
ETH1_L1_ETH_GATEWAY_ADDRESS="$2"
......@@ -230,6 +240,7 @@ if [[ ! -z "$ROLLUP_SYNC_SERVICE_ENABLE" ]]; then
fi
cmd="$cmd --datadir $DATADIR"
cmd="$cmd --eth1.l1crossdomainmessengeraddress $ETH1_L1_CROSS_DOMAIN_MESSENGER_ADDRESS"
cmd="$cmd --eth1.l1feewalletaddress $ETH1_L1_FEE_WALLET_ADDRESS"
cmd="$cmd --rollup.addressmanagerowneraddress $ADDRESS_MANAGER_OWNER_ADDRESS"
cmd="$cmd --rollup.statedumppath $ROLLUP_STATE_DUMP_PATH"
cmd="$cmd --eth1.ctcdeploymentheight $ETH1_CTC_DEPLOYMENT_HEIGHT"
......
......@@ -8,6 +8,7 @@ import { iOVM_ECDSAContractAccount } from "../../iOVM/accounts/iOVM_ECDSAContrac
/* Library Imports */
import { Lib_EIP155Tx } from "../../libraries/codec/Lib_EIP155Tx.sol";
import { Lib_ExecutionManagerWrapper } from "../../libraries/wrappers/Lib_ExecutionManagerWrapper.sol";
import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
/* Contract Imports */
import { OVM_ETH } from "../predeploys/OVM_ETH.sol";
......@@ -40,7 +41,6 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount {
// TODO: should be the amount sufficient to cover the gas costs of all of the transactions up
// to and including the CALL/CREATE which forms the entrypoint of the transaction.
uint256 constant EXECUTION_VALIDATION_GAS_OVERHEAD = 25000;
OVM_ETH constant ovmETH = OVM_ETH(0x4200000000000000000000000000000000000006);
/********************
......@@ -92,8 +92,8 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount {
// Transfer fee to relayer.
require(
ovmETH.transfer(
msg.sender,
OVM_ETH(Lib_PredeployAddresses.OVM_ETH).transfer(
Lib_PredeployAddresses.SEQUENCER_FEE_WALLET,
SafeMath.mul(transaction.gasLimit, transaction.gasPrice)
),
"Fee was not transferred to relayer."
......@@ -131,7 +131,7 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount {
);
require(
ovmETH.transfer(
OVM_ETH(Lib_PredeployAddresses.OVM_ETH).transfer(
transaction.to,
transaction.value
),
......
// SPDX-License-Identifier: MIT
pragma solidity >0.5.0 <0.8.0;
/* Library Imports */
import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
/* Contract Imports */
import { OVM_ETH } from "../predeploys/OVM_ETH.sol";
/**
* @title OVM_SequencerFeeVault
* @dev Simple holding contract for fees paid to the Sequencer. Likely to be replaced in the future
* but "good enough for now".
*
* Compiler used: optimistic-solc
* Runtime target: OVM
*/
contract OVM_SequencerFeeVault {
/*************
* Constants *
*************/
// Minimum ETH balance that can be withdrawn in a single withdrawal.
uint256 public constant MIN_WITHDRAWAL_AMOUNT = 15 ether;
/*************
* Variables *
*************/
// Address on L1 that will hold the fees once withdrawn. Dynamically initialized within l2geth.
address public l1FeeWallet;
/***************
* Constructor *
***************/
/**
* @param _l1FeeWallet Initial address for the L1 wallet that will hold fees once withdrawn.
* Currently HAS NO EFFECT in production because l2geth will mutate this storage slot during
* the genesis block. This is ONLY for testing purposes.
*/
constructor(
address _l1FeeWallet
) {
l1FeeWallet = _l1FeeWallet;
}
/********************
* Public Functions *
********************/
function withdraw()
public
{
uint256 balance = OVM_ETH(Lib_PredeployAddresses.OVM_ETH).balanceOf(address(this));
require(
balance >= MIN_WITHDRAWAL_AMOUNT,
"OVM_SequencerFeeVault: withdrawal amount must be greater than minimum withdrawal amount"
);
OVM_ETH(Lib_PredeployAddresses.OVM_ETH).withdrawTo(
l1FeeWallet,
balance,
0,
bytes("")
);
}
}
......@@ -14,5 +14,6 @@ library Lib_PredeployAddresses {
address internal constant L2_CROSS_DOMAIN_MESSENGER = 0x4200000000000000000000000000000000000007;
address internal constant LIB_ADDRESS_MANAGER = 0x4200000000000000000000000000000000000008;
address internal constant PROXY_EOA = 0x4200000000000000000000000000000000000009;
address internal constant SEQUENCER_FEE_WALLET = 0x4200000000000000000000000000000000000011;
address internal constant ERC1820_REGISTRY = 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24;
}
......@@ -275,5 +275,9 @@ export const makeContractDeployConfig = async (
config.gasPriceOracleConfig.initialGasPrice,
],
},
OVM_SequencerFeeVault: {
factory: getContractFactory('OVM_SequencerFeeVault'),
params: [`0x${'11'.repeat(20)}`],
},
}
}
......@@ -19,5 +19,6 @@ export const predeploys = {
OVM_ProxyEOA: '0x4200000000000000000000000000000000000009',
OVM_ExecutionManagerWrapper: '0x420000000000000000000000000000000000000B',
OVM_GasPriceOracle: '0x420000000000000000000000000000000000000F',
OVM_SequencerFeeVault: '0x4200000000000000000000000000000000000011',
ERC1820Registry: '0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24',
}
......@@ -137,6 +137,7 @@ export const makeStateDump = async (cfg: RollupDeployConfig): Promise<any> => {
'OVM_ETH',
'OVM_ExecutionManagerWrapper',
'OVM_GasPriceOracle',
'OVM_SequencerFeeVault',
],
deployOverrides: {},
waitForReceipts: false,
......@@ -159,6 +160,7 @@ export const makeStateDump = async (cfg: RollupDeployConfig): Promise<any> => {
'OVM_ProxyEOA',
'OVM_ExecutionManagerWrapper',
'OVM_GasPriceOracle',
'OVM_SequencerFeeVault',
]
const deploymentResult = await deploy(config)
......
import { expect } from '../../../setup'
/* Imports: External */
import hre from 'hardhat'
import { MockContract, smockit } from '@eth-optimism/smock'
import { Contract, Signer } from 'ethers'
/* Imports: Internal */
import { predeploys } from '../../../../src'
describe('OVM_SequencerFeeVault', () => {
let signer1: Signer
before(async () => {
;[signer1] = await hre.ethers.getSigners()
})
let Mock__OVM_ETH: MockContract
before(async () => {
Mock__OVM_ETH = await smockit('OVM_ETH', {
address: predeploys.OVM_ETH,
})
})
let OVM_SequencerFeeVault: Contract
beforeEach(async () => {
const factory = await hre.ethers.getContractFactory('OVM_SequencerFeeVault')
OVM_SequencerFeeVault = await factory.deploy(await signer1.getAddress())
})
describe('withdraw', async () => {
it('should fail if the contract does not have more than the minimum balance', async () => {
Mock__OVM_ETH.smocked.balanceOf.will.return.with(0)
await expect(OVM_SequencerFeeVault.withdraw()).to.be.reverted
})
it('should succeed when the contract has exactly sufficient balance', async () => {
const amount = await OVM_SequencerFeeVault.MIN_WITHDRAWAL_AMOUNT()
Mock__OVM_ETH.smocked.balanceOf.will.return.with(amount)
await expect(OVM_SequencerFeeVault.withdraw()).to.not.be.reverted
expect(Mock__OVM_ETH.smocked.withdrawTo.calls[0]).to.deep.equal([
await signer1.getAddress(),
amount,
0,
'0x',
])
})
it('should succeed when the contract has more than sufficient balance', async () => {
const amount = hre.ethers.utils.parseEther('100')
Mock__OVM_ETH.smocked.balanceOf.will.return.with(amount)
await expect(OVM_SequencerFeeVault.withdraw()).to.not.be.reverted
expect(Mock__OVM_ETH.smocked.withdrawTo.calls[0]).to.deep.equal([
await signer1.getAddress(),
amount,
0,
'0x',
])
})
it('should have an owner in storage slot 0x00...00', async () => {
// Deploy a new temporary instance with an address that's easier to make assertions about.
const factory = await hre.ethers.getContractFactory(
'OVM_SequencerFeeVault'
)
OVM_SequencerFeeVault = await factory.deploy(`0x${'11'.repeat(20)}`)
expect(
await hre.ethers.provider.getStorageAt(
OVM_SequencerFeeVault.address,
hre.ethers.constants.HashZero
)
).to.equal(`0x000000000000000000000000${'11'.repeat(20)}`)
})
})
})
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