-
Notifications
You must be signed in to change notification settings - Fork 14
/
fee_hook.go
227 lines (196 loc) · 7.12 KB
/
fee_hook.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
package hook
import (
"bytes"
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/log"
"github.com/specularL2/specular/bindings-go/bindings"
)
// TODO: maybe move these constants to the rollup config or a separate file
const (
txDataZero = 4
txDataOne = 16
txSignatureOverhead = 68
)
var L1OracleAddress = common.HexToAddress("0x2A00000000000000000000000000000000000010")
type RollupConfig interface {
GetL1FeeRecipient() common.Address // recipient of the L1 Fee
GetL2ChainID() uint64 // chain ID of the specular rollup
}
// MakeSpecularEVMPreTransferHook creates specular's vm.EVMHook function
// which is injected into the EVM and runs before every transfer
// currently this is only used to calculate & charge the L1 Fee
func MakeSpecularEVMPreTransferHook(l2ChainId uint64, l1FeeRecipient common.Address) vm.EVMHook {
log.Info("Injected Specular EVM hook")
feeStorageSlots := getStorageSlots()
log.Info("L1Oracle config", "address", L1OracleAddress, "overheadSlot", feeStorageSlots.overheadSlot, "baseFeeSlot", feeStorageSlots.baseFeeSlot, "scalarSlot", feeStorageSlots.scalarSlot)
return func(msg vm.MessageInterface, db vm.StateDB) error {
tx := transactionFromMessage(msg, l2ChainId)
fee, err := calculateL1Fee(tx, db)
if err != nil {
return err
}
return chargeL1Fee(fee, msg, db, l1FeeRecipient)
}
}
// MakeSpecularL1FeeReader creates specular's vm.EVMReader function
// which is injected into the EVM and can be used to return the L1Fee of a transaction.
// This is a read only method and does not change the state.
func MakeSpecularL1FeeReader(l2ChainId uint64) vm.EVMReader {
log.Info("Injected Specular EVM reader")
log.Info("L1Oracle config", "address", L1OracleAddress)
return func(tx *types.Transaction, db vm.StateDB) (*big.Int, error) {
return calculateL1Fee(tx, db)
}
}
// creates a Transaction from a transaction
// the Tx Type is reconstructed and the signature is left empty
func transactionFromMessage(msg vm.MessageInterface, l2ChainId uint64) *types.Transaction {
var txData types.TxData
if msg.GetGasTipCap() != nil {
txData = &types.DynamicFeeTx{
ChainID: new(big.Int).SetUint64(l2ChainId),
Nonce: msg.GetNonce(),
GasTipCap: msg.GetGasTipCap(),
GasFeeCap: msg.GetGasFeeCap(),
Gas: msg.GetGasLimit(),
To: msg.GetTo(),
Value: msg.GetValue(),
Data: msg.GetData(),
AccessList: msg.GetAccessList(),
}
} else if msg.GetAccessList() != nil {
txData = &types.AccessListTx{
ChainID: new(big.Int).SetUint64(l2ChainId),
Nonce: msg.GetNonce(),
GasPrice: msg.GetGasPrice(),
Gas: msg.GetGasLimit(),
To: msg.GetTo(),
Value: msg.GetValue(),
Data: msg.GetData(),
AccessList: msg.GetAccessList(),
}
} else {
txData = &types.LegacyTx{
Nonce: msg.GetNonce(),
GasPrice: msg.GetGasPrice(),
Gas: msg.GetGasLimit(),
To: msg.GetTo(),
Value: msg.GetValue(),
Data: msg.GetData(),
}
}
return types.NewTx(txData)
}
// calculates the L1 fee for a transaction using the following formula:
// L1Fee = L1FeeMultiplier * L1BaseFee * (TxDataGas + L1OverheadGas)
// L1BaseFee is dynamically set by the L1Oracle
// L1FeeMultiplier & L1OverheadGas are set in the rollup configuration
func calculateL1Fee(tx *types.Transaction, db vm.StateDB) (*big.Int, error) {
// calculate L1 gas from RLP encoding
buf := new(bytes.Buffer)
if err := tx.EncodeRLP(buf); err != nil {
return common.Big0, err
}
bytes := buf.Bytes()
// remove the last 3 bytes containing the signature
// this mirrors the optimism implementation [1]
// but contradicts the optimism spec [2]
// [1] https://github.com/ethereum-optimism/optimism/blob/5d9a38dcd9dc79dce41a6d08f9b28ff850f77811/l2geth/rollup/fees/rollup_fee.go#L204
// [2] https://github.com/ethereum-optimism/optimism/blob/develop/specs/exec-engine.md#l1-cost-fees-l1-fee-vault
rlp := bytes[:len(bytes)-3]
var (
zeroes, ones = zeroesAndOnes(rlp)
rollupDataGas = zeroes*txDataZero + (ones+txSignatureOverhead)*txDataOne
feeStorageSlots = getStorageSlots()
overhead = readStorageSlot(db, L1OracleAddress, feeStorageSlots.overheadSlot)
basefee = readStorageSlot(db, L1OracleAddress, feeStorageSlots.baseFeeSlot)
scalar = readStorageSlot(db, L1OracleAddress, feeStorageSlots.scalarSlot)
)
log.Trace(
"calculated l1 fee",
"rollupDataGas", rollupDataGas,
"overhead", overhead,
"basefee", basefee,
"scalar", scalar,
)
l1GasUsed := new(big.Int).SetUint64(rollupDataGas)
l1GasUsed = l1GasUsed.Add(l1GasUsed, overhead)
l1Cost := l1GasUsed.Mul(l1GasUsed, basefee)
l1Cost = l1Cost.Mul(l1Cost, scalar)
return l1Cost.Div(l1Cost, big.NewInt(1_000_000)), nil
}
// multiply a big.Int with a float
// only the first 3 decimal places of the scalar are used to guarantee precision
func ScaleBigInt(num *big.Int, scalar float64) *big.Int {
var (
f = new(big.Float).SetInt(num)
s, _ = new(big.Float).SetString(fmt.Sprintf("%.3f", scalar))
scaledNum = new(big.Float).Mul(f, s)
roundedNum, _ = scaledNum.Int(nil)
)
if !scaledNum.IsInt() {
roundedNum = roundedNum.Add(roundedNum, common.Big1)
}
return roundedNum
}
// subtract the L1 Fee from the sender of the Tx
// add the Fee to the balance of the coinbase address
func chargeL1Fee(l1Fee *big.Int, msg vm.MessageInterface, db vm.StateDB, l1FeeRecipient common.Address) error {
senderBalance := db.GetBalance(msg.GetFrom())
if senderBalance.Cmp(l1Fee) < 0 {
return errors.New("insufficient balance to cover L1 fee")
}
db.AddBalance(l1FeeRecipient, l1Fee)
db.SubBalance(msg.GetFrom(), l1Fee)
log.Trace("charged L1 Fee", "fee", l1Fee.Uint64())
return nil
}
// read the value from a given address / storage slot
func readStorageSlot(db vm.StateDB, address common.Address, slot common.Hash) *big.Int {
return db.GetState(address, slot).Big()
}
// zeroesAndOnes counts the number of 0 bytes and non 0 bytes in a byte slice
func zeroesAndOnes(data []byte) (uint64, uint64) {
var zeroes, ones uint64
for _, b := range data {
if b == 0 {
zeroes++
} else {
ones++
}
}
return zeroes, ones
}
type feeStorageSlots struct {
baseFeeSlot common.Hash
overheadSlot common.Hash
scalarSlot common.Hash
}
func getStorageSlots() feeStorageSlots {
layout, err := bindings.GetStorageLayout("L1Oracle")
if err != nil {
panic("could not get storage layout for L1Oracle")
}
baseFeeEntry, err := layout.GetStorageLayoutEntry("baseFee")
if err != nil {
panic("could not get basefee storage slot")
}
overheadEntry, err := layout.GetStorageLayoutEntry("l1FeeOverhead")
if err != nil {
panic("could not get overhead storage slot")
}
scalarEntry, err := layout.GetStorageLayoutEntry("l1FeeScalar")
if err != nil {
panic("could not get scalar storage slot")
}
return feeStorageSlots{
baseFeeSlot: common.BigToHash(new(big.Int).SetUint64(uint64(baseFeeEntry.Slot))),
overheadSlot: common.BigToHash(new(big.Int).SetUint64(uint64(overheadEntry.Slot))),
scalarSlot: common.BigToHash(new(big.Int).SetUint64(uint64(scalarEntry.Slot))),
}
}