diff --git a/config/operation.go b/config/operation.go index eeb3d8a..7f2629c 100644 --- a/config/operation.go +++ b/config/operation.go @@ -38,15 +38,38 @@ const ( OpERC721Mint = "erc721_mint" // OpDisperseEther is Disperse disperseEtherFixed(address[]). OpDisperseEther = "disperse_ether" + // OpSwapAToB is AMM swapAToB(uint256), the A-for-B leg of a pair. + OpSwapAToB = "swap_a_to_b" + // OpSwapBToA is AMM swapBToA(uint256), the B-for-A leg. + OpSwapBToA = "swap_b_to_a" ) // StorageRWOperations is the operation set the storagerw scenario draws from. var StorageRWOperations = NewOperationSet(OpRmw, OpRead, OpWrite) +// AMMOperations is the operation set the amm scenario draws from: both legs of +// one pair, so a profile can weight the direction. A profile that sets no mix +// draws the first name for every transaction, which is one direction — the +// contract holds its reserves between a floor and a ceiling, so that run prices +// the same at its end as at its start. +var AMMOperations = NewOperationSet(OpSwapAToB, OpSwapBToA) + // scenarioOperations maps a scenario's wire name, lowercased, to the operations // it supports. A scenario absent from the table supports none. var scenarioOperations = map[string]*OperationSet{ "storagerw": StorageRWOperations, + "amm": AMMOperations, +} + +// OperationNamesFor returns every operation a scenario can draw, or nil if it +// draws none. It is exported so a test can check the whole vocabulary a +// scenario reaches the metrics with, rather than only its default. +func OperationNamesFor(scenario string) []string { + set := operationsFor(scenario) + if set == nil { + return nil + } + return set.Names() } // operationsFor returns the operations a scenario supports, or nil if it diff --git a/generator/bindings/AMM.go b/generator/bindings/AMM.go new file mode 100644 index 0000000..2d01591 --- /dev/null +++ b/generator/bindings/AMM.go @@ -0,0 +1,586 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "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/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// AMMMetaData contains all meta data concerning the AMM contract. +var AMMMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bool\",\"name\":\"aToB\",\"type\":\"bool\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amountIn\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amountOut\",\"type\":\"uint256\"}],\"name\":\"Swap\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"RESERVE_CEIL\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"RESERVE_FLOOR\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"balanceOfA\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"balanceOfB\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"reserveA\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"reserveB\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"amountIn\",\"type\":\"uint256\"}],\"name\":\"swapAToB\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"amountIn\",\"type\":\"uint256\"}],\"name\":\"swapBToA\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + Bin: "0x608060405234801561001057600080fd5b5069d3c21bcecceda10000006000819055600155610405806100336000396000f3fe608060405234801561001057600080fd5b50600436106100885760003560e01c806338720f721161005b57806338720f72146100ea57806385949788146100fd578063dc5fa6c514610110578063ddc4838d1461011957600080fd5b80630563e8671461008d57806319e36f3b146100b057806325d33d2f146100b95780632a6622e6146100c1575b600080fd5b61009e69d3c21bcecceda100000081565b60405190815260200160405180910390f35b61009e60015481565b61009e610142565b61009e6100cf36600461033a565b6001600160a01b031660009081526003602052604090205490565b61009e6100f836600461036a565b61015a565b61009e61010b36600461036a565b61016d565b61009e60005481565b61009e61012736600461033a565b6001600160a01b031660009081526002602052604090205490565b61015769d3c21bcecceda10000006002610399565b81565b6000610167600183610176565b92915050565b60006101676000835b600080836101865760015461018a565b6000545b905060008461019b5760005461019f565b6001545b905069d3c21bcecceda10000008210806101cc57506101c969d3c21bcecceda10000006002610399565b82115b156101df5769d3c21bcecceda100000091505b69d3c21bcecceda100000081108061020a575061020769d3c21bcecceda10000006002610399565b81115b1561021c575069d3c21bcecceda10000005b600061022a856103e5610399565b905060006102388383610399565b9050600082610249866103e8610399565b61025391906103b0565b9050600061026182846103c3565b9050600089610271576003610274565b60025b905060008a610284576002610287565b60035b3360009081526020848152604080832080548f9003905590839052902080548501905590508a156102d1576102bc8a896103b0565b6000556102c983886103e5565b6001556102ec565b6102db8a896103b0565b6001556102e883886103e5565b6000555b604080518b8152602081018590528c15159133917fbfd50a04f1e6e4aee344f5d0e7f15d74d0dbb58cd1f711daa6463094ca9508cd910160405180910390a350909998505050505050505050565b60006020828403121561034c57600080fd5b81356001600160a01b038116811461036357600080fd5b9392505050565b60006020828403121561037c57600080fd5b5035919050565b634e487b7160e01b600052601160045260246000fd5b808202811582820484141761016757610167610383565b8082018082111561016757610167610383565b6000826103e057634e487b7160e01b600052601260045260246000fd5b500490565b818103818111156101675761016761038356fea164736f6c6343000813000a", +} + +// AMMABI is the input ABI used to generate the binding from. +// Deprecated: Use AMMMetaData.ABI instead. +var AMMABI = AMMMetaData.ABI + +// AMMBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use AMMMetaData.Bin instead. +var AMMBin = AMMMetaData.Bin + +// DeployAMM deploys a new Ethereum contract, binding an instance of AMM to it. +func DeployAMM(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *AMM, error) { + parsed, err := AMMMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(AMMBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &AMM{AMMCaller: AMMCaller{contract: contract}, AMMTransactor: AMMTransactor{contract: contract}, AMMFilterer: AMMFilterer{contract: contract}}, nil +} + +// AMM is an auto generated Go binding around an Ethereum contract. +type AMM struct { + AMMCaller // Read-only binding to the contract + AMMTransactor // Write-only binding to the contract + AMMFilterer // Log filterer for contract events +} + +// AMMCaller is an auto generated read-only Go binding around an Ethereum contract. +type AMMCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// AMMTransactor is an auto generated write-only Go binding around an Ethereum contract. +type AMMTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// AMMFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type AMMFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// AMMSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type AMMSession struct { + Contract *AMM // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// AMMCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type AMMCallerSession struct { + Contract *AMMCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// AMMTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type AMMTransactorSession struct { + Contract *AMMTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// AMMRaw is an auto generated low-level Go binding around an Ethereum contract. +type AMMRaw struct { + Contract *AMM // Generic contract binding to access the raw methods on +} + +// AMMCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type AMMCallerRaw struct { + Contract *AMMCaller // Generic read-only contract binding to access the raw methods on +} + +// AMMTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type AMMTransactorRaw struct { + Contract *AMMTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewAMM creates a new instance of AMM, bound to a specific deployed contract. +func NewAMM(address common.Address, backend bind.ContractBackend) (*AMM, error) { + contract, err := bindAMM(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &AMM{AMMCaller: AMMCaller{contract: contract}, AMMTransactor: AMMTransactor{contract: contract}, AMMFilterer: AMMFilterer{contract: contract}}, nil +} + +// NewAMMCaller creates a new read-only instance of AMM, bound to a specific deployed contract. +func NewAMMCaller(address common.Address, caller bind.ContractCaller) (*AMMCaller, error) { + contract, err := bindAMM(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &AMMCaller{contract: contract}, nil +} + +// NewAMMTransactor creates a new write-only instance of AMM, bound to a specific deployed contract. +func NewAMMTransactor(address common.Address, transactor bind.ContractTransactor) (*AMMTransactor, error) { + contract, err := bindAMM(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &AMMTransactor{contract: contract}, nil +} + +// NewAMMFilterer creates a new log filterer instance of AMM, bound to a specific deployed contract. +func NewAMMFilterer(address common.Address, filterer bind.ContractFilterer) (*AMMFilterer, error) { + contract, err := bindAMM(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &AMMFilterer{contract: contract}, nil +} + +// bindAMM binds a generic wrapper to an already deployed contract. +func bindAMM(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := AMMMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), 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 (_AMM *AMMRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _AMM.Contract.AMMCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_AMM *AMMRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _AMM.Contract.AMMTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_AMM *AMMRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _AMM.Contract.AMMTransactor.contract.Transact(opts, method, params...) +} + +// 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 (_AMM *AMMCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _AMM.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_AMM *AMMTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _AMM.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_AMM *AMMTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _AMM.Contract.contract.Transact(opts, method, params...) +} + +// RESERVECEIL is a free data retrieval call binding the contract method 0x25d33d2f. +// +// Solidity: function RESERVE_CEIL() view returns(uint256) +func (_AMM *AMMCaller) RESERVECEIL(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _AMM.contract.Call(opts, &out, "RESERVE_CEIL") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// RESERVECEIL is a free data retrieval call binding the contract method 0x25d33d2f. +// +// Solidity: function RESERVE_CEIL() view returns(uint256) +func (_AMM *AMMSession) RESERVECEIL() (*big.Int, error) { + return _AMM.Contract.RESERVECEIL(&_AMM.CallOpts) +} + +// RESERVECEIL is a free data retrieval call binding the contract method 0x25d33d2f. +// +// Solidity: function RESERVE_CEIL() view returns(uint256) +func (_AMM *AMMCallerSession) RESERVECEIL() (*big.Int, error) { + return _AMM.Contract.RESERVECEIL(&_AMM.CallOpts) +} + +// RESERVEFLOOR is a free data retrieval call binding the contract method 0x0563e867. +// +// Solidity: function RESERVE_FLOOR() view returns(uint256) +func (_AMM *AMMCaller) RESERVEFLOOR(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _AMM.contract.Call(opts, &out, "RESERVE_FLOOR") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// RESERVEFLOOR is a free data retrieval call binding the contract method 0x0563e867. +// +// Solidity: function RESERVE_FLOOR() view returns(uint256) +func (_AMM *AMMSession) RESERVEFLOOR() (*big.Int, error) { + return _AMM.Contract.RESERVEFLOOR(&_AMM.CallOpts) +} + +// RESERVEFLOOR is a free data retrieval call binding the contract method 0x0563e867. +// +// Solidity: function RESERVE_FLOOR() view returns(uint256) +func (_AMM *AMMCallerSession) RESERVEFLOOR() (*big.Int, error) { + return _AMM.Contract.RESERVEFLOOR(&_AMM.CallOpts) +} + +// BalanceOfA is a free data retrieval call binding the contract method 0xddc4838d. +// +// Solidity: function balanceOfA(address account) view returns(uint256) +func (_AMM *AMMCaller) BalanceOfA(opts *bind.CallOpts, account common.Address) (*big.Int, error) { + var out []interface{} + err := _AMM.contract.Call(opts, &out, "balanceOfA", account) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// BalanceOfA is a free data retrieval call binding the contract method 0xddc4838d. +// +// Solidity: function balanceOfA(address account) view returns(uint256) +func (_AMM *AMMSession) BalanceOfA(account common.Address) (*big.Int, error) { + return _AMM.Contract.BalanceOfA(&_AMM.CallOpts, account) +} + +// BalanceOfA is a free data retrieval call binding the contract method 0xddc4838d. +// +// Solidity: function balanceOfA(address account) view returns(uint256) +func (_AMM *AMMCallerSession) BalanceOfA(account common.Address) (*big.Int, error) { + return _AMM.Contract.BalanceOfA(&_AMM.CallOpts, account) +} + +// BalanceOfB is a free data retrieval call binding the contract method 0x2a6622e6. +// +// Solidity: function balanceOfB(address account) view returns(uint256) +func (_AMM *AMMCaller) BalanceOfB(opts *bind.CallOpts, account common.Address) (*big.Int, error) { + var out []interface{} + err := _AMM.contract.Call(opts, &out, "balanceOfB", account) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// BalanceOfB is a free data retrieval call binding the contract method 0x2a6622e6. +// +// Solidity: function balanceOfB(address account) view returns(uint256) +func (_AMM *AMMSession) BalanceOfB(account common.Address) (*big.Int, error) { + return _AMM.Contract.BalanceOfB(&_AMM.CallOpts, account) +} + +// BalanceOfB is a free data retrieval call binding the contract method 0x2a6622e6. +// +// Solidity: function balanceOfB(address account) view returns(uint256) +func (_AMM *AMMCallerSession) BalanceOfB(account common.Address) (*big.Int, error) { + return _AMM.Contract.BalanceOfB(&_AMM.CallOpts, account) +} + +// ReserveA is a free data retrieval call binding the contract method 0xdc5fa6c5. +// +// Solidity: function reserveA() view returns(uint256) +func (_AMM *AMMCaller) ReserveA(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _AMM.contract.Call(opts, &out, "reserveA") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// ReserveA is a free data retrieval call binding the contract method 0xdc5fa6c5. +// +// Solidity: function reserveA() view returns(uint256) +func (_AMM *AMMSession) ReserveA() (*big.Int, error) { + return _AMM.Contract.ReserveA(&_AMM.CallOpts) +} + +// ReserveA is a free data retrieval call binding the contract method 0xdc5fa6c5. +// +// Solidity: function reserveA() view returns(uint256) +func (_AMM *AMMCallerSession) ReserveA() (*big.Int, error) { + return _AMM.Contract.ReserveA(&_AMM.CallOpts) +} + +// ReserveB is a free data retrieval call binding the contract method 0x19e36f3b. +// +// Solidity: function reserveB() view returns(uint256) +func (_AMM *AMMCaller) ReserveB(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _AMM.contract.Call(opts, &out, "reserveB") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// ReserveB is a free data retrieval call binding the contract method 0x19e36f3b. +// +// Solidity: function reserveB() view returns(uint256) +func (_AMM *AMMSession) ReserveB() (*big.Int, error) { + return _AMM.Contract.ReserveB(&_AMM.CallOpts) +} + +// ReserveB is a free data retrieval call binding the contract method 0x19e36f3b. +// +// Solidity: function reserveB() view returns(uint256) +func (_AMM *AMMCallerSession) ReserveB() (*big.Int, error) { + return _AMM.Contract.ReserveB(&_AMM.CallOpts) +} + +// SwapAToB is a paid mutator transaction binding the contract method 0x38720f72. +// +// Solidity: function swapAToB(uint256 amountIn) returns(uint256) +func (_AMM *AMMTransactor) SwapAToB(opts *bind.TransactOpts, amountIn *big.Int) (*types.Transaction, error) { + return _AMM.contract.Transact(opts, "swapAToB", amountIn) +} + +// SwapAToB is a paid mutator transaction binding the contract method 0x38720f72. +// +// Solidity: function swapAToB(uint256 amountIn) returns(uint256) +func (_AMM *AMMSession) SwapAToB(amountIn *big.Int) (*types.Transaction, error) { + return _AMM.Contract.SwapAToB(&_AMM.TransactOpts, amountIn) +} + +// SwapAToB is a paid mutator transaction binding the contract method 0x38720f72. +// +// Solidity: function swapAToB(uint256 amountIn) returns(uint256) +func (_AMM *AMMTransactorSession) SwapAToB(amountIn *big.Int) (*types.Transaction, error) { + return _AMM.Contract.SwapAToB(&_AMM.TransactOpts, amountIn) +} + +// SwapBToA is a paid mutator transaction binding the contract method 0x85949788. +// +// Solidity: function swapBToA(uint256 amountIn) returns(uint256) +func (_AMM *AMMTransactor) SwapBToA(opts *bind.TransactOpts, amountIn *big.Int) (*types.Transaction, error) { + return _AMM.contract.Transact(opts, "swapBToA", amountIn) +} + +// SwapBToA is a paid mutator transaction binding the contract method 0x85949788. +// +// Solidity: function swapBToA(uint256 amountIn) returns(uint256) +func (_AMM *AMMSession) SwapBToA(amountIn *big.Int) (*types.Transaction, error) { + return _AMM.Contract.SwapBToA(&_AMM.TransactOpts, amountIn) +} + +// SwapBToA is a paid mutator transaction binding the contract method 0x85949788. +// +// Solidity: function swapBToA(uint256 amountIn) returns(uint256) +func (_AMM *AMMTransactorSession) SwapBToA(amountIn *big.Int) (*types.Transaction, error) { + return _AMM.Contract.SwapBToA(&_AMM.TransactOpts, amountIn) +} + +// AMMSwapIterator is returned from FilterSwap and is used to iterate over the raw logs and unpacked data for Swap events raised by the AMM contract. +type AMMSwapIterator struct { + Event *AMMSwap // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *AMMSwapIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(AMMSwap) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(AMMSwap) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *AMMSwapIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *AMMSwapIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// AMMSwap represents a Swap event raised by the AMM contract. +type AMMSwap struct { + Sender common.Address + AToB bool + AmountIn *big.Int + AmountOut *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSwap is a free log retrieval operation binding the contract event 0xbfd50a04f1e6e4aee344f5d0e7f15d74d0dbb58cd1f711daa6463094ca9508cd. +// +// Solidity: event Swap(address indexed sender, bool indexed aToB, uint256 amountIn, uint256 amountOut) +func (_AMM *AMMFilterer) FilterSwap(opts *bind.FilterOpts, sender []common.Address, aToB []bool) (*AMMSwapIterator, error) { + + var senderRule []interface{} + for _, senderItem := range sender { + senderRule = append(senderRule, senderItem) + } + var aToBRule []interface{} + for _, aToBItem := range aToB { + aToBRule = append(aToBRule, aToBItem) + } + + logs, sub, err := _AMM.contract.FilterLogs(opts, "Swap", senderRule, aToBRule) + if err != nil { + return nil, err + } + return &AMMSwapIterator{contract: _AMM.contract, event: "Swap", logs: logs, sub: sub}, nil +} + +// WatchSwap is a free log subscription operation binding the contract event 0xbfd50a04f1e6e4aee344f5d0e7f15d74d0dbb58cd1f711daa6463094ca9508cd. +// +// Solidity: event Swap(address indexed sender, bool indexed aToB, uint256 amountIn, uint256 amountOut) +func (_AMM *AMMFilterer) WatchSwap(opts *bind.WatchOpts, sink chan<- *AMMSwap, sender []common.Address, aToB []bool) (event.Subscription, error) { + + var senderRule []interface{} + for _, senderItem := range sender { + senderRule = append(senderRule, senderItem) + } + var aToBRule []interface{} + for _, aToBItem := range aToB { + aToBRule = append(aToBRule, aToBItem) + } + + logs, sub, err := _AMM.contract.WatchLogs(opts, "Swap", senderRule, aToBRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(AMMSwap) + if err := _AMM.contract.UnpackLog(event, "Swap", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSwap is a log parse operation binding the contract event 0xbfd50a04f1e6e4aee344f5d0e7f15d74d0dbb58cd1f711daa6463094ca9508cd. +// +// Solidity: event Swap(address indexed sender, bool indexed aToB, uint256 amountIn, uint256 amountOut) +func (_AMM *AMMFilterer) ParseSwap(log types.Log) (*AMMSwap, error) { + event := new(AMMSwap) + if err := _AMM.contract.UnpackLog(event, "Swap", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/generator/contracts/AMM.sol b/generator/contracts/AMM.sol new file mode 100644 index 0000000..c0469ac --- /dev/null +++ b/generator/contracts/AMM.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title AMM +/// @notice A constant-product swap with the storage and gas shape of a +/// UniswapV2 pair, sized for load generation rather than for correctness. +/// +/// A swap here touches what a real pair touches: both reserves, the caller's +/// balance in each token, and an event. That is the cost this contract exists +/// to reproduce. What it does not reproduce is the economics — there is no +/// router, no fee split to an LP, no price oracle and no minimum-output check. +/// +/// Nothing reverts on bookkeeping, which is the same choice StorageRWv1.sol +/// makes. The balances wrap instead of checking, because nothing reads them +/// back and a load generator that fails on its own accounting stops measuring +/// the chain. The arithmetic on lines outside the unchecked block stays +/// checked: it reverts only on an input the scenario never sends, and a silent +/// wrap there would price a swap from garbage. +/// +/// Every swap writes the same four slots, so after a caller's first swap the +/// cost of its thousandth is the cost of its second. +contract AMM { + uint256 public reserveA; + uint256 public reserveB; + + /// @dev The reserve a swap restores an exhausted side to. + uint256 public constant RESERVE_FLOOR = 1_000_000 * 10**18; + + /// @dev The reserve above which a swap restores a side to the floor. A run + /// drives one pair for its whole length and only ever adds to the input + /// side, so without a ceiling the price walks away from where it started: + /// measured, the output halves every 100,000 swaps. With it the reserves + /// saw-tooth and the price holds for any run length. + /// + /// A swap clamps a reserve as it reads it and writes the result after, so a + /// side settles within one swap's size of each bound rather than exactly on + /// it. Measured over 40 swaps of a tenth of the floor, the input side stayed + /// in [1.1x, 2.1x] of the floor. + /// + /// The ceiling is also what keeps one oversized call from ending the pair. + /// A swap of 1e49 leaves the input reserve at 1e49, which without a ceiling + /// prices every later swap at nothing; measured, the next ordinary swap + /// instead resets that side to the floor and pays out in full. + uint256 public constant RESERVE_CEIL = 2 * RESERVE_FLOOR; + + mapping(address => uint256) private _balanceA; + mapping(address => uint256) private _balanceB; + + event Swap(address indexed sender, bool indexed aToB, uint256 amountIn, uint256 amountOut); + + constructor() { + reserveA = RESERVE_FLOOR; + reserveB = RESERVE_FLOOR; + } + + function balanceOfA(address account) public view returns (uint256) { + return _balanceA[account]; + } + + function balanceOfB(address account) public view returns (uint256) { + return _balanceB[account]; + } + + /// @notice Swap amountIn of token A for token B. + function swapAToB(uint256 amountIn) public returns (uint256) { + return _swap(true, amountIn); + } + + /// @notice Swap amountIn of token B for token A. + function swapBToA(uint256 amountIn) public returns (uint256) { + return _swap(false, amountIn); + } + + function _swap(bool aToB, uint256 amountIn) private returns (uint256) { + uint256 reserveIn = aToB ? reserveA : reserveB; + uint256 reserveOut = aToB ? reserveB : reserveA; + + // Hold both sides between the floor and the ceiling. See RESERVE_CEIL. + if (reserveIn < RESERVE_FLOOR || reserveIn > RESERVE_CEIL) { + reserveIn = RESERVE_FLOOR; + } + if (reserveOut < RESERVE_FLOOR || reserveOut > RESERVE_CEIL) { + reserveOut = RESERVE_FLOOR; + } + + // x*y=k with the 0.3% fee UniswapV2 charges, so the arithmetic is the + // same width and the same number of operations. + uint256 amountInWithFee = amountIn * 997; + uint256 numerator = amountInWithFee * reserveOut; + uint256 denominator = reserveIn * 1000 + amountInWithFee; + uint256 amountOut = numerator / denominator; + + mapping(address => uint256) storage balIn = aToB ? _balanceA : _balanceB; + mapping(address => uint256) storage balOut = aToB ? _balanceB : _balanceA; + + // Wrap rather than revert, and rather than credit a short caller. A + // credit that restores the slot to what it held leaves it at zero for a + // caller that started there, and a zero-to-non-zero write costs four + // times one that changes a slot already holding a value. Under the + // default mix, which draws one direction, that cold write would land on + // every swap the run makes rather than on the first. + unchecked { + balIn[msg.sender] = balIn[msg.sender] - amountIn; + balOut[msg.sender] = balOut[msg.sender] + amountOut; + } + + if (aToB) { + reserveA = reserveIn + amountIn; + reserveB = reserveOut - amountOut; + } else { + reserveB = reserveIn + amountIn; + reserveA = reserveOut - amountOut; + } + + emit Swap(msg.sender, aToB, amountIn, amountOut); + return amountOut; + } +} diff --git a/generator/scenarios/AMM.go b/generator/scenarios/AMM.go new file mode 100644 index 0000000..b45c673 --- /dev/null +++ b/generator/scenarios/AMM.go @@ -0,0 +1,136 @@ +package scenarios + +import ( + "fmt" + "math/big" + mrand "math/rand/v2" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/sei-protocol/sei-load/config" + "github.com/sei-protocol/sei-load/generator/bindings" + "github.com/sei-protocol/sei-load/types" +) + +const AMM = "amm" + +// ammSwapGas bounds one swap. +// +// The number is a required gas limit read from eth_estimateGas, not a receipt's +// GasUsed. GasUsed is what the chain charges after the refund lands at the end +// of execution; a transaction still has to be provisioned for the peak before +// it. Sizing this constant from a receipt once put it 20% under the limit the +// same swap needed, which fails every transaction and burns the whole limit. +// +// Two shapes, measured over six accounts against the deployed binding on a +// chain running the default storage gas costs: +// +// - 79,988, an account's first swap. It writes the account's balance in both +// tokens from zero, and a zero to non-zero storage write costs four times +// one that changes a slot already holding a value. +// - 45,177, every later swap by that account. The balances wrap rather than +// return to zero, so the slots stay non-zero for the rest of the run. +// +// One limit has to cover the higher shape, so a run in steady state declares +// about 44% more gas than it spends. A chain that admits transactions against +// their declared limit reserves that difference for gas no swap uses, which +// costs the throughput a profile can reach. Priming both slots during prewarm +// would let this drop near the lower shape; that needs a transaction class the +// prewarm path does not have yet, and PLT-1093 carries it. +// +// The calibration assumes the chain charges the default 20,000 for a zero to +// non-zero storage write. Sei sets that as a chain parameter, and pacific-1 and +// atlantic-2 charge about 74,700, which puts the first shape near 185,000 +// there. Every hard-coded limit in this package has the same exposure, so +// PLT-1092 covers the package rather than this constant. +// +// Estimating per transaction would put an eth_estimateGas on the send path, +// which is the load this tool exists to avoid adding. +const ammSwapGas = 85_000 + +// ammSwapAmount is the input every swap sends. +// +// It is fixed rather than drawn, because drawing it would buy no gas coverage. +// Measured across the steady shape, a swap needed 45,141 gas at 1e6 and 45,201 +// at 1e23: sixty gas over seventeen orders of magnitude, and all of it the +// calldata bytes of the larger number rather than execution. +// +// The one amount that does change the shape is zero, which makes the output zero +// and turns the payout write into a write of the value already there. The +// scenario never sends it. +// +// A draw would also cost a draw from the run's single PRNG, which every other +// scenario's replay at the same seed depends on. +var ammSwapAmount = new(big.Int).Mul(big.NewInt(10), big.NewInt(1e18)) + +// AMMScenario drives a constant-product pair, which is the shape most of a DeFi +// workload's transactions have. +type AMMScenario struct { + *ContractScenarioBase[bindings.AMM] + contract *bindings.AMM + operations *config.OperationPicker +} + +// NewAMMScenario creates a new AMM scenario. +func NewAMMScenario(cfg config.Scenario) TxGenerator { + scenario := &AMMScenario{ + operations: config.AMMOperations.Picker(cfg.Operations), + } + scenario.ContractScenarioBase = NewContractScenarioBase[bindings.AMM](scenario, cfg) + return scenario +} + +// Name returns the name of the scenario. +func (s *AMMScenario) Name() string { + return AMM +} + +// Operation returns the scenario's default operation. +func (s *AMMScenario) Operation() string { + return config.OpSwapAToB +} + +// DeployContract implements ContractDeployer. AMM seeds both reserves in its +// constructor and takes no arguments. +func (s *AMMScenario) DeployContract(opts *bind.TransactOpts, client *ethclient.Client) (common.Address, *ethtypes.Transaction, error) { + address, tx, _, err := bindings.DeployAMM(opts, client) + return address, tx, err +} + +// GetBindFunc implements ContractDeployer. +func (s *AMMScenario) GetBindFunc() ContractBindFunc[bindings.AMM] { + return bindings.NewAMM +} + +// SetContract implements ContractDeployer. +func (s *AMMScenario) SetContract(contract *bindings.AMM) { + s.contract = contract +} + +// CreateContractTransaction implements ContractDeployer - builds one swap in the +// direction the operation mix drew. +func (s *AMMScenario) CreateContractTransaction(rng *mrand.Rand, auth *bind.TransactOpts, scenario *types.TxScenario) (*ethtypes.Transaction, error) { + auth.GasLimit = ammSwapGas + + // The draw is part of the replay contract. One draw today, so there is no + // order to get wrong; a second axis must land after this one, because every + // scenario shares the run's PRNG and a reordered draw shifts every later + // one at the same seed. + op := s.operations.Select(rng) + scenario.Operation = op + + switch op { + case config.OpSwapAToB: + return s.contract.SwapAToB(auth, ammSwapAmount) + case config.OpSwapBToA: + return s.contract.SwapBToA(auth, ammSwapAmount) + default: + // A name added to the set with no call here would otherwise be stamped + // into the metric and sent as the other leg, so the dimension would say + // one thing while the chain saw another. + return nil, fmt.Errorf("amm: no call for operation %q", op) + } +} diff --git a/generator/scenarios/AMM_test.go b/generator/scenarios/AMM_test.go new file mode 100644 index 0000000..bca6023 --- /dev/null +++ b/generator/scenarios/AMM_test.go @@ -0,0 +1,181 @@ +package scenarios_test + +import ( + mrand "math/rand/v2" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-load/config" + "github.com/sei-protocol/sei-load/generator/bindings" + "github.com/sei-protocol/sei-load/generator/scenarios" + "github.com/sei-protocol/sei-load/types" +) + +// ammColdSwapGas is the largest gas limit one swap required when measured +// against the deployed binding over six accounts, on a chain running the +// default storage gas costs. It is a required limit read from eth_estimateGas, +// not a receipt's GasUsed, because a transaction is provisioned for the peak +// before the refund lands. The steady-state shape needed 45,177. +// +// It is written down so the limit is checked against a measurement rather than +// against itself. An assertion comparing the limit to its own constant passes +// at any value. +const ammColdSwapGas = 79_988 + +func newAttachedAMM(t *testing.T, sc config.Scenario) (scenarios.TxGenerator, *types.TxScenario) { + t.Helper() + sc.Name = scenarios.AMM + cfg := &config.LoadConfig{ + ChainID: 7777, + MockDeploy: true, + Endpoints: []string{"http://localhost:8545"}, + } + gen := scenarios.CreateScenario(sc) + require.NoError(t, gen.Ready(cfg)) + require.NoError(t, gen.Binder()(nil, types.GenerateAccounts(1, false)[0].Address)) + return gen, &types.TxScenario{ + Name: scenarios.AMM, + Nonce: 0, + Sender: types.GenerateAccounts(1, true)[0], + } +} + +// decodeAMM names the method AMM calldata calls, through the binding's own ABI +// so the test cannot drift from the contract. +func decodeAMM(t *testing.T, data []byte) string { + t.Helper() + parsed, err := bindings.AMMMetaData.GetAbi() + require.NoError(t, err) + method, err := parsed.MethodById(data[:4]) + require.NoError(t, err) + return method.Name +} + +// ammMethodFor maps an operation name to the contract method that name promises. +var ammMethodFor = map[string]string{ + config.OpSwapAToB: "swapAToB", + config.OpSwapBToA: "swapBToA", +} + +// TestAMMCoversItsDeclaredOperations closes the seam between the names config +// declares for this scenario and the methods the scenario calls: weighting one +// name alone must produce calldata for that name's method. A name added to the +// set with no call behind it fails here rather than mislabelling every +// transaction of a run. +func TestAMMCoversItsDeclaredOperations(t *testing.T) { + names := config.OperationNamesFor(scenarios.AMM) + require.NotEmpty(t, names) + for _, name := range names { + t.Run(name, func(t *testing.T) { + gen, txs := newAttachedAMM(t, config.Scenario{ + Operations: config.OperationMix{name: 1}, + }) + tx, err := gen.Generate(newTestRng(1), txs) + require.NoError(t, err) + want, known := ammMethodFor[name] + require.True(t, known, "no expected method recorded for %q", name) + require.Equal(t, want, decodeAMM(t, tx.Data())) + }) + } +} + +// TestAMMStampsTheDrawnOperation asserts the operation recorded on the +// TxScenario is the method the calldata actually calls, across a mix. +// +// The weights are deliberately lopsided and written with the later-declared +// name first. A balanced two-name mix in declared order passes for any picker +// that is not wholly degenerate, including one that walks the map in Go's +// randomised order, so it would assert nothing about the draw. +func TestAMMStampsTheDrawnOperation(t *testing.T) { + gen, txs := newAttachedAMM(t, config.Scenario{ + Operations: config.OperationMix{ + config.OpSwapBToA: 1, + config.OpSwapAToB: 3, + }, + }) + rng := newTestRng(7) + seen := map[string]int{} + for range 200 { + tx, err := gen.Generate(rng, txs) + require.NoError(t, err) + require.Equal(t, ammMethodFor[txs.Operation], decodeAMM(t, tx.Data()), + "the metric says %q and the chain sees a different method", txs.Operation) + seen[txs.Operation]++ + } + require.Len(t, seen, 2, "one leg never drew, so the mix is not being read") + require.Greater(t, seen[config.OpSwapAToB], seen[config.OpSwapBToA], + "the heavier weight did not draw more often, so the weights are ignored") +} + +// TestAMMLegsCallDifferentMethods fails when both operations produce the same +// calldata. Two names that reach the metrics as separate dimensions must reach +// the chain as separate calls, or the run reports a split it did not make. +func TestAMMLegsCallDifferentMethods(t *testing.T) { + sel := map[string]string{} + for _, name := range config.OperationNamesFor(scenarios.AMM) { + gen, txs := newAttachedAMM(t, config.Scenario{ + Operations: config.OperationMix{name: 1}, + }) + tx, err := gen.Generate(newTestRng(3), txs) + require.NoError(t, err) + sel[name] = string(tx.Data()[:4]) + } + require.Len(t, sel, 2) + require.NotEqual(t, sel[config.OpSwapAToB], sel[config.OpSwapBToA], + "both legs send the same selector, so the two dimensions are one workload") +} + +// TestAMMGasCoversAMeasuredSwap holds the declared limit between the cost a swap +// actually has and a ceiling above it. +// +// Both bounds matter and they fail differently. Below the real cost every +// transaction lands with a failed status and burns the whole limit, which a run +// without receipt tracking reports as success. Far above it, a chain that admits +// transactions against their declared limit reserves block space the swap never +// spends, and the profile's reachable throughput falls for no visible reason. +// +// The bounds are measured, not the constant restated: an assertion against the +// constant itself passes at any value. +func TestAMMGasCoversAMeasuredSwap(t *testing.T) { + gen, txs := newAttachedAMM(t, config.Scenario{}) + tx, err := gen.Generate(newTestRng(1), txs) + require.NoError(t, err) + + require.Greater(t, tx.Gas(), uint64(ammColdSwapGas), + "the limit is below the most a swap cost when measured, so an account's "+ + "first swap lands with a failed status and burns the whole limit") + require.Less(t, tx.Gas(), uint64(ammColdSwapGas*13/10), + "the limit is far above a measured swap, so it reserves block space nothing spends") +} + +// TestAMMDefaultPathDrawsNoRandomness asserts a profile with no operation mix +// consumes no randomness, so adding this scenario to a run cannot shift the +// draw sequence every other scenario's replay depends on. +func TestAMMDefaultPathDrawsNoRandomness(t *testing.T) { + gen, txs := newAttachedAMM(t, config.Scenario{}) + rng := newTestRng(11) + _, err := gen.Generate(rng, txs) + require.NoError(t, err) + require.Equal(t, newTestRng(11).Uint64(), rng.Uint64(), + "the default path drew from the run's PRNG, which shifts every later draw") +} + +// TestAMMMixDrawsExactlyOnce asserts a profile with a mix consumes one draw per +// transaction, for the same reason. +func TestAMMMixDrawsExactlyOnce(t *testing.T) { + gen, txs := newAttachedAMM(t, config.Scenario{ + Operations: config.OperationMix{config.OpSwapAToB: 1, config.OpSwapBToA: 1}, + }) + rng := newTestRng(11) + _, err := gen.Generate(rng, txs) + require.NoError(t, err) + + want := newTestRng(11) + consumeOne(want) + require.Equal(t, want.Uint64(), rng.Uint64(), + "the mix path drew a different number of times than one") +} + +// consumeOne advances a PRNG by the one draw an operation pick costs. +func consumeOne(rng *mrand.Rand) { rng.Uint64N(2) } diff --git a/generator/scenarios/ERC721.go b/generator/scenarios/ERC721.go index 764987a..7390879 100644 --- a/generator/scenarios/ERC721.go +++ b/generator/scenarios/ERC721.go @@ -47,6 +47,22 @@ func (s *ERC721Scenario) DeployContract(opts *bind.TransactOpts, client *ethclie return address, tx, err } +// erc721MintGas bounds one mint. +// +// Measured against the deployed binding as a required gas limit, which is what +// eth_estimateGas returns and what a transaction has to carry before its refund +// lands at the end of execution. A receipt's GasUsed is the post-refund charge +// and runs lower, so it is the wrong number to size from. +// +// 69,319 to a receiver holding none of the token, and 51,757 to one that already +// holds some. A run draws its receivers from the account pool, so most mints pay +// the higher shape and the limit covers it. +// +// This constant read 22460 until it was measured. At that value every mint +// landed in a block with a failed status and burned the whole limit, and a run +// with trackReceipts off reported each one as a success. +const erc721MintGas = 75_000 + // GetBindFunc implements ContractDeployer interface - returns the binding function func (s *ERC721Scenario) GetBindFunc() ContractBindFunc[bindings.ERC721] { return bindings.NewERC721 @@ -59,6 +75,6 @@ func (s *ERC721Scenario) SetContract(contract *bindings.ERC721) { // CreateContractTransaction implements ContractDeployer interface - creates ERC721 transaction func (s *ERC721Scenario) CreateContractTransaction(rng *mrand.Rand, auth *bind.TransactOpts, scenario *types.TxScenario) (*ethtypes.Transaction, error) { - auth.GasLimit = 22460 + auth.GasLimit = erc721MintGas return s.contract.Mint(auth, scenario.Receiver, big.NewInt(atomic.AddInt64(&s.id, 1))) } diff --git a/generator/scenarios/ERC721_test.go b/generator/scenarios/ERC721_test.go new file mode 100644 index 0000000..e008372 --- /dev/null +++ b/generator/scenarios/ERC721_test.go @@ -0,0 +1,55 @@ +package scenarios_test + +import ( + mrand "math/rand/v2" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-load/config" + "github.com/sei-protocol/sei-load/generator/scenarios" + "github.com/sei-protocol/sei-load/types" +) + +// erc721ColdMintGas is the gas limit one mint required when measured against +// the deployed binding, sending to a receiver that holds none of the token. A +// receiver that already holds some needed 51,757. A run draws its receivers +// from the account pool, so the higher shape is the common one. +// +// It is a required limit read from eth_estimateGas, not a receipt's GasUsed, +// because a transaction is provisioned for the peak before its refund lands. +// +// It is written down so the limit is checked against a measurement rather than +// against itself. An assertion comparing the limit to its own constant passes +// at any value. +const erc721ColdMintGas = 69_319 + +// TestERC721GasCoversAMeasuredMint guards the failure this constant shipped +// with: a limit under what a mint needs still reaches a block, with a failed +// status, having burned the whole limit. A run with trackReceipts off counts +// that as a success, so no other test in this package can see it. +func TestERC721GasCoversAMeasuredMint(t *testing.T) { + cfg := &config.LoadConfig{ + ChainID: 7777, + MockDeploy: true, + Endpoints: []string{"http://localhost:8545"}, + } + gen := scenarios.CreateScenario(config.Scenario{Name: scenarios.ERC721}) + require.NoError(t, gen.Ready(cfg)) + require.NoError(t, gen.Binder()(nil, types.GenerateAccounts(1, false)[0].Address)) + + tx, err := gen.Generate(mrand.New(mrand.NewPCG(1, 2)), &types.TxScenario{ + Name: scenarios.ERC721, + Nonce: 0, + Sender: types.GenerateAccounts(1, true)[0], + Receiver: types.GenerateAccounts(1, false)[0].Address, + }) + require.NoError(t, err) + + require.GreaterOrEqual(t, tx.Gas(), uint64(erc721ColdMintGas), + "a mint needs %d gas and the limit is %d, so every mint lands with a failed status and burns the limit while the run reports it as sent", + erc721ColdMintGas, tx.Gas()) + require.LessOrEqual(t, tx.Gas(), uint64(erc721ColdMintGas*13/10), + "the limit is %d against a measured %d, so every mint reserves block space nothing spends", + tx.Gas(), erc721ColdMintGas) +} diff --git a/generator/scenarios/factory.go b/generator/scenarios/factory.go index 409ad5c..a6d9346 100644 --- a/generator/scenarios/factory.go +++ b/generator/scenarios/factory.go @@ -18,10 +18,11 @@ var scenarioFactories = map[string]ScenarioFactory{ // Auto-generated entries will be added below this line by make generate // DO NOT EDIT BELOW THIS LINE - AUTO-GENERATED CONTENT + AMM: NewAMMScenario, Disperse: NewDisperseScenario, + ERC20: NewERC20Scenario, ERC20Conflict: NewERC20ConflictScenario, ERC20Noop: NewERC20NoopScenario, - ERC20: NewERC20Scenario, ERC721: NewERC721Scenario, StorageRW: NewStorageRWScenario, diff --git a/generator/scenarios/operation_internal_test.go b/generator/scenarios/operation_internal_test.go index 70c2b27..cb2e447 100644 --- a/generator/scenarios/operation_internal_test.go +++ b/generator/scenarios/operation_internal_test.go @@ -19,6 +19,8 @@ var frozenOperations = map[string]struct{}{ config.OpERC20Transfer: {}, config.OpERC721Mint: {}, config.OpDisperseEther: {}, + config.OpSwapAToB: {}, + config.OpSwapBToA: {}, } // TestEveryScenarioNamesAFrozenOperation asserts that every registered scenario @@ -37,6 +39,16 @@ func TestEveryScenarioNamesAFrozenOperation(t *testing.T) { op := factory(config.Scenario{Name: name}).Operation() require.NotEmpty(t, op) require.Contains(t, frozenOperations, op) + + // Every name the scenario can draw, not only the one it defaults to. + // A scenario with an operation set reaches the metric dimension with + // any of them, so checking the default alone leaves the rest of the + // vocabulary unchecked while the test's name says otherwise. + for _, drawable := range config.OperationNamesFor(name) { + require.Contains(t, frozenOperations, drawable, + "scenario %q can draw %q, which is not in the frozen vocabulary", + name, drawable) + } }) } }