-
Notifications
You must be signed in to change notification settings - Fork 3k
/
binding.ts
223 lines (194 loc) · 7.4 KB
/
binding.ts
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
/* Imports: External */
import { HardhatNetworkProvider } from 'hardhat/internal/hardhat-network/provider/provider'
import { decodeRevertReason } from 'hardhat/internal/hardhat-network/stack-traces/revert-reasons'
import { VmError } from '@nomiclabs/ethereumjs-vm/dist/exceptions'
import BN from 'bn.js'
// Handle hardhat ^2.2.0
let TransactionExecutionError: any
try {
// tslint:disable-next-line
TransactionExecutionError = require('hardhat/internal/hardhat-network/provider/errors')
.TransactionExecutionError
} catch (err) {
// tslint:disable-next-line
TransactionExecutionError = require('hardhat/internal/core/providers/errors')
.TransactionExecutionError
}
/* Imports: Internal */
import { MockContract, SmockedVM } from './types'
import { fromFancyAddress, toFancyAddress } from '../common'
/**
* Checks to see if smock has been initialized already. Basically just checking to see if we've
* attached smock state to the VM already.
* @param provider Base hardhat network provider to check.
* @return Whether or not the provider has already been modified to support smock.
*/
const isSmockInitialized = (provider: HardhatNetworkProvider): boolean => {
return (provider as any)._node._vm._smockState !== undefined
}
/**
* Modifies a hardhat provider to be compatible with smock.
* @param provider Base hardhat network provider to modify.
*/
const initializeSmock = (provider: HardhatNetworkProvider): void => {
if (isSmockInitialized(provider)) {
return
}
// Will need to reference these things.
const node = (provider as any)._node
const vm: SmockedVM = node._vm
// Attach some extra state to the VM.
vm._smockState = {
mocks: {},
calls: {},
messages: [],
}
// Wipe out our list of calls before each transaction.
vm.on('beforeTx', () => {
vm._smockState.calls = {}
})
// Watch for new EVM messages (call frames).
vm.on('beforeMessage', (message: any) => {
// Happens with contract creations. If the current message is a contract creation then it can't
// be a call to a smocked contract.
if (!message.to) {
return
}
let target: string
if (message.delegatecall) {
target = fromFancyAddress(message._codeAddress)
} else {
target = fromFancyAddress(message.to)
}
// Check if the target address is a smocked contract.
if (!(target in vm._smockState.mocks)) {
return
}
// Initialize the array of calls to this smock if not done already.
if (!(target in vm._smockState.calls)) {
vm._smockState.calls[target] = []
}
// Record this message for later.
vm._smockState.calls[target].push(message.data)
vm._smockState.messages.push(message)
})
// Now *this* is a hack.
// Ethereumjs-vm passes `result` by *reference* into the `afterMessage` event. Mutating the
// `result` object here will actually mutate the result in the VM. Magic.
vm.on('afterMessage', async (result: any) => {
// We currently defer to contract creations, meaning we'll "unsmock" an address if a user
// later creates a contract at that address. Not sure how to handle this case. Very open to
// ideas.
if (result.createdAddress) {
const created = fromFancyAddress(result.createdAddress)
if (created in vm._smockState.mocks) {
delete vm._smockState.mocks[created]
}
}
// Check if we have messages that need to be handled.
if (vm._smockState.messages.length === 0) {
return
}
// Handle the last message that was pushed to the array of messages. This works because smock
// contracts never create new sub-calls (meaning this `afterMessage` event corresponds directly
// to a `beforeMessage` event emitted during a call to a smock contract).
const message = vm._smockState.messages.pop()
let target: string
if (message.delegatecall) {
target = fromFancyAddress(message._codeAddress)
} else {
target = fromFancyAddress(message.to)
}
// Not sure if this can ever actually happen? Just being safe.
if (!(target in vm._smockState.mocks)) {
return
}
// Compute the mock return data.
const mock: MockContract = vm._smockState.mocks[target]
const {
resolve,
functionName,
rawReturnValue,
returnValue,
gasUsed,
} = await mock._smockit(message.data)
// Set the mock return data, potentially set the `exceptionError` field if the user requested
// a revert.
result.gasUsed = new BN(gasUsed)
result.execResult.returnValue = returnValue
result.execResult.gasUsed = new BN(gasUsed)
result.execResult.exceptionError =
resolve === 'revert' ? new VmError('smocked revert' as any) : undefined
})
// Here we're fixing with hardhat's internal error management. Smock is a bit weird and messes
// with stack traces so we need to help hardhat out a bit when it comes to smock-specific
// errors.
const originalManagerErrorsFn = node._manageErrors.bind(node)
node._manageErrors = async (
vmResult: any,
vmTrace: any,
vmTracerError?: any
): Promise<any> => {
if (
vmResult.exceptionError &&
vmResult.exceptionError.error === 'smocked revert'
) {
return new TransactionExecutionError(
`VM Exception while processing transaction: revert ${decodeRevertReason(
vmResult.returnValue
)}`
)
}
return originalManagerErrorsFn(vmResult, vmTrace, vmTracerError)
}
}
/**
* Attaches a smocked contract to a hardhat network provider. Will also modify the provider to be
* compatible with smock if not done already.
* @param mock Smocked contract to attach to a provider.
* @param provider Hardhat network provider to attach the contract to.
*/
export const bindSmock = async (
mock: MockContract,
provider: HardhatNetworkProvider
): Promise<void> => {
if (!isSmockInitialized(provider)) {
initializeSmock(provider)
}
const vm: SmockedVM = (provider as any)._node._vm
const pStateManager = vm.pStateManager || vm.stateManager
// Add mock to our list of mocks currently attached to the VM.
vm._smockState.mocks[mock.address.toLowerCase()] = mock
// Set the contract code for our mock to 0x00 == STOP. Need some non-empty contract code because
// Solidity will sometimes throw if it's calling something without code (I forget the exact
// scenario that causes this throw).
await pStateManager.putContractCode(
toFancyAddress(mock.address),
Buffer.from('00', 'hex')
)
}
/**
* Detaches a smocked contract from a hardhat network provider.
* @param mock Smocked contract to detach to a provider, or an address.
* @param provider Hardhat network provider to detatch the contract from.
*/
export const unbindSmock = async (
mock: MockContract | string,
provider: HardhatNetworkProvider
): Promise<void> => {
if (!isSmockInitialized(provider)) {
initializeSmock(provider)
}
const vm: SmockedVM = (provider as any)._node._vm
const pStateManager = vm.pStateManager || vm.stateManager
// Add mock to our list of mocks currently attached to the VM.
const address = typeof mock === 'string' ? mock : mock.address.toLowerCase()
delete vm._smockState.mocks[address]
// Set the contract code for our mock to 0x00 == STOP. Need some non-empty contract code because
// Solidity will sometimes throw if it's calling something without code (I forget the exact
// scenario that causes this throw).
await pStateManager.putContractCode(
toFancyAddress(address),
Buffer.from('', 'hex')
)
}