/
simbackend.go
286 lines (246 loc) · 8.99 KB
/
simbackend.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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
// The ethtest package provides helpers for testing Ethereum smart contracts.
package ethtest
import (
"bytes"
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/proofxyz/solgo/go/eth"
"github.com/proofxyz/solgo/go/solcover"
)
// A SimulatedBackend embeds a go-ethereum SimulatedBackend and extends its
// functionality to simplify standard testing.
type SimulatedBackend struct {
*backends.SimulatedBackend
AutoCommit bool
accounts []*bind.TransactOpts
keys []*ecdsa.PrivateKey
// See comment on MockedEntity.
mockAccounts map[MockedEntity]*bind.TransactOpts
coverageReport func() []byte
}
var _ bind.ContractBackend = (*SimulatedBackend)(nil)
// NewSimulatedBackend returns a new simulated ETH backend with the specified
// number of accounts. Transactions are automatically committed unless. Close()
// must be called to free resources after use.
//
// Accounts are deterministically generated so have identical addresses between
// backends, but balances are coupled to the specific instance of the backend.
func NewSimulatedBackend(numAccounts int) (*SimulatedBackend, error) {
sb := &SimulatedBackend{
AutoCommit: true,
mockAccounts: make(map[MockedEntity]*bind.TransactOpts),
}
alloc := make(core.GenesisAlloc)
// Ensure that the pre-compiled contracts are available.
// TODO: check if this is absolutely necessary.
for addr := byte(1); addr <= 8; addr++ {
alloc[common.BytesToAddress([]byte{addr})] = core.GenesisAccount{
Balance: big.NewInt(1),
}
}
createAccount := func(seed []byte) (*bind.TransactOpts, *ecdsa.PrivateKey, error) {
entropy := bytes.NewReader(crypto.Keccak512(seed))
key, err := ecdsa.GenerateKey(crypto.S256(), entropy)
if err != nil {
return nil, nil, fmt.Errorf("ecdsa.GenerateKey(crypto.S256, [deterministic entropy; Keccak512(%q)]): %v", seed, err)
}
txOpts, err := bind.NewKeyedTransactorWithChainID(key, big.NewInt(1337))
if err != nil {
return nil, nil, fmt.Errorf("NewKeyedTransactorWithChainID(<new key>, sim-backend-id=1337): %v", err)
}
alloc[txOpts.From] = core.GenesisAccount{
Balance: eth.Ether(100),
}
return txOpts, key, nil
}
for i := 0; i < numAccounts; i++ {
txOpts, pk, err := createAccount([]byte(fmt.Sprintf("account:%d", i)))
if err != nil {
return nil, err
}
sb.accounts = append(sb.accounts, txOpts)
sb.keys = append(sb.keys, pk)
}
// These accounts need to be deterministic so that any contracts they deploy
// have deterministic addresses.
for _, mock := range []MockedEntity{OpenSea, Chainlink, Ethier, WETH} {
txOpts, _, err := createAccount([]byte(mock))
if err != nil {
return nil, err
}
sb.mockAccounts[mock] = txOpts
}
sb.SimulatedBackend = backends.NewSimulatedBackend(alloc, 3e7)
sb.AdjustTime(365 * 24 * time.Hour)
sb.Commit()
coll, report := solcover.Collector()
cfg := sb.Blockchain().GetVMConfig()
cfg.Tracer = coll
sb.coverageReport = report
return sb, nil
}
// NewSimulatedBackendTB calls NewSimulatedBackend(), reports any errors with
// tb.Fatal, and calls Close() with tb.Cleanup().
func NewSimulatedBackendTB(tb testing.TB, numAccounts int) *SimulatedBackend {
tb.Helper()
sim, err := NewSimulatedBackend(numAccounts)
if err != nil {
tb.Fatal(err)
}
tb.Cleanup(func() {
if err := sim.Close(); err != nil {
tb.Errorf("%T.Close(): %v", sim.SimulatedBackend, err)
}
})
return sim
}
// SendTransaction functions pipes its parameters to the embedded backend and
// also calls Commit() if sb.AutoCommit==true.
func (sb *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transaction) error {
if err := sb.SimulatedBackend.SendTransaction(ctx, tx); err != nil {
return err
}
if sb.AutoCommit {
sb.SimulatedBackend.Commit()
}
return nil
}
// Acc returns a TransactOpts signing as the specified account number.
func (sb *SimulatedBackend) Acc(account int) *bind.TransactOpts {
acc := sb.accounts[account]
return &bind.TransactOpts{
From: acc.From,
Signer: acc.Signer,
}
}
// Addr returns the Address of the specified account number.
func (sb *SimulatedBackend) Addr(account int) common.Address {
return sb.accounts[account].From
}
// PrivateKey returns the private key of the specified account number.
func (sb *SimulatedBackend) PrivateKey(account int) *ecdsa.PrivateKey {
return sb.keys[account]
}
// WithValueFrom returns a TransactOpts that sends the specified value from the
// account. If value==0, sb.Acc(account) can be used directly.
func (sb *SimulatedBackend) WithValueFrom(account int, value *big.Int) *bind.TransactOpts {
opts := sb.Acc(account)
opts.Value = value
return opts
}
// CallFrom returns a CallOpts from the specified account number.
func (sb *SimulatedBackend) CallFrom(account int) *bind.CallOpts {
return &bind.CallOpts{
From: sb.accounts[account].From,
}
}
// A MockedEntity mocks a real-world entity such as Uniswap or Opensea with
// deterministically generated accounts (and therefore contract addresses).
type MockedEntity string
// Mocked entities.
const (
OpenSea = MockedEntity("OpenSea")
Chainlink = MockedEntity("Chainlink")
Ethier = MockedEntity("Ethier")
WETH = MockedEntity("wETH")
)
// AsMockedEntity calls the provided function with the mocked entity's account
// if it is supported, propagating any returned error.
//
// MockedEntity accounts SHOULD NOT be used in general tests; prefer provided
// packages like openseatest to using AsMockedEntity() directly.
func (sb *SimulatedBackend) AsMockedEntity(mock MockedEntity, fn func(*bind.TransactOpts) error) error {
a, ok := sb.mockAccounts[mock]
if !ok {
return fmt.Errorf("unsupported %T %q", mock, mock)
}
return fn(a)
}
// BalanceOf returns the current balance of the address, calling tb.Fatalf on
// error.
func (sb *SimulatedBackend) BalanceOf(ctx context.Context, tb testing.TB, addr common.Address) *big.Int {
tb.Helper()
bal, err := sb.BalanceAt(ctx, addr, nil)
if err != nil {
tb.Fatalf("%T.BalanceAt(ctx, %s, nil) error %v", sb.SimulatedBackend, addr, err)
}
return bal
}
// BlockNumber returns the current block number.
func (sb *SimulatedBackend) BlockNumber() *big.Int {
return sb.Blockchain().CurrentBlock().Number
}
// FastForward calls sb.Commit() until sb.BlockNumber() >= blockNumber. It
// returns whether fast-forwarding was required; i.e. false if the requested
// block number is current or in the past.
//
// NOTE: FastForward is O(curr - requested).
func (sb *SimulatedBackend) FastForward(blockNumber *big.Int) bool {
done := false
for ; blockNumber.Cmp(sb.BlockNumber()) == 1; done = true {
// TODO: is there a more efficient way to do this?
sb.Commit()
}
return done
}
// GasSpent returns the gas spent (i.e. used*cost) by the transaction.
func (sb *SimulatedBackend) GasSpent(ctx context.Context, tb testing.TB, tx *types.Transaction) *big.Int {
rcpt, err := bind.WaitMined(ctx, sb, tx)
if err != nil {
tb.Fatalf("bind.WaitMined(<simulated backend>, %s) error %v", tx.Hash(), err)
}
return new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(rcpt.GasUsed))
}
// Must returns a function that ensures a successful transaction, reporting any
// error on tb.Fatal, or propagating the transaction.
//
// Intended usage:
//
// sb.Must(t, "ContractFunc()")(foo.ContractFunc(sim.Acc(<acc>), …))
//
// The description format and associated args will be used as a prefix in any
// reported errors. The returned function MUST be used immediately, and can only
// be used once.
func (sb *SimulatedBackend) Must(tb testing.TB, descFormat string, descArgs ...interface{}) func(*types.Transaction, error) *types.Transaction {
// This function is "naughty" and not strictly within idiomatic Go
// style. Similarly to how contexts mustn't be held within structs, holding
// a testing.T risks it becoming irrelevant with respect to the scope within
// which it's used. To avoid this, we limit the returned function to single
// use; it's not a perfect solution, but a user would have to deliberately
// misuse the API.
var used bool
desc := fmt.Sprintf(descFormat, descArgs...)
return func(tx *types.Transaction, err error) *types.Transaction {
tb.Helper()
if used {
tb.Errorf("Function returned by %T.Must(%q) must only be used once", sb, desc)
}
used = true
if err != nil {
tb.Fatalf("%s; got err %v; want nil err", desc, err)
return nil
}
return tx
}
}
// CoverageReport returns an LCOV trace file for contracts registered mapped by
// an solcover.Collector() EVMLogger injected into the SimulatedBackend during
// construction. The report can be generated at any time that collection is not
// currently active (i.e. it is not threadsafe with respect to the VM). See
// solcover.Collector() for more information.
func (sb *SimulatedBackend) CoverageReport() []byte {
if sb.coverageReport == nil {
return nil
}
return sb.coverageReport()
}