prevent stuck transactions by async locking nonce sequencing (+ estimate gas) (#55)
- async lock during nonce sequencing + gas estimation - simplified cancelTransaction (still exported) such that the new transaction is populated using populateTransaction, so that all gas and fees are reset - moved reverting contract function into its own testing helpers module, and refactored any tests to use it - updated the test helper reverts to check EstimateGasErrors - combine ensureNonceSequence into populateTransaction
This commit is contained in:
parent
620b402a7d
commit
7eac8410af
|
@ -246,7 +246,6 @@ proc confirm*(tx: Future[?TransactionResponse],
|
||||||
## `await token.connect(signer0)
|
## `await token.connect(signer0)
|
||||||
## .mint(accounts[1], 100.u256)
|
## .mint(accounts[1], 100.u256)
|
||||||
## .confirm(3)`
|
## .confirm(3)`
|
||||||
|
|
||||||
without response =? (await tx):
|
without response =? (await tx):
|
||||||
raise newException(
|
raise newException(
|
||||||
EthersError,
|
EthersError,
|
||||||
|
|
|
@ -28,7 +28,7 @@ type
|
||||||
subscriptions: JsonRpcSubscriptions
|
subscriptions: JsonRpcSubscriptions
|
||||||
id: JsonNode
|
id: JsonNode
|
||||||
|
|
||||||
proc raiseProviderError(message: string) {.upraises: [JsonRpcProviderError].} =
|
proc raiseJsonRpcProviderError(message: string) {.upraises: [JsonRpcProviderError].} =
|
||||||
var message = message
|
var message = message
|
||||||
try:
|
try:
|
||||||
message = parseJson(message){"message"}.getStr
|
message = parseJson(message){"message"}.getStr
|
||||||
|
@ -40,11 +40,11 @@ template convertError(body) =
|
||||||
try:
|
try:
|
||||||
body
|
body
|
||||||
except JsonRpcError as error:
|
except JsonRpcError as error:
|
||||||
raiseProviderError(error.msg)
|
raiseJsonRpcProviderError(error.msg)
|
||||||
# Catch all ValueErrors for now, at least until JsonRpcError is actually
|
# Catch all ValueErrors for now, at least until JsonRpcError is actually
|
||||||
# raised. PR created: https://github.com/status-im/nim-json-rpc/pull/151
|
# raised. PR created: https://github.com/status-im/nim-json-rpc/pull/151
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
raiseProviderError(error.msg)
|
raiseJsonRpcProviderError(error.msg)
|
||||||
|
|
||||||
# Provider
|
# Provider
|
||||||
|
|
||||||
|
@ -228,7 +228,7 @@ method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} =
|
||||||
if accounts.len > 0:
|
if accounts.len > 0:
|
||||||
return accounts[0]
|
return accounts[0]
|
||||||
|
|
||||||
raiseProviderError "no address found"
|
raiseJsonRpcProviderError "no address found"
|
||||||
|
|
||||||
method signMessage*(signer: JsonRpcSigner,
|
method signMessage*(signer: JsonRpcSigner,
|
||||||
message: seq[byte]): Future[seq[byte]] {.async.} =
|
message: seq[byte]): Future[seq[byte]] {.async.} =
|
||||||
|
@ -240,7 +240,8 @@ method signMessage*(signer: JsonRpcSigner,
|
||||||
method sendTransaction*(signer: JsonRpcSigner,
|
method sendTransaction*(signer: JsonRpcSigner,
|
||||||
transaction: Transaction): Future[TransactionResponse] {.async.} =
|
transaction: Transaction): Future[TransactionResponse] {.async.} =
|
||||||
convertError:
|
convertError:
|
||||||
signer.updateNonce(transaction.nonce)
|
if nonce =? transaction.nonce:
|
||||||
|
signer.updateNonce(nonce)
|
||||||
let
|
let
|
||||||
client = await signer.provider.client
|
client = await signer.provider.client
|
||||||
hash = await client.eth_sendTransaction(transaction)
|
hash = await client.eth_sendTransaction(transaction)
|
||||||
|
|
|
@ -6,11 +6,25 @@ export basics
|
||||||
type
|
type
|
||||||
Signer* = ref object of RootObj
|
Signer* = ref object of RootObj
|
||||||
lastSeenNonce: ?UInt256
|
lastSeenNonce: ?UInt256
|
||||||
|
populateLock: AsyncLock
|
||||||
|
|
||||||
type SignerError* = object of EthersError
|
type
|
||||||
|
SignerError* = object of EthersError
|
||||||
|
EstimateGasError* = object of SignerError
|
||||||
|
transaction*: Transaction
|
||||||
|
|
||||||
template raiseSignerError(message: string) =
|
template raiseSignerError(message: string, parent: ref ProviderError = nil) =
|
||||||
raise newException(SignerError, message)
|
raise newException(SignerError, message, parent)
|
||||||
|
|
||||||
|
proc raiseEstimateGasError(
|
||||||
|
transaction: Transaction,
|
||||||
|
parent: ref ProviderError = nil
|
||||||
|
) =
|
||||||
|
let e = (ref EstimateGasError)(
|
||||||
|
msg: "Estimate gas failed",
|
||||||
|
transaction: transaction,
|
||||||
|
parent: parent)
|
||||||
|
raise e
|
||||||
|
|
||||||
method provider*(signer: Signer): Provider {.base, gcsafe.} =
|
method provider*(signer: Signer): Provider {.base, gcsafe.} =
|
||||||
doAssert false, "not implemented"
|
doAssert false, "not implemented"
|
||||||
|
@ -39,7 +53,10 @@ method estimateGas*(signer: Signer,
|
||||||
transaction: Transaction): Future[UInt256] {.base, async.} =
|
transaction: Transaction): Future[UInt256] {.base, async.} =
|
||||||
var transaction = transaction
|
var transaction = transaction
|
||||||
transaction.sender = some(await signer.getAddress)
|
transaction.sender = some(await signer.getAddress)
|
||||||
return await signer.provider.estimateGas(transaction)
|
try:
|
||||||
|
return await signer.provider.estimateGas(transaction)
|
||||||
|
except ProviderError as e:
|
||||||
|
raiseEstimateGasError transaction, e
|
||||||
|
|
||||||
method getChainId*(signer: Signer): Future[UInt256] {.base, gcsafe.} =
|
method getChainId*(signer: Signer): Future[UInt256] {.base, gcsafe.} =
|
||||||
signer.provider.getChainId()
|
signer.provider.getChainId()
|
||||||
|
@ -53,9 +70,10 @@ method getNonce(signer: Signer): Future[UInt256] {.base, gcsafe, async.} =
|
||||||
|
|
||||||
return nonce
|
return nonce
|
||||||
|
|
||||||
method updateNonce*(signer: Signer, nonce: ?UInt256) {.base, gcsafe.} =
|
method updateNonce*(
|
||||||
without nonce =? nonce:
|
signer: Signer,
|
||||||
return
|
nonce: UInt256
|
||||||
|
) {.base, gcsafe.} =
|
||||||
|
|
||||||
without lastSeen =? signer.lastSeenNonce:
|
without lastSeen =? signer.lastSeenNonce:
|
||||||
signer.lastSeenNonce = some nonce
|
signer.lastSeenNonce = some nonce
|
||||||
|
@ -64,6 +82,10 @@ method updateNonce*(signer: Signer, nonce: ?UInt256) {.base, gcsafe.} =
|
||||||
if nonce > lastSeen:
|
if nonce > lastSeen:
|
||||||
signer.lastSeenNonce = some nonce
|
signer.lastSeenNonce = some nonce
|
||||||
|
|
||||||
|
method decreaseNonce*(signer: Signer) {.base, gcsafe.} =
|
||||||
|
if lastSeen =? signer.lastSeenNonce and lastSeen > 0:
|
||||||
|
signer.lastSeenNonce = some lastSeen - 1
|
||||||
|
|
||||||
method populateTransaction*(signer: Signer,
|
method populateTransaction*(signer: Signer,
|
||||||
transaction: Transaction):
|
transaction: Transaction):
|
||||||
Future[Transaction] {.base, async.} =
|
Future[Transaction] {.base, async.} =
|
||||||
|
@ -73,17 +95,55 @@ method populateTransaction*(signer: Signer,
|
||||||
if chainId =? transaction.chainId and chainId != await signer.getChainId():
|
if chainId =? transaction.chainId and chainId != await signer.getChainId():
|
||||||
raiseSignerError("chain id mismatch")
|
raiseSignerError("chain id mismatch")
|
||||||
|
|
||||||
|
if signer.populateLock.isNil:
|
||||||
|
signer.populateLock = newAsyncLock()
|
||||||
|
|
||||||
|
await signer.populateLock.acquire()
|
||||||
|
|
||||||
var populated = transaction
|
var populated = transaction
|
||||||
|
|
||||||
if transaction.sender.isNone:
|
if transaction.sender.isNone:
|
||||||
populated.sender = some(await signer.getAddress())
|
populated.sender = some(await signer.getAddress())
|
||||||
if transaction.nonce.isNone:
|
|
||||||
populated.nonce = some(await signer.getNonce())
|
|
||||||
if transaction.chainId.isNone:
|
if transaction.chainId.isNone:
|
||||||
populated.chainId = some(await signer.getChainId())
|
populated.chainId = some(await signer.getChainId())
|
||||||
if transaction.gasPrice.isNone and (transaction.maxFee.isNone or transaction.maxPriorityFee.isNone):
|
if transaction.gasPrice.isNone and (populated.maxFee.isNone or populated.maxPriorityFee.isNone):
|
||||||
populated.gasPrice = some(await signer.getGasPrice())
|
populated.gasPrice = some(await signer.getGasPrice())
|
||||||
if transaction.gasLimit.isNone:
|
|
||||||
populated.gasLimit = some(await signer.estimateGas(populated))
|
if transaction.nonce.isNone and transaction.gasLimit.isNone:
|
||||||
|
# when both nonce and gasLimit are not populated, we must ensure getNonce is
|
||||||
|
# followed by an estimateGas so we can determine if there was an error. If
|
||||||
|
# there is an error, the nonce must be deprecated to prevent nonce gaps and
|
||||||
|
# stuck transactions
|
||||||
|
try:
|
||||||
|
populated.nonce = some(await signer.getNonce())
|
||||||
|
populated.gasLimit = some(await signer.estimateGas(populated))
|
||||||
|
except ProviderError, EstimateGasError:
|
||||||
|
let e = getCurrentException()
|
||||||
|
signer.decreaseNonce()
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
signer.populateLock.release()
|
||||||
|
|
||||||
|
else:
|
||||||
|
if transaction.nonce.isNone:
|
||||||
|
populated.nonce = some(await signer.getNonce())
|
||||||
|
if transaction.gasLimit.isNone:
|
||||||
|
populated.gasLimit = some(await signer.estimateGas(populated))
|
||||||
|
|
||||||
return populated
|
return populated
|
||||||
|
|
||||||
|
method cancelTransaction*(
|
||||||
|
signer: Signer,
|
||||||
|
tx: Transaction
|
||||||
|
): Future[TransactionResponse] {.async, base.} =
|
||||||
|
# cancels a transaction by sending with a 0-valued transaction to ourselves
|
||||||
|
# with the failed tx's nonce
|
||||||
|
|
||||||
|
without sender =? tx.sender:
|
||||||
|
raiseSignerError "transaction must have sender"
|
||||||
|
without nonce =? tx.nonce:
|
||||||
|
raiseSignerError "transaction must have nonce"
|
||||||
|
|
||||||
|
var cancelTx = Transaction(to: sender, value: 0.u256, nonce: some nonce)
|
||||||
|
cancelTx = await signer.populateTransaction(cancelTx)
|
||||||
|
return await signer.sendTransaction(cancelTx)
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import std/strutils
|
import std/strutils
|
||||||
import ./provider
|
import ./provider
|
||||||
|
import ./signer
|
||||||
|
|
||||||
proc revertReason*(e: ref ProviderError): string =
|
proc revertReason*(emsg: string): string =
|
||||||
var msg = e.msg
|
var msg = emsg
|
||||||
const revertPrefixes = @[
|
const revertPrefixes = @[
|
||||||
# hardhat
|
# hardhat
|
||||||
"Error: VM Exception while processing transaction: reverted with " &
|
"Error: VM Exception while processing transaction: reverted with " &
|
||||||
|
@ -15,6 +16,10 @@ proc revertReason*(e: ref ProviderError): string =
|
||||||
msg = msg.replace("\'")
|
msg = msg.replace("\'")
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
proc revertReason*(e: ref EthersError): string =
|
||||||
|
var msg = e.msg
|
||||||
|
msg.revertReason
|
||||||
|
|
||||||
proc reverts*[T](call: Future[T]): Future[bool] {.async.} =
|
proc reverts*[T](call: Future[T]): Future[bool] {.async.} =
|
||||||
try:
|
try:
|
||||||
when T is void:
|
when T is void:
|
||||||
|
@ -22,7 +27,7 @@ proc reverts*[T](call: Future[T]): Future[bool] {.async.} =
|
||||||
else:
|
else:
|
||||||
discard await call
|
discard await call
|
||||||
return false
|
return false
|
||||||
except ProviderError:
|
except ProviderError, EstimateGasError:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
proc reverts*[T](call: Future[T], reason: string): Future[bool] {.async.} =
|
proc reverts*[T](call: Future[T], reason: string): Future[bool] {.async.} =
|
||||||
|
@ -32,5 +37,12 @@ proc reverts*[T](call: Future[T], reason: string): Future[bool] {.async.} =
|
||||||
else:
|
else:
|
||||||
discard await call
|
discard await call
|
||||||
return false
|
return false
|
||||||
except ProviderError as error:
|
except ProviderError, EstimateGasError:
|
||||||
return reason == error.revertReason
|
let e = getCurrentException()
|
||||||
|
var passed = reason == (ref EthersError)(e).revertReason
|
||||||
|
if not passed and
|
||||||
|
not e.parent.isNil and
|
||||||
|
e.parent of (ref EthersError):
|
||||||
|
let revertReason = (ref EthersError)(e.parent).revertReason
|
||||||
|
passed = reason == revertReason
|
||||||
|
return passed
|
||||||
|
|
|
@ -70,5 +70,6 @@ proc signTransaction*(wallet: Wallet,
|
||||||
|
|
||||||
method sendTransaction*(wallet: Wallet, transaction: Transaction): Future[TransactionResponse] {.async.} =
|
method sendTransaction*(wallet: Wallet, transaction: Transaction): Future[TransactionResponse] {.async.} =
|
||||||
let signed = await signTransaction(wallet, transaction)
|
let signed = await signTransaction(wallet, transaction)
|
||||||
wallet.updateNonce(transaction.nonce)
|
if nonce =? transaction.nonce:
|
||||||
|
wallet.updateNonce(nonce)
|
||||||
return await provider(wallet).sendTransaction(signed)
|
return await provider(wallet).sendTransaction(signed)
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import pkg/ethers
|
||||||
|
import ./hardhat
|
||||||
|
|
||||||
|
type
|
||||||
|
TestHelpers* = ref object of Contract
|
||||||
|
|
||||||
|
method doRevert*(
|
||||||
|
self: TestHelpers,
|
||||||
|
revertReason: string
|
||||||
|
): ?TransactionResponse {.base, contract.}
|
||||||
|
|
||||||
|
proc new*(_: type TestHelpers, signer: Signer): TestHelpers =
|
||||||
|
let deployment = readDeployment()
|
||||||
|
TestHelpers.new(!deployment.address(TestHelpers), signer)
|
|
@ -1,10 +1,12 @@
|
||||||
import std/json
|
import std/json
|
||||||
|
import std/options
|
||||||
import pkg/asynctest
|
import pkg/asynctest
|
||||||
import pkg/questionable
|
import pkg/questionable
|
||||||
import pkg/stint
|
import pkg/stint
|
||||||
import pkg/ethers
|
import pkg/ethers
|
||||||
import pkg/ethers/erc20
|
import pkg/ethers/erc20
|
||||||
import ./hardhat
|
import ./hardhat
|
||||||
|
import ./helpers
|
||||||
import ./miner
|
import ./miner
|
||||||
import ./mocks
|
import ./mocks
|
||||||
|
|
||||||
|
@ -235,3 +237,32 @@ for url in ["ws://localhost:8545", "http://localhost:8545"]:
|
||||||
check logs == @[
|
check logs == @[
|
||||||
Transfer(receiver: accounts[0], value: 100.u256)
|
Transfer(receiver: accounts[0], value: 100.u256)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
test "concurrent transactions with first failing increment nonce correctly":
|
||||||
|
let signer = provider.getSigner()
|
||||||
|
let token = TestToken.new(token.address, signer)
|
||||||
|
let helpersContract = TestHelpers.new(signer)
|
||||||
|
|
||||||
|
# emulate concurrent populateTransaction calls, where the first one fails
|
||||||
|
let futs = await allFinished(
|
||||||
|
helpersContract.doRevert("some reason"),
|
||||||
|
token.mint(accounts[0], 100.u256)
|
||||||
|
)
|
||||||
|
check futs[0].error of EstimateGasError
|
||||||
|
let receipt = await futs[1].confirm(1)
|
||||||
|
|
||||||
|
check receipt.status == TransactionStatus.Success
|
||||||
|
|
||||||
|
test "non-concurrent transactions with first failing increment nonce correctly":
|
||||||
|
let signer = provider.getSigner()
|
||||||
|
let token = TestToken.new(token.address, signer)
|
||||||
|
let helpersContract = TestHelpers.new(signer)
|
||||||
|
|
||||||
|
expect EstimateGasError:
|
||||||
|
discard await helpersContract.doRevert("some reason")
|
||||||
|
|
||||||
|
let receipt = await token
|
||||||
|
.mint(accounts[0], 100.u256)
|
||||||
|
.confirm(1)
|
||||||
|
|
||||||
|
check receipt.status == TransactionStatus.Success
|
||||||
|
|
|
@ -3,7 +3,7 @@ import pkg/asynctest
|
||||||
import pkg/chronos
|
import pkg/chronos
|
||||||
import pkg/ethers
|
import pkg/ethers
|
||||||
import pkg/ethers/testing
|
import pkg/ethers/testing
|
||||||
import ./hardhat
|
import ./helpers
|
||||||
|
|
||||||
suite "Testing helpers":
|
suite "Testing helpers":
|
||||||
|
|
||||||
|
@ -13,13 +13,13 @@ suite "Testing helpers":
|
||||||
|
|
||||||
test "checks that call reverts":
|
test "checks that call reverts":
|
||||||
proc call() {.async.} =
|
proc call() {.async.} =
|
||||||
raise newException(ProviderError, $rpcResponse)
|
raise newException(EstimateGasError, $rpcResponse)
|
||||||
|
|
||||||
check await call().reverts()
|
check await call().reverts()
|
||||||
|
|
||||||
test "checks reason for revert":
|
test "checks reason for revert":
|
||||||
proc call() {.async.} =
|
proc call() {.async.} =
|
||||||
raise newException(ProviderError, $rpcResponse)
|
raise newException(EstimateGasError, $rpcResponse)
|
||||||
|
|
||||||
check await call().reverts(revertReason)
|
check await call().reverts(revertReason)
|
||||||
|
|
||||||
|
@ -28,19 +28,31 @@ suite "Testing helpers":
|
||||||
|
|
||||||
check not await call().reverts()
|
check not await call().reverts()
|
||||||
|
|
||||||
test "reverts only checks ProviderErrors":
|
test "reverts only checks ProviderErrors, EstimateGasErrors":
|
||||||
proc call() {.async.} =
|
proc callProviderError() {.async.} =
|
||||||
raise newException(ContractError, "test")
|
raise newException(ProviderError, "test")
|
||||||
|
proc callEstimateGasError() {.async.} =
|
||||||
|
raise newException(EstimateGasError, "test")
|
||||||
|
proc callEthersError() {.async.} =
|
||||||
|
raise newException(EthersError, "test")
|
||||||
|
|
||||||
expect ContractError:
|
check await callProviderError().reverts()
|
||||||
check await call().reverts()
|
check await callEstimateGasError().reverts()
|
||||||
|
expect EthersError:
|
||||||
|
check await callEthersError().reverts()
|
||||||
|
|
||||||
test "reverts with reason only checks ProviderErrors":
|
test "reverts with reason only checks ProviderErrors, EstimateGasErrors":
|
||||||
proc call() {.async.} =
|
proc callProviderError() {.async.} =
|
||||||
raise newException(ContractError, "test")
|
raise newException(ProviderError, revertReason)
|
||||||
|
proc callEstimateGasError() {.async.} =
|
||||||
|
raise newException(EstimateGasError, revertReason)
|
||||||
|
proc callEthersError() {.async.} =
|
||||||
|
raise newException(EthersError, revertReason)
|
||||||
|
|
||||||
expect ContractError:
|
check await callProviderError().reverts(revertReason)
|
||||||
check await call().reverts(revertReason)
|
check await callEstimateGasError().reverts(revertReason)
|
||||||
|
expect EthersError:
|
||||||
|
check await callEthersError().reverts(revertReason)
|
||||||
|
|
||||||
test "reverts with reason is false when there is no revert":
|
test "reverts with reason is false when there is no revert":
|
||||||
proc call() {.async.} = discard
|
proc call() {.async.} = discard
|
||||||
|
@ -49,29 +61,24 @@ suite "Testing helpers":
|
||||||
|
|
||||||
test "reverts is false when the revert reason doesn't match":
|
test "reverts is false when the revert reason doesn't match":
|
||||||
proc call() {.async.} =
|
proc call() {.async.} =
|
||||||
raise newException(ProviderError, "other reason")
|
raise newException(EstimateGasError, "other reason")
|
||||||
|
|
||||||
check not await call().reverts(revertReason)
|
check not await call().reverts(revertReason)
|
||||||
|
|
||||||
test "revert handles non-standard revert prefix":
|
test "revert handles non-standard revert prefix":
|
||||||
let nonStdMsg = fmt"Provider VM Exception: reverted with {revertReason}"
|
let nonStdMsg = fmt"Provider VM Exception: reverted with {revertReason}"
|
||||||
proc call() {.async.} =
|
proc call() {.async.} =
|
||||||
raise newException(ProviderError, nonStdMsg)
|
raise newException(EstimateGasError, nonStdMsg)
|
||||||
|
|
||||||
check await call().reverts(nonStdMsg)
|
check await call().reverts(nonStdMsg)
|
||||||
|
|
||||||
test "works with functions that return a value":
|
test "works with functions that return a value":
|
||||||
proc call(): Future[int] {.async.} = return 42
|
proc call(): Future[int] {.async.} = return 42
|
||||||
check not await call().reverts()
|
check not await call().reverts()
|
||||||
check not await call().reverts("some reason")
|
check not await call().reverts(revertReason)
|
||||||
|
|
||||||
type
|
|
||||||
TestHelpers* = ref object of Contract
|
|
||||||
|
|
||||||
method revertsWith*(self: TestHelpers,
|
suite "Testing helpers - contracts":
|
||||||
revertReason: string) {.base, contract, view.}
|
|
||||||
|
|
||||||
suite "Testing helpers - provider":
|
|
||||||
|
|
||||||
var helpersContract: TestHelpers
|
var helpersContract: TestHelpers
|
||||||
var provider: JsonRpcProvider
|
var provider: JsonRpcProvider
|
||||||
|
@ -83,12 +90,11 @@ suite "Testing helpers - provider":
|
||||||
provider = JsonRpcProvider.new("ws://127.0.0.1:8545")
|
provider = JsonRpcProvider.new("ws://127.0.0.1:8545")
|
||||||
snapshot = await provider.send("evm_snapshot")
|
snapshot = await provider.send("evm_snapshot")
|
||||||
accounts = await provider.listAccounts()
|
accounts = await provider.listAccounts()
|
||||||
let deployment = readDeployment()
|
helpersContract = TestHelpers.new(provider.getSigner())
|
||||||
helpersContract = TestHelpers.new(!deployment.address(TestHelpers), provider)
|
|
||||||
|
|
||||||
teardown:
|
teardown:
|
||||||
discard await provider.send("evm_revert", @[snapshot])
|
discard await provider.send("evm_revert", @[snapshot])
|
||||||
await provider.close()
|
await provider.close()
|
||||||
|
|
||||||
test "revert works with provider":
|
test "revert works with provider":
|
||||||
check await helpersContract.revertsWith(revertReason).reverts(revertReason)
|
check await helpersContract.doRevert(revertReason).reverts(revertReason)
|
||||||
|
|
|
@ -3,7 +3,8 @@ pragma solidity ^0.8.0;
|
||||||
|
|
||||||
contract TestHelpers {
|
contract TestHelpers {
|
||||||
|
|
||||||
function revertsWith(string calldata revertReason) public pure {
|
function doRevert(string calldata reason) public pure {
|
||||||
require(false, revertReason);
|
// Revert every tx with given reason
|
||||||
|
require(false, reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue