Skip to content

Commit

Permalink
feat[contracts]: add sequencer fee wallet (#1029)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
smartcontracts authored Jun 9, 2021
1 parent baacda3 commit e045f58
Show file tree
Hide file tree
Showing 18 changed files with 279 additions and 15 deletions.
7 changes: 7 additions & 0 deletions .changeset/shy-brooms-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@eth-optimism/integration-tests': patch
'@eth-optimism/l2geth': patch
'@eth-optimism/contracts': patch
---

Adds new SequencerFeeVault contract to store generated fees
71 changes: 66 additions & 5 deletions integration-tests/test/fee-payment.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
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)
Expand All @@ -36,17 +51,63 @@ 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)
const receipt = await tx.wait()
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)
)
})
})
2 changes: 1 addition & 1 deletion integration-tests/test/shared/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions l2geth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
1 change: 1 addition & 0 deletions l2geth/cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ var (
utils.Eth1SyncServiceEnable,
utils.Eth1CanonicalTransactionChainDeployHeightFlag,
utils.Eth1L1CrossDomainMessengerAddressFlag,
utils.Eth1L1FeeWalletAddressFlag,
utils.Eth1ETHGatewayAddressFlag,
utils.Eth1ChainIdFlag,
utils.RollupClientHttpFlag,
Expand Down
1 change: 1 addition & 0 deletions l2geth/cmd/geth/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ var AppHelpFlagGroups = []flagGroup{
utils.Eth1SyncServiceEnable,
utils.Eth1CanonicalTransactionChainDeployHeightFlag,
utils.Eth1L1CrossDomainMessengerAddressFlag,
utils.Eth1L1FeeWalletAddressFlag,
utils.Eth1ETHGatewayAddressFlag,
utils.Eth1ChainIdFlag,
utils.RollupClientHttpFlag,
Expand Down
13 changes: 12 additions & 1 deletion l2geth/cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion l2geth/console/console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
Expand Down
15 changes: 12 additions & 3 deletions l2geth/core/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions l2geth/rollup/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions l2geth/scripts/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);


/********************
Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -131,7 +131,7 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount {
);

require(
ovmETH.transfer(
OVM_ETH(Lib_PredeployAddresses.OVM_ETH).transfer(
transaction.to,
transaction.value
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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("")
);
}
}
Loading

0 comments on commit e045f58

Please sign in to comment.