Commit 21724d2c authored by Georgios Konstantopoulos's avatar Georgios Konstantopoulos Committed by GitHub

feat: smock (#8)

* chore(core-utils): cleanup dir config files

* fix: use proper tsconfig files

* feat: copy over smock
parent 24b75c11
...@@ -38,6 +38,9 @@ jobs: ...@@ -38,6 +38,9 @@ jobs:
- name: Install Dependencies - name: Install Dependencies
run: yarn install run: yarn install
- name: Build
run: yarn build
- name: Lint - name: Lint
run: yarn lint run: yarn lint
......
node_modules/ node_modules
dist
results
.nyc_output
*.tsbuildinfo
...@@ -6,10 +6,12 @@ ...@@ -6,10 +6,12 @@
git clone git@github.com:ethereum-optimism/optimism.git git clone git@github.com:ethereum-optimism/optimism.git
cd optimism cd optimism
yarn yarn
yarn lint yarn build # this is needed to generate the dist files locally
yarn test yarn test
``` ```
If you get dependency errors: `git clean -fx && yarn clean && rm -rf node_modules/@eth-optimism/* && yarn install --force`
## Taming the Monorepo ## Taming the Monorepo
1. You solely use yarn workspaces for the Mono-Repo workflow. 1. You solely use yarn workspaces for the Mono-Repo workflow.
......
{ {
"name": "optimism", "name": "optimism",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
...@@ -11,7 +10,10 @@ ...@@ -11,7 +10,10 @@
"lerna": "^4.0.0" "lerna": "^4.0.0"
}, },
"scripts": { "scripts": {
"clean": "yarn workspaces run clean",
"build": "yarn workspaces run build",
"test": "yarn workspaces run test", "test": "yarn workspaces run test",
"lint": "yarn workspaces run lint" "lint": "yarn workspaces run lint",
"lint:fix": "yarn workspaces run lint:fix"
} }
} }
{ {
"name": "@eth-optimism/core-utils", "name": "@eth-optimism/core-utils",
"version": "0.1.10", "version": "0.1.10",
"main": "build/src/index.js", "main": "dist/index",
"files": [ "files": [
"build/src/**/*" "dist/index"
], ],
"types": "build/src/index.d.ts", "types": "build/src/index.d.ts",
"repository": "git@github.com:ethereum-optimism/core-utils.git", "repository": "git@github.com:ethereum-optimism/core-utils.git",
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"all": "yarn clean && yarn build && yarn test && yarn lint:fix && yarn lint", "all": "yarn clean && yarn build && yarn test && yarn lint:fix && yarn lint",
"build": "tsc -p .", "build": "tsc -p tsconfig.build.json",
"clean": "rimraf build/", "clean": "rimraf dist/",
"lint": "tslint --format stylish --project .", "lint": "tslint --format stylish --project .",
"lint:fix": "prettier --config prettier-config.json --write '{src,test}/**/*.ts'", "lint:fix": "prettier --config prettier-config.json --write '{src,test}/**/*.ts'",
"test": "ts-mocha test/**/*.spec.ts" "test": "ts-mocha test/**/*.spec.ts"
...@@ -31,8 +31,6 @@ ...@@ -31,8 +31,6 @@
}, },
"dependencies": { "dependencies": {
"@ethersproject/abstract-provider": "^5.0.9", "@ethersproject/abstract-provider": "^5.0.9",
"colors": "^1.4.0",
"debug": "^4.3.1",
"ethers": "^5.0.31", "ethers": "^5.0.31",
"pino": "^6.11.1" "pino": "^6.11.1"
} }
......
{
"$schema": "http://json.schemastore.org/prettierrc",
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"arrowParens": "always"
}
../../prettier-config.json
\ No newline at end of file
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": [
"src/**/*"
]
}
{ {
"extends" : "../../tsconfig.base.json" "extends": "../../tsconfig.json"
} }
name: smock - lint, build, test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build-test-lint:
name: Run job on ${{matrix.node}}
runs-on: ubuntu-latest
strategy:
matrix:
node: [ '10', '12', '14' ]
steps:
- uses: actions/checkout@v2
- name: Setup node ${{ matrix.node }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
# START DEPENDENCY CACHING
- name: Cache root deps
uses: actions/cache@v1
id: cache_base
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('package.json') }}
# END DEPENDENCY CACHING
- name: Install Dependencies
run: yarn install
- name: Lint
run: yarn lint
- name: Build
run: |
yarn clean
yarn build
- name: Test
run: yarn test
name: Auto tag-release-publish
on:
push:
branches:
- main
jobs:
tag:
name: Create tag for new version
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.create_new_tag.outputs.tag }}
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 2
- uses: salsify/action-detect-and-tag-new-version@v2
id: create_new_tag
release:
name: Create release
runs-on: ubuntu-latest
needs: tag
if: needs.tag.outputs.tag_name
steps:
- uses: actions/checkout@v2
- uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ needs.tag.outputs.tag_name }}
release_name: ${{ needs.tag.outputs.tag_name }}
draft: false
prerelease: false
publish:
name: Build and publish
runs-on: ubuntu-latest
needs: tag
if: needs.tag.outputs.tag_name
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- run: yarn
- run: yarn build
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
node_modules/
cache/
artifacts/
build/
.DS_Store
temp/
\ No newline at end of file
# @eth-optimisim/smock
`smock` is a utility package that can generate mock Solidity contracts (for testing). `smock` hooks into a `ethereumjs-vm` instance so that mock contract functions can be written entirely in JavaScript. `smock` currently only supports [Hardhat](http://hardhat.org/), but will be extended to support other testing frameworks.
Some nice benefits of hooking in at the VM level:
* Don't need to deploy any special contracts just for mocking!
* All of the calls are synchronous.
* Perform arbitrary javascript logic within your return value (return a function).
* It sounds cool.
`smock` also contains `smoddit`, another utility that allows you to modify the internal storage of contracts. We've found this to be quite useful in cases where many interactions occur within a single contract (typically to save gas).
## Installation
You can easily install `smock` via `npm`:
```sh
npm install @eth-optimism/smock
```
Or via `yarn`:
```sh
yarn add @eth-optimism/smock
```
## Note on Using `smoddit`
`smoddit` requires access to the internal storage layout of your smart contracts. The Solidity compiler exposes this via the `storageLayout` flag, which you need to enable at your hardhat config.
Here's an example `hardhat.config.ts` that shows how to import the plugin:
```typescript
// hardhat.config.ts
import { HardhatUserConfig } from 'hardhat/config'
const config: HardhatUserConfig = {
...,
solidity: {
version: '0.7.0',
settings: {
outputSelection: {
"*": {
"*": ["storageLayout"],
},
},
}
},
}
export default config
```
## Table of Contents
- [API](#api)
* [Functions](#functions)
+ [`smockit`](#-smockit-)
- [Import](#import)
- [Signature](#signature)
+ [`smoddit`](#-smoddit-)
- [Import](#import-1)
- [Signature](#signature-1)
* [Types](#types)
+ [`smockit`](#-smockit--1)
- [`MockContract`](#-mockcontract-)
- [`MockContractFunction`](#-mockcontractfunction-)
- [`MockReturnValue`](#-mockreturnvalue-)
+ [`smoddit`](#-smoddit--1)
- [`ModifiableContractFactory`](#-modifiablecontractfactory-)
- [`ModifiableContract`](#-modifiablecontract-)
- [Examples (smockit)](#examples--smockit-)
* [Via `ethers.Contract`](#via--etherscontract-)
* [Asserting Call Count](#asserting-call-count)
* [Asserting Call Data](#asserting-call-data)
* [Returning (w/o Data)](#returning--w-o-data-)
* [Returning a Struct](#returning-a-struct)
* [Returning a Function](#returning-a-function)
* [Returning a Function (w/ Arguments)](#returning-a-function--w--arguments-)
* [Reverting (w/o Data)](#reverting--w-o-data-)
* [Reverting (w/ Data)](#reverting--w--data-)
- [Examples (smoddit)](#examples--smoddit-)
* [Creating a Modifiable Contract](#creating-a-modifiable-contract)
* [Modifying a `uint256`](#modifying-a--uint256-)
* [Modifying a Struct](#modifying-a-struct)
* [Modifying a Mapping](#modifying-a-mapping)
* [Modifying a Nested Mapping](#modifying-a-nested-mapping)
## API
### Functions
#### `smockit`
##### Import
```typescript
import { smockit } from '@eth-optimism/smock'
```
##### Signature
```typescript
const smockit = async (
spec: ContractInterface | Contract | ContractFactory,
opts: {
provider?: any,
address?: string,
},
): Promise<MockContract>
```
#### `smoddit`
##### Import
```typescript
import { smoddit } from '@eth-optimism/smock'
```
##### Signature
```typescript
const smoddit = async (
name: string,
signer?: any
): Promise<ModifiableContractFactory>
```
### Types
#### `smockit`
##### `MockContract`
```typescript
interface MockContract extends Contract {
smocked: {
[functionName: string]: MockContractFunction
}
}
```
##### `MockContractFunction`
```typescript
interface MockContractFunction {
calls: string[]
will: {
return: {
(): void
with: (returnValue?: MockReturnValue) => void
}
revert: {
(): void
with: (revertValue?: string) => void
}
resolve: 'return' | 'revert'
}
}
```
##### `MockReturnValue`
```typescript
export type MockReturnValue =
| string
| Object
| any[]
| ((...params: any[]) => MockReturnValue)
```
#### `smoddit`
##### `ModifiableContractFactory`
```typescript
interface ModifiableContractFactory extends ContractFactory {
deploy: (...args: any[]) => Promise<ModifiableContract>
}
```
##### `ModifiableContract`
```typescript
interface ModifiableContract extends Contract {
smodify: {
put: (storage: any) => void
set: (storage: any) => void
reset: () => void
}
}
```
## Examples (smockit)
### Via `ethers.Contract`
```typescript
import { ethers } from 'hardhat'
import { smockit } from '@eth-optimism/smock'
const MyContractFactory = await ethers.getContractFactory('MyContract')
const MyContract = await MyContractFactory.deploy(...)
// Smockit!
const MyMockContract = await smockit(MyContract)
MyMockContract.smocked.myFunction.will.return.with('Some return value!')
console.log(await MyMockContract.myFunction()) // 'Some return value!'
```
### Asserting Call Count
```typescript
import { ethers } from 'hardhat'
import { smockit } from '@eth-optimism/smock'
const MyContractFactory = await ethers.getContractFactory('MyContract')
const MyContract = await MyContractFactory.deploy(...)
const MyOtherContractFactory = await ethers.getContractFactory('MyOtherContract')
const MyOtherContract = await MyOtherContract.deploy(...)
// Smockit!
const MyMockContract = await smockit(MyContract)
MyMockContract.smocked.myFunction.will.return.with('Some return value!')
// Assuming that MyOtherContract.myOtherFunction calls MyContract.myFunction.
await MyOtherContract.myOtherFunction()
console.log(MyMockContract.smocked.myFunction.calls.length) // 1
```
### Asserting Call Data
```typescript
import { ethers } from 'hardhat'
import { smockit } from '@eth-optimism/smock'
const MyContractFactory = await ethers.getContractFactory('MyContract')
const MyContract = await MyContractFactory.deploy(...)
const MyOtherContractFactory = await ethers.getContractFactory('MyOtherContract')
const MyOtherContract = await MyOtherContract.deploy(...)
// Smockit!
const MyMockContract = await smockit(MyContract)
MyMockContract.smocked.myFunction.will.return.with('Some return value!')
// Assuming that MyOtherContract.myOtherFunction calls MyContract.myFunction with 'Hello World!'.
await MyOtherContract.myOtherFunction()
console.log(MyMockContract.smocked.myFunction.calls[0]) // 'Hello World!'
```
### Returning (w/o Data)
```typescript
import { ethers } from 'hardhat'
import { smockit } from '@eth-optimism/smock'
const MyContractFactory = await ethers.getContractFactory('MyContract')
const MyContract = await MyContractFactory.deploy(...)
// Smockit!
const MyMockContract = await smockit(MyContract)
MyMockContract.smocked.myFunction.will.return()
console.log(await MyMockContract.myFunction()) // []
```
### Returning a Struct
```typescript
import { ethers } from 'hardhat'
import { smockit } from '@eth-optimism/smock'
const MyContractFactory = await ethers.getContractFactory('MyContract')
const MyContract = await MyContractFactory.deploy(...)
// Smockit!
const MyMockContract = await smockit(MyContract)
MyMockContract.smocked.myFunction.will.return.with({
valueA: 'Some value',
valueB: 1234,
valueC: true
})
console.log(await MyMockContract.myFunction()) // ['Some value', 1234, true]
```
### Returning a Function
```typescript
import { ethers } from 'hardhat'
import { smockit } from '@eth-optimism/smock'
const MyContractFactory = await ethers.getContractFactory('MyContract')
const MyContract = await MyContractFactory.deploy(...)
// Smockit!
const MyMockContract = await smockit(MyContract)
MyMockContract.smocked.myFunction.will.return.with(() => {
return 'Some return value!'
})
console.log(await MyMockContract.myFunction()) // ['Some return value!']
```
### Returning a Function (w/ Arguments)
```typescript
import { ethers } from 'hardhat'
import { smockit } from '@eth-optimism/smock'
const MyContractFactory = await ethers.getContractFactory('MyContract')
const MyContract = await MyContractFactory.deploy(...)
// Smockit!
const MyMockContract = await smockit(MyContract)
MyMockContract.smocked.myFunction.will.return.with((myFunctionArgument: string) => {
return myFunctionArgument
})
console.log(await MyMockContract.myFunction('Some return value!')) // ['Some return value!']
```
### Reverting (w/o Data)
```typescript
import { ethers } from 'hardhat'
import { smockit } from '@eth-optimism/smock'
const MyContractFactory = await ethers.getContractFactory('MyContract')
const MyContract = await MyContractFactory.deploy(...)
// Smockit!
const MyMockContract = await smockit(MyContract)
MyMockContract.smocked.myFunction.will.revert()
console.log(await MyMockContract.myFunction()) // Revert!
```
### Reverting (w/ Data)
```typescript
import { ethers } from 'hardhat'
import { smockit } from '@eth-optimism/smock'
const MyContractFactory = await ethers.getContractFactory('MyContract')
const MyContract = await MyContractFactory.deploy(...)
// Smockit!
const MyMockContract = await smockit(MyContract)
MyMockContract.smocked.myFunction.will.revert.with('0x1234')
console.log(await MyMockContract.myFunction('Some return value!')) // Revert!
```
## Examples (smoddit)
### Creating a Modifiable Contract
```typescript
import { ethers } from 'hardhat'
import { smoddit } from '@eth-optimism/smock'
// Smoddit!
const MyModifiableContractFactory = await smoddit('MyContract')
const MyModifiableContract = await MyModifiableContractFactory.deploy(...)
```
### Modifying a `uint256`
```typescript
import { ethers } from 'hardhat'
import { smoddit } from '@eth-optimism/smock'
// Smoddit!
const MyModifiableContractFactory = await smoddit('MyContract')
const MyModifiableContract = await MyModifiableContractFactory.deploy(...)
MyModifiableContract.smodify.put({
myInternalUint256: 1234
})
console.log(await MyMockContract.getMyInternalUint256()) // 1234
```
### Modifying a Struct
```typescript
import { ethers } from 'hardhat'
import { smoddit } from '@eth-optimism/smock'
// Smoddit!
const MyModifiableContractFactory = await smoddit('MyContract')
const MyModifiableContract = await MyModifiableContractFactory.deploy(...)
MyModifiableContract.smodify.put({
myInternalStruct: {
valueA: 1234,
valueB: true
}
})
console.log(await MyMockContract.getMyInternalStruct()) // { valueA: 1234, valueB: true }
```
### Modifying a Mapping
```typescript
import { ethers } from 'hardhat'
import { smoddit } from '@eth-optimism/smock'
// Smoddit!
const MyModifiableContractFactory = await smoddit('MyContract')
const MyModifiableContract = await MyModifiableContractFactory.deploy(...)
MyModifiableContract.smodify.put({
myInternalMapping: {
1234: 5678
}
})
console.log(await MyMockContract.getMyInternalMappingValue(1234)) // 5678
```
### Modifying a Nested Mapping
```typescript
import { ethers } from 'hardhat'
import { smoddit } from '@eth-optimism/smock'
// Smoddit!
const MyModifiableContractFactory = await smoddit('MyContract')
const MyModifiableContract = await MyModifiableContractFactory.deploy(...)
MyModifiableContract.smodify.put({
myInternalNestedMapping: {
1234: {
4321: 5678
}
}
})
console.log(await MyMockContract.getMyInternalNestedMappingValue(1234, 4321)) // 5678
```
import { HardhatUserConfig } from 'hardhat/config'
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-waffle'
const config: HardhatUserConfig = {
paths: {
sources: './test/contracts',
},
solidity: {
version: '0.7.6',
settings: {
outputSelection: {
'*': {
'*': ['storageLayout'],
},
},
},
},
}
export default config
{
"name": "@eth-optimism/smock",
"version": "1.0.0-alpha.3",
"main": "dist/index",
"types": "dist/index",
"files": [
"dist"
],
"license": "MIT",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "hardhat test --show-stack-traces",
"lint": "yarn lint:fix && yarn lint:check",
"lint:check": "tslint --format stylish --project .",
"lint:fix": "prettier --config ./prettier-config.json --write \"hardhat.config.ts\" \"{src,test}/**/*.ts\"",
"clean": "rimraf ./artifacts ./cache ./dist"
},
"peerDependencies": {
"@nomiclabs/ethereumjs-vm": "^4",
"@nomiclabs/hardhat-ethers": "^2",
"ethers": "^5",
"hardhat": "^2"
},
"dependencies": {
"@eth-optimism/core-utils": "0.1.10",
"@ethersproject/abi": "^5.0.13",
"@ethersproject/abstract-provider": "^5.0.10",
"bn.js": "^5.2.0"
},
"devDependencies": {
"@nomiclabs/ethereumjs-vm": "4.2.2",
"@eth-optimism/dev": "^1.1.1",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@types/lodash": "^4.14.161",
"chai": "^4.3.0",
"ethereum-waffle": "^3.3.0",
"ethers": "^5.0.32",
"glob": "^7.1.6",
"hardhat": "^2.1.1",
"lodash": "^4.17.20",
"prettier": "^2.2.1"
}
}
../../prettier-config.json
\ No newline at end of file
/* Imports: External */
import { HardhatRuntimeEnvironment } from 'hardhat/types'
import { HardhatNetworkProvider } from 'hardhat/internal/hardhat-network/provider/provider'
/**
* Finds the "base" Ethereum provider of the current hardhat environment.
*
* Basically, hardhat uses a system of nested providers where each provider wraps the next and
* "provides" some extra features. When you're running on top of the "hardhat evm" the bottom of
* this series of providers is the "HardhatNetworkProvider":
* https://github.com/nomiclabs/hardhat/blob/master/packages/hardhat-core/src/internal/hardhat-network/provider/provider.ts
* This object has direct access to the node (provider._node), which in turn has direct access to
* the ethereumjs-vm instance (provider._node._vm). So it's quite useful to be able to find this
* object reliably!
* @param hre hardhat runtime environment to pull the base provider from.
* @return base hardhat network provider
*/
export const findBaseHardhatProvider = (
runtime: HardhatRuntimeEnvironment
): HardhatNetworkProvider => {
// This function is pretty approximate. Haven't spent enough time figuring out if there's a more
// reliable way to get the base provider. I can imagine a future in which there's some circular
// references and this function ends up looping. So I'll just preempt this by capping the maximum
// search depth.
const maxLoopIterations = 1024
let currentLoopIterations = 0
// Search by looking for the internal "_wrapped" variable. Base provider doesn't have this
// property (at least for now!).
let provider = runtime.network.provider
while ((provider as any)._wrapped !== undefined) {
provider = (provider as any)._wrapped
// Just throw if we ever end up in (what seems to be) an infinite loop.
currentLoopIterations += 1
if (currentLoopIterations > maxLoopIterations) {
throw new Error(
`[smock]: unable to find base hardhat provider. are you sure you're running locally?`
)
}
}
// TODO: Figure out a reliable way to do a type check here. Source for inspiration:
// https://github.com/nomiclabs/hardhat/blob/master/packages/hardhat-core/src/internal/hardhat-network/provider/provider.ts
return provider as any
}
export * from './hardhat-common'
export * from './smockit'
export * from './smoddit'
/* Imports: External */
import { TransactionExecutionError } from 'hardhat/internal/hardhat-network/provider/errors'
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 { toHexString, fromHexString } from '@eth-optimism/core-utils'
import BN from 'bn.js'
/* Imports: Internal */
import { MockContract, SmockedVM } from './types'
/**
* 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
}
const target = toHexString(message.to).toLowerCase()
// 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 = toHexString(result.createdAddress).toLowerCase()
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()
const target = toHexString(message.to).toLowerCase()
// 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
// 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(
fromHexString(mock.address),
Buffer.from('00', 'hex')
)
}
export * from './smockit'
export * from './types'
/* Imports: External */
import hre from 'hardhat'
import { Contract, ContractFactory, ethers } from 'ethers'
import { toHexString, fromHexString } from '@eth-optimism/core-utils'
/* Imports: Internal */
import {
isArtifact,
MockContract,
MockContractFunction,
MockReturnValue,
SmockedVM,
SmockOptions,
SmockSpec,
} from './types'
import { bindSmock } from './binding'
import { makeRandomAddress } from '../utils'
import { findBaseHardhatProvider } from '../common'
/**
* Generates an ethers Interface instance when given a smock spec. Meant for standardizing the
* various input types we might reasonably want to support.
* @param spec Smock specification object. Thing you want to base the interface on.
* @param hre Hardhat runtime environment. Used so we can
* @return Interface generated from the spec.
*/
const makeContractInterfaceFromSpec = async (
spec: SmockSpec
): Promise<ethers.utils.Interface> => {
if (spec instanceof Contract) {
return spec.interface
} else if (spec instanceof ContractFactory) {
return spec.interface
} else if (spec instanceof ethers.utils.Interface) {
return spec
} else if (isArtifact(spec)) {
return new ethers.utils.Interface(spec.abi)
} else if (typeof spec === 'string') {
try {
return new ethers.utils.Interface(spec)
} catch (err) {
return (await hre.ethers.getContractFactory(spec)).interface
}
} else {
return new ethers.utils.Interface(spec)
}
}
/**
* Creates a mock contract function from a real contract function.
* @param contract Contract object to make a mock function for.
* @param functionName Name of the function to mock.
* @param vm Virtual machine reference, necessary for call assertions to work.
* @return Mock contract function.
*/
const smockifyFunction = (
contract: Contract,
functionName: string,
vm: SmockedVM
): MockContractFunction => {
return {
reset: () => {
return
},
get calls() {
return vm._smockState.calls[contract.address.toLowerCase()]
.map((calldataBuf: Buffer) => {
const sighash = toHexString(calldataBuf.slice(0, 4))
const fragment = contract.interface.getFunction(sighash)
let data: any = toHexString(calldataBuf)
try {
data = contract.interface.decodeFunctionData(fragment.name, data)
} catch (e) {
console.error(e)
}
return {
functionName: fragment.name,
data,
}
})
.filter((functionResult: any) => {
return functionResult.functionName === functionName
})
.map((functionResult: any) => {
return functionResult.data
})
},
will: {
get return() {
const fn: any = () => {
this.resolve = 'return'
this.returnValue = undefined
}
fn.with = (returnValue?: MockReturnValue): void => {
this.resolve = 'return'
this.returnValue = returnValue
}
return fn
},
get revert() {
const fn: any = () => {
this.resolve = 'revert'
this.returnValue = undefined
}
fn.with = (revertValue?: string): void => {
this.resolve = 'revert'
this.returnValue = revertValue
}
return fn
},
resolve: 'return',
},
}
}
/**
* Turns a specification into a mock contract.
* @param spec Smock contract specification.
* @param opts Optional additional settings.
*/
export const smockit = async (
spec: SmockSpec,
opts: SmockOptions = {}
): Promise<MockContract> => {
// Only support native hardhat runtime, haven't bothered to figure it out for anything else.
if (hre.network.name !== 'hardhat') {
throw new Error(
`[smock]: smock is only compatible with the "hardhat" network, got: ${hre.network.name}`
)
}
// Find the provider object. See comments for `findBaseHardhatProvider`
const provider = findBaseHardhatProvider(hre)
// Sometimes the VM hasn't been initialized by the time we get here, depending on what the user
// is doing with hardhat (e.g., sending a transaction before calling this function will
// initialize the vm). Initialize it here if it hasn't been already.
if ((provider as any)._node === undefined) {
await (provider as any)._init()
}
// Generate the contract object that we're going to attach our fancy functions to. Doing it this
// way is nice because it "feels" more like a contract (as long as you're using ethers).
const contract = new ethers.Contract(
opts.address || makeRandomAddress(),
await makeContractInterfaceFromSpec(spec),
opts.provider || hre.ethers.provider // TODO: Probably check that this exists.
) as MockContract
// Start by smocking the fallback.
contract.smocked = {
fallback: smockifyFunction(
contract,
'fallback',
(provider as any)._node._vm
),
}
// Smock the rest of the contract functions.
for (const functionName of Object.keys(contract.functions)) {
contract.smocked[functionName] = smockifyFunction(
contract,
functionName,
(provider as any)._node._vm
)
}
// TODO: Make this less of a hack.
;(contract as any)._smockit = async function (
data: Buffer
): Promise<{
resolve: 'return' | 'revert'
functionName: string
rawReturnValue: any
returnValue: Buffer
gasUsed: number
}> {
let fn: any
try {
const sighash = toHexString(data.slice(0, 4))
fn = this.interface.getFunction(sighash)
} catch (err) {
fn = null
}
let params: any
let mockFn: any
if (fn !== null) {
params = this.interface.decodeFunctionData(fn, toHexString(data))
mockFn = this.smocked[fn.name]
} else {
params = toHexString(data)
mockFn = this.smocked.fallback
}
const rawReturnValue =
mockFn.will?.returnValue instanceof Function
? await mockFn.will.returnValue(...params)
: mockFn.will.returnValue
let encodedReturnValue: string = '0x'
if (rawReturnValue !== undefined) {
if (mockFn.will?.resolve === 'revert') {
if (typeof rawReturnValue !== 'string') {
throw new Error(
`Smock: Tried to revert with a non-string (or non-bytes) type: ${typeof rawReturnValue}`
)
}
if (rawReturnValue.startsWith('0x')) {
encodedReturnValue = rawReturnValue
} else {
const errorface = new ethers.utils.Interface([
{
inputs: [
{
name: '_reason',
type: 'string',
},
],
name: 'Error',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
])
encodedReturnValue = errorface.encodeFunctionData('Error', [
rawReturnValue,
])
}
} else {
if (fn === null) {
encodedReturnValue = rawReturnValue
} else {
try {
encodedReturnValue = this.interface.encodeFunctionResult(fn, [
rawReturnValue,
])
} catch (err) {
if (err.code === 'INVALID_ARGUMENT') {
try {
encodedReturnValue = this.interface.encodeFunctionResult(
fn,
rawReturnValue
)
} catch {
if (typeof rawReturnValue !== 'string') {
throw new Error(
`Could not properly encode mock return value for ${fn.name}`
)
}
encodedReturnValue = rawReturnValue
}
} else {
throw err
}
}
}
}
} else {
if (fn === null) {
encodedReturnValue = '0x'
} else {
encodedReturnValue = '0x' + '00'.repeat(2048)
}
}
return {
resolve: mockFn.will?.resolve,
functionName: fn ? fn.name : null,
rawReturnValue,
returnValue: fromHexString(encodedReturnValue),
gasUsed: mockFn.gasUsed || 0,
}
}
await bindSmock(contract, provider)
return contract
}
/* Imports: External */
import { Artifact } from 'hardhat/types'
import { Contract, ContractFactory, ethers } from 'ethers'
import { Provider } from '@ethersproject/abstract-provider'
import { JsonFragment, Fragment } from '@ethersproject/abi'
export type SmockSpec =
| Artifact
| Contract
| ContractFactory
| ethers.utils.Interface
| string
| (JsonFragment | Fragment | string)[]
export interface SmockOptions {
provider?: Provider
address?: string
}
export type MockReturnValue =
| string
| Object
| any[]
| ((...params: any[]) => MockReturnValue)
export interface MockContractFunction {
calls: any[]
reset: () => void
will: {
return: {
(): void
with: (returnValue?: MockReturnValue) => void
}
revert: {
(): void
with: (
revertValue?: string | (() => string) | (() => Promise<string>)
) => void
}
resolve: 'return' | 'revert'
}
}
export type MockContract = Contract & {
smocked: {
[name: string]: MockContractFunction
}
}
export interface SmockedVM {
_smockState: {
mocks: {
[address: string]: MockContract
}
calls: {
[address: string]: any[]
}
messages: any[]
}
on: (event: string, callback: Function) => void
pStateManager: {
putContractCode: (address: Buffer, code: Buffer) => Promise<void>
}
}
const isMockFunction = (obj: any): obj is MockContractFunction => {
return (
obj &&
obj.will &&
obj.will.return &&
obj.will.return.with &&
obj.will.revert &&
obj.will.revert.with
// TODO: obj.will.emit
)
}
export const isMockContract = (obj: any): obj is MockContract => {
return (
obj &&
obj.smocked &&
obj.smocked.fallback &&
Object.values(obj.smocked).every((smockFunction: any) => {
return isMockFunction(smockFunction)
})
)
}
export const isArtifact = (obj: any): obj is Artifact => {
return (
obj &&
typeof obj._format === 'string' &&
typeof obj.contractName === 'string' &&
typeof obj.sourceName === 'string' &&
Array.isArray(obj.abi) &&
typeof obj.bytecode === 'string' &&
typeof obj.deployedBytecode === 'string' &&
obj.linkReferences &&
obj.deployedLinkReferences
)
}
/* External Imports */
import { toHexString, fromHexString } from '@eth-optimism/core-utils'
import { HardhatNetworkProvider } from 'hardhat/internal/hardhat-network/provider/provider'
/* Internal Imports */
import { ModifiableContract } from './types'
/**
* Checks to see if smoddit has been initialized already.
* @param provider Base hardhat network provider to check.
* @return Whether or not the provider has already been modified to support smoddit.
*/
const isSmodInitialized = (provider: HardhatNetworkProvider): boolean => {
return (provider as any)._node._vm._smod !== undefined
}
/**
* Initializes smodding functionality.
* @param provider Base hardhat network provider to modify.
*/
const initializeSmod = (provider: HardhatNetworkProvider): void => {
if (isSmodInitialized(provider)) {
return
}
// Will need to reference these things.
const node = (provider as any)._node
const vm = node._vm
const pStateManager = vm.pStateManager
vm._smod = {
contracts: {},
}
const originalGetStorageFn = pStateManager.getContractStorage.bind(
pStateManager
)
pStateManager.getContractStorage = async (
addressBuf: Buffer,
keyBuf: Buffer
): Promise<Buffer> => {
const originalReturnValue = await originalGetStorageFn(addressBuf, keyBuf)
const address = toHexString(addressBuf).toLowerCase()
const key = toHexString(keyBuf).toLowerCase()
if (!(address in vm._smod.contracts)) {
return originalReturnValue
}
const contract: ModifiableContract = vm._smod.contracts[address]
if (!(key in contract._smodded)) {
return originalReturnValue
}
return fromHexString(contract._smodded[key])
}
const originalPutStorageFn = pStateManager.putContractStorage.bind(
pStateManager
)
pStateManager.putContractStorage = async (
addressBuf: Buffer,
keyBuf: Buffer,
valBuf: Buffer
): Promise<void> => {
await originalPutStorageFn(addressBuf, keyBuf, valBuf)
const address = toHexString(addressBuf).toLowerCase()
const key = toHexString(keyBuf).toLowerCase()
if (!(address in vm._smod.contracts)) {
return
}
const contract: ModifiableContract = vm._smod.contracts[address]
if (!(key in contract._smodded)) {
return
}
delete contract._smodded[key]
}
}
/**
* Binds the smodded contract to the VM.
* @param contract Contract to bind.
*/
export const bindSmod = (
contract: ModifiableContract,
provider: HardhatNetworkProvider
): void => {
if (!isSmodInitialized(provider)) {
initializeSmod(provider)
}
const vm = (provider as any)._node._vm
// Add mock to our list of mocks currently attached to the VM.
vm._smod.contracts[contract.address.toLowerCase()] = contract
}
export * from './smoddit'
export * from './types'
/* External Imports */
import hre from 'hardhat'
import { fromHexString } from '@eth-optimism/core-utils'
/* Internal Imports */
import { ModifiableContract, ModifiableContractFactory, Smodify } from './types'
import { getStorageLayout, getStorageSlots } from './storage'
import { bindSmod } from './binding'
import { toHexString32 } from '../utils'
import { findBaseHardhatProvider } from '../common'
/**
* Creates a modifiable contract factory.
* @param name Name of the contract to smoddify.
* @param signer Optional signer to attach to the factory.
* @returns Smoddified contract factory.
*/
export const smoddit = async (
name: string,
signer?: any
): Promise<ModifiableContractFactory> => {
// Find the provider object. See comments for `findBaseHardhatProvider`
const provider = findBaseHardhatProvider(hre)
// Sometimes the VM hasn't been initialized by the time we get here, depending on what the user
// is doing with hardhat (e.g., sending a transaction before calling this function will
// initialize the vm). Initialize it here if it hasn't been already.
if ((provider as any)._node === undefined) {
await (provider as any)._init()
}
// Pull out a reference to the vm's state manager.
const pStateManager = (provider as any)._node._vm.pStateManager
const layout = await getStorageLayout(name)
const factory = (await hre.ethers.getContractFactory(
name,
signer
)) as ModifiableContractFactory
const originalDeployFn = factory.deploy.bind(factory)
factory.deploy = async (...args: any[]): Promise<ModifiableContract> => {
const contract: ModifiableContract = await originalDeployFn(...args)
contract._smodded = {}
const put = (storage: any) => {
if (!storage) {
return
}
const slots = getStorageSlots(layout, storage)
for (const slot of slots) {
contract._smodded[slot.hash.toLowerCase()] = slot.value
}
}
const reset = () => {
contract._smodded = {}
}
const set = (storage: any) => {
contract.smodify.reset()
contract.smodify.put(storage)
}
const check = async (storage: any) => {
if (!storage) {
return true
}
const slots = getStorageSlots(layout, storage)
return slots.every(async (slot) => {
return (
toHexString32(
await pStateManager.getContractStorage(
fromHexString(contract.address),
fromHexString(slot.hash.toLowerCase())
)
) === slot.value
)
})
}
contract.smodify = {
put,
reset,
set,
check,
}
bindSmod(contract, provider)
return contract
}
return factory
}
/* External Imports */
import hre from 'hardhat'
import { Artifacts } from 'hardhat/internal/artifacts'
import { ethers } from 'ethers'
import { remove0x } from '@eth-optimism/core-utils'
import _ from 'lodash'
/* Internal Imports */
import { toHexString32 } from '../utils'
interface InputSlot {
label: string
slot: number
}
interface StorageSlot {
label: string
hash: string
value: string
}
/**
* Reads the storage layout of a contract.
* @param name Name of the contract to get a storage layout for.
* @return Storage layout for the given contract name.
*/
export const getStorageLayout = async (name: string): Promise<any> => {
const artifacts = new Artifacts(hre.config.paths.artifacts)
const { sourceName, contractName } = artifacts.readArtifactSync(name)
const buildInfo = await hre.artifacts.getBuildInfo(
`${sourceName}:${contractName}`
)
const output = buildInfo.output.contracts[sourceName][contractName]
if (!('storageLayout' in output)) {
throw new Error(
`Storage layout for ${name} not found. Did you forget to set the storage layout compiler option in your hardhat config? Read more: https://github.com/ethereum-optimism/smock#note-on-using-smoddit`
)
}
return (output as any).storageLayout
}
/**
* Converts storage into a list of storage slots.
* @param storageLayout Contract storage layout.
* @param obj Storage object to convert.
* @returns List of storage slots.
*/
export const getStorageSlots = (
storageLayout: any,
obj: any
): StorageSlot[] => {
const slots: StorageSlot[] = []
const flat = flattenObject(obj)
for (const key of Object.keys(flat)) {
const path = key.split('.')
const variableLabel = path[0]
const variableDef = storageLayout.storage.find((vDef: any) => {
return vDef.label === variableLabel
})
if (!variableDef) {
throw new Error(
`Could not find a matching variable definition for ${variableLabel}`
)
}
const baseSlot = parseInt(variableDef.slot, 10)
const baseDepth = (variableDef.type.match(/t_mapping/g) || []).length
const slotLabel =
path.length > 1 + baseDepth ? path[path.length - 1] : 'default'
const inputSlot = getInputSlots(storageLayout, variableDef.type).find(
(iSlot) => {
return iSlot.label === slotLabel
}
)
if (!inputSlot) {
throw new Error(
`Could not find a matching slot definition for ${slotLabel}`
)
}
let slotHash = toHexString32(baseSlot)
for (let i = 0; i < baseDepth; i++) {
slotHash = ethers.utils.keccak256(
toHexString32(path[i + 1]) + remove0x(slotHash)
)
}
slotHash = toHexString32(
ethers.BigNumber.from(slotHash).add(inputSlot.slot)
)
slots.push({
label: key,
hash: slotHash,
value: toHexString32(flat[key]),
})
}
return slots
}
/**
* Flattens an object.
* @param obj Object to flatten.
* @param prefix Current object prefix (used recursively).
* @param res Current result (used recursively).
* @returns Flattened object.
*/
const flattenObject = (
obj: any,
prefix: string = '',
res: any = {}
): Object => {
if (ethers.BigNumber.isBigNumber(obj)) {
res[prefix] = obj.toNumber()
return res
} else if (_.isString(obj) || _.isNumber(obj) || _.isBoolean(obj)) {
res[prefix] = obj
return res
} else if (_.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
const pre = _.isEmpty(prefix) ? `${i}` : `${prefix}.${i}`
flattenObject(obj[i], pre, res)
}
return res
} else if (_.isPlainObject(obj)) {
for (const key of Object.keys(obj)) {
const pre = _.isEmpty(prefix) ? key : `${prefix}.${key}`
flattenObject(obj[key], pre, res)
}
return res
} else {
throw new Error('Cannot flatten unsupported object type.')
}
}
/**
* Gets the slot positions for a provided variable type.
* @param storageLayout Contract's storage layout.
* @param inputTypeName Variable type name.
* @returns Slot positions.
*/
const getInputSlots = (
storageLayout: any,
inputTypeName: string
): InputSlot[] => {
const inputType = storageLayout.types[inputTypeName]
if (inputType.encoding === 'mapping') {
return getInputSlots(storageLayout, inputType.value)
} else if (inputType.encoding === 'inplace') {
if (inputType.members) {
return inputType.members.map((member: any) => {
return {
label: member.label,
slot: member.slot,
}
})
} else {
return [
{
label: 'default',
slot: 0,
},
]
}
} else {
throw new Error(`Encoding type not supported: ${inputType.encoding}`)
}
}
/* External Imports */
import { Contract, ContractFactory } from 'ethers'
export interface Smodify {
put: (storage: any) => void
set: (storage: any) => void
check: (storage: any) => Promise<boolean>
reset: () => void
}
export interface Smodded {
[hash: string]: string
}
export interface ModifiableContract extends Contract {
smodify: Smodify
_smodded: Smodded
}
export interface ModifiableContractFactory extends ContractFactory {
deploy: (...args: any[]) => Promise<ModifiableContract>
}
import { ethers } from 'ethers'
export const makeRandomAddress = (): string => {
return ethers.utils.getAddress(
'0x' +
[...Array(40)]
.map(() => {
return Math.floor(Math.random() * 16).toString(16)
})
.join('')
)
}
/* External Imports */
import { BigNumber } from 'ethers'
import { remove0x } from '@eth-optimism/core-utils'
export const toHexString32 = (
value: string | number | BigNumber | boolean
): string => {
if (typeof value === 'string' && value.startsWith('0x')) {
return '0x' + remove0x(value).padStart(64, '0').toLowerCase()
} else if (typeof value === 'boolean') {
return toHexString32(value ? 1 : 0)
} else {
return toHexString32(BigNumber.from(value).toHexString())
}
}
export * from './hex-utils'
export * from './address-utils'
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
contract SimpleStorageGetter {
struct SimpleStruct {
uint256 valueA;
bool valueB;
}
uint256 internal _constructorUint256;
uint256 internal _uint256;
bool internal _bool;
SimpleStruct internal _SimpleStruct;
mapping (uint256 => uint256) _uint256Map;
mapping (uint256 => mapping (uint256 => uint256)) _uint256NestedMap;
constructor(
uint256 _inA
) {
_constructorUint256 = _inA;
}
function getConstructorUint256()
public
view
returns (
uint256 _out
)
{
return _constructorUint256;
}
function getUint256()
public
view
returns (
uint256 _out
)
{
return _uint256;
}
function setUint256(
uint256 _in
)
public
{
_uint256 = _in;
}
function getBool()
public
view
returns (
bool _out
)
{
return _bool;
}
function getSimpleStruct()
public
view
returns (
SimpleStruct memory _out
)
{
return _SimpleStruct;
}
function getUint256MapValue(
uint256 _key
)
public
view
returns (
uint256 _out
)
{
return _uint256Map[_key];
}
function getNestedUint256MapValue(
uint256 _keyA,
uint256 _keyB
)
public
view
returns (
uint256 _out
)
{
return _uint256NestedMap[_keyA][_keyB];
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
contract TestHelpers_BasicReturnContract {
fallback()
external
{}
function empty()
public
{}
function getBoolean()
public
returns (
bool _out1
)
{}
function getUint256()
public
returns (
uint256 _out1
)
{}
function getBytes32()
public
returns (
bytes32 _out1
)
{}
function getBytes()
public
returns (
bytes memory _out1
)
{}
function getString()
public
returns (
string memory _out1
)
{}
function getInputtedBoolean(
bool _in1
)
public
returns (
bool _out1
)
{}
function getInputtedUint256(
uint256 _in1
)
public
returns (
uint256 _out1
)
{}
function getInputtedBytes32(
bytes32 _in1
)
public
returns (
bytes32 _out1
)
{}
struct StructFixedSize {
bool valBoolean;
uint256 valUint256;
bytes32 valBytes32;
}
function getStructFixedSize()
public
returns (
StructFixedSize memory _out1
)
{}
struct StructDynamicSize {
bytes valBytes;
string valString;
}
function getStructDynamicSize()
public
returns (
StructDynamicSize memory _out1
)
{}
struct StructMixedSize {
bool valBoolean;
uint256 valUint256;
bytes32 valBytes32;
bytes valBytes;
string valString;
}
function getStructMixedSize()
public
returns (
StructMixedSize memory _out1
)
{}
struct StructNested {
StructFixedSize valStructFixedSize;
StructDynamicSize valStructDynamicSize;
}
function getStructNested()
public
returns (
StructNested memory _out1
)
{}
function getArrayUint256()
public
returns (
uint256[] memory _out
)
{}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract TestHelpers_EmptyContract {}
/* Imports: External */
import { ethers } from 'hardhat'
import { expect } from 'chai'
import { toPlainObject } from 'lodash'
import { BigNumber } from 'ethers'
/* Imports: Internal */
import { MockContract, smockit } from '../../src'
describe('[smock]: function manipulation tests', () => {
let mock: MockContract
beforeEach(async () => {
mock = await smockit('TestHelpers_BasicReturnContract')
})
describe('manipulating fallback functions', () => {
it('should return with no data by default', async () => {
const expected = '0x'
expect(
await ethers.provider.call({
to: mock.address,
})
).to.equal(expected)
})
it('should be able to make a fallback function return without any data', async () => {
const expected = '0x'
mock.smocked.fallback.will.return()
expect(
await ethers.provider.call({
to: mock.address,
})
).to.equal(expected)
})
it('should be able to make a fallback function return with data', async () => {
const expected = '0x1234123412341234'
mock.smocked.fallback.will.return.with(expected)
expect(
await ethers.provider.call({
to: mock.address,
})
).to.equal(expected)
})
it('should be able to make a fallback function revert without any data', async () => {
mock.smocked.fallback.will.revert()
await expect(
ethers.provider.call({
to: mock.address,
})
).to.be.reverted
})
it('should be able to make a fallback function revert with a string', async () => {
const expected = 'this is a revert message'
mock.smocked.fallback.will.revert.with(expected)
await expect(
ethers.provider.call({
to: mock.address,
})
).to.be.revertedWith(expected)
})
it('should be able to make a fallback function emit an event', async () => {
// TODO
})
it('should be able to change behaviors', async () => {
mock.smocked.fallback.will.revert()
await expect(
ethers.provider.call({
to: mock.address,
})
).to.be.reverted
const expected = '0x'
mock.smocked.fallback.will.return()
expect(
await ethers.provider.call({
to: mock.address,
})
).to.equal(expected)
})
describe.skip('resetting the fallback function', () => {
it('should go back to default behavior when reset', async () => {
mock.smocked.fallback.will.revert()
await expect(
ethers.provider.call({
to: mock.address,
})
).to.be.reverted
const expected = '0x'
mock.smocked.fallback.reset()
expect(
await ethers.provider.call({
to: mock.address,
})
).to.equal(expected)
})
})
})
describe('manipulating functions', () => {
it('should be able to make a function return without any data', async () => {
const expected = []
mock.smocked.empty.will.return()
expect(await mock.callStatic.empty()).to.deep.equal(expected)
})
it('should be able to make a function revert without any data', async () => {
mock.smocked.empty.will.revert()
await expect(mock.callStatic.empty()).to.be.reverted
})
it('should be able to make a function emit an event', async () => {
// TODO
})
describe('returning with data', () => {
describe('fixed data types', () => {
describe('default behaviors', () => {
it('should return false for a boolean', async () => {
const expected = false
expect(await mock.callStatic.getBoolean()).to.equal(expected)
})
it('should return zero for a uint256', async () => {
const expected = 0
expect(await mock.callStatic.getUint256()).to.equal(expected)
})
it('should return 32 zero bytes for a bytes32', async () => {
const expected =
'0x0000000000000000000000000000000000000000000000000000000000000000'
expect(await mock.callStatic.getBytes32()).to.equal(expected)
})
})
describe('from a specified value', () => {
it('should be able to return a boolean', async () => {
const expected = true
mock.smocked.getBoolean.will.return.with(expected)
expect(await mock.callStatic.getBoolean()).to.equal(expected)
})
it('should be able to return a uint256', async () => {
const expected = 1234
mock.smocked.getUint256.will.return.with(expected)
expect(await mock.callStatic.getUint256()).to.equal(expected)
})
it('should be able to return a bytes32', async () => {
const expected =
'0x1234123412341234123412341234123412341234123412341234123412341234'
mock.smocked.getBytes32.will.return.with(expected)
expect(await mock.callStatic.getBytes32()).to.equal(expected)
})
})
describe('from a function', () => {
describe('without input arguments', () => {
it('should be able to return a boolean', async () => {
const expected = true
mock.smocked.getBoolean.will.return.with(() => {
return expected
})
expect(await mock.callStatic.getBoolean()).to.equal(expected)
})
it('should be able to return a uint256', async () => {
const expected = 1234
mock.smocked.getUint256.will.return.with(() => {
return expected
})
expect(await mock.callStatic.getUint256()).to.equal(expected)
})
it('should be able to return a bytes32', async () => {
const expected =
'0x1234123412341234123412341234123412341234123412341234123412341234'
mock.smocked.getBytes32.will.return.with(() => {
return expected
})
expect(await mock.callStatic.getBytes32()).to.equal(expected)
})
})
describe('with input arguments', () => {
it('should be able to return a boolean', async () => {
const expected = true
mock.smocked.getInputtedBoolean.will.return.with(
(arg1: boolean) => {
return arg1
}
)
expect(
await mock.callStatic.getInputtedBoolean(expected)
).to.equal(expected)
})
it('should be able to return a uint256', async () => {
const expected = 1234
mock.smocked.getInputtedUint256.will.return.with(
(arg1: number) => {
return arg1
}
)
expect(
await mock.callStatic.getInputtedUint256(expected)
).to.equal(expected)
})
it('should be able to return a bytes32', async () => {
const expected =
'0x1234123412341234123412341234123412341234123412341234123412341234'
mock.smocked.getInputtedBytes32.will.return.with(
(arg1: string) => {
return arg1
}
)
expect(
await mock.callStatic.getInputtedBytes32(expected)
).to.equal(expected)
})
})
})
describe('from an asynchronous function', () => {
describe('without input arguments', () => {
it('should be able to return a boolean', async () => {
const expected = async () => {
return true
}
mock.smocked.getBoolean.will.return.with(async () => {
return expected()
})
expect(await mock.callStatic.getBoolean()).to.equal(
await expected()
)
})
it('should be able to return a uint256', async () => {
const expected = async () => {
return 1234
}
mock.smocked.getUint256.will.return.with(async () => {
return expected()
})
expect(await mock.callStatic.getUint256()).to.equal(
await expected()
)
})
it('should be able to return a bytes32', async () => {
const expected = async () => {
return '0x1234123412341234123412341234123412341234123412341234123412341234'
}
mock.smocked.getBytes32.will.return.with(async () => {
return expected()
})
expect(await mock.callStatic.getBytes32()).to.equal(
await expected()
)
})
})
})
describe.skip('resetting function behavior', () => {
describe('for a boolean', () => {
it('should return false after resetting', async () => {
const expected1 = true
mock.smocked.getBoolean.will.return.with(expected1)
expect(await mock.callStatic.getBoolean()).to.equal(expected1)
const expected2 = false
mock.smocked.getBoolean.reset()
expect(await mock.callStatic.getBoolean()).to.equal(expected2)
})
it('should be able to reset and change behaviors', async () => {
const expected1 = true
mock.smocked.getBoolean.will.return.with(expected1)
expect(await mock.callStatic.getBoolean()).to.equal(expected1)
const expected2 = false
mock.smocked.getBoolean.reset()
expect(await mock.callStatic.getBoolean()).to.equal(expected2)
const expected3 = true
mock.smocked.getBoolean.will.return.with(expected3)
expect(await mock.callStatic.getBoolean()).to.equal(expected3)
})
})
describe('for a uint256', () => {
it('should return zero after resetting', async () => {
const expected1 = 1234
mock.smocked.getUint256.will.return.with(expected1)
expect(await mock.callStatic.getUint256()).to.equal(expected1)
const expected2 = 0
mock.smocked.getUint256.reset()
expect(await mock.callStatic.getUint256()).to.equal(expected2)
})
it('should be able to reset and change behaviors', async () => {
const expected1 = 1234
mock.smocked.getUint256.will.return.with(expected1)
expect(await mock.callStatic.getUint256()).to.equal(expected1)
const expected2 = 0
mock.smocked.getUint256.reset()
expect(await mock.callStatic.getUint256()).to.equal(expected2)
const expected3 = 4321
mock.smocked.getUint256.will.return.with(expected3)
expect(await mock.callStatic.getUint256()).to.equal(expected3)
})
})
describe('for a bytes32', () => {
it('should return 32 zero bytes after resetting', async () => {
const expected1 =
'0x1234123412341234123412341234123412341234123412341234123412341234'
mock.smocked.getBytes32.will.return.with(expected1)
expect(await mock.callStatic.getBytes32()).to.equal(expected1)
const expected2 =
'0x0000000000000000000000000000000000000000000000000000000000000000'
mock.smocked.getBytes32.reset()
expect(await mock.callStatic.getBytes32()).to.equal(expected2)
})
it('should be able to reset and change behaviors', async () => {
const expected1 =
'0x1234123412341234123412341234123412341234123412341234123412341234'
mock.smocked.getBytes32.will.return.with(expected1)
expect(await mock.callStatic.getBytes32()).to.equal(expected1)
const expected2 =
'0x0000000000000000000000000000000000000000000000000000000000000000'
mock.smocked.getBytes32.reset()
expect(await mock.callStatic.getBytes32()).to.equal(expected2)
const expected3 =
'0x4321432143214321432143214321432143214321432143214321432143214321'
mock.smocked.getBytes32.will.return.with(expected3)
expect(await mock.callStatic.getBytes32()).to.equal(expected3)
})
})
})
})
describe('dynamic data types', () => {
describe('from a specified value', () => {
it('should be able to return a bytes value', async () => {
const expected =
'0x56785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678'
mock.smocked.getBytes.will.return.with(expected)
expect(await mock.callStatic.getBytes()).to.equal(expected)
})
it('should be able to return a string value', async () => {
const expected = 'this is an expected return string'
mock.smocked.getString.will.return.with(expected)
expect(await mock.callStatic.getString()).to.equal(expected)
})
it('should be able to return a struct with fixed size values', async () => {
const expected = {
valBoolean: true,
valUint256: BigNumber.from(1234),
valBytes32:
'0x1234123412341234123412341234123412341234123412341234123412341234',
}
mock.smocked.getStructFixedSize.will.return.with(expected)
const result = toPlainObject(
await mock.callStatic.getStructFixedSize()
)
expect(result.valBoolean).to.equal(expected.valBoolean)
expect(result.valUint256).to.deep.equal(expected.valUint256)
expect(result.valBytes32).to.equal(expected.valBytes32)
})
it('should be able to return a struct with dynamic size values', async () => {
const expected = {
valBytes:
'0x56785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678',
valString: 'this is an expected return string',
}
mock.smocked.getStructDynamicSize.will.return.with(expected)
const result = toPlainObject(
await mock.callStatic.getStructDynamicSize()
)
expect(result.valBytes).to.equal(expected.valBytes)
expect(result.valString).to.equal(expected.valString)
})
it('should be able to return a struct with both fixed and dynamic size values', async () => {
const expected = {
valBoolean: true,
valUint256: BigNumber.from(1234),
valBytes32:
'0x1234123412341234123412341234123412341234123412341234123412341234',
valBytes:
'0x56785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678',
valString: 'this is an expected return string',
}
mock.smocked.getStructMixedSize.will.return.with(expected)
const result = toPlainObject(
await mock.callStatic.getStructMixedSize()
)
expect(result.valBoolean).to.equal(expected.valBoolean)
expect(result.valUint256).to.deep.equal(expected.valUint256)
expect(result.valBytes32).to.equal(expected.valBytes32)
expect(result.valBytes).to.equal(expected.valBytes)
expect(result.valString).to.equal(expected.valString)
})
it('should be able to return a nested struct', async () => {
const expected = {
valStructFixedSize: {
valBoolean: true,
valUint256: BigNumber.from(1234),
valBytes32:
'0x1234123412341234123412341234123412341234123412341234123412341234',
},
valStructDynamicSize: {
valBytes:
'0x56785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678567856785678',
valString: 'this is an expected return string',
},
}
mock.smocked.getStructNested.will.return.with(expected)
const result = toPlainObject(
await mock.callStatic.getStructNested()
)
expect(result.valStructFixedSize[0]).to.deep.equal(
expected.valStructFixedSize.valBoolean
)
expect(result.valStructFixedSize[1]).to.deep.equal(
expected.valStructFixedSize.valUint256
)
expect(result.valStructFixedSize[2]).to.deep.equal(
expected.valStructFixedSize.valBytes32
)
expect(result.valStructDynamicSize[0]).to.deep.equal(
expected.valStructDynamicSize.valBytes
)
expect(result.valStructDynamicSize[1]).to.deep.equal(
expected.valStructDynamicSize.valString
)
})
it('should be able to return an array of uint256 values', async () => {
const expected = [1234, 2345, 3456, 4567, 5678, 6789].map((n) => {
return BigNumber.from(n)
})
mock.smocked.getArrayUint256.will.return.with(expected)
const result = await mock.callStatic.getArrayUint256()
for (let i = 0; i < result.length; i++) {
expect(result[i]).to.deep.equal(expected[i])
}
})
})
})
})
describe('reverting with data', () => {
describe('from a specified value', () => {
it('should be able to revert with a string value', async () => {
const expected = 'this is a revert string'
mock.smocked.getUint256.will.revert.with(expected)
await expect(mock.callStatic.getUint256()).to.be.revertedWith(
expected
)
})
})
describe('from a function', () => {
it('should be able to revert with a string value', async () => {
const expected = 'this is a revert string'
mock.smocked.getUint256.will.revert.with(() => {
return expected
})
await expect(mock.callStatic.getUint256()).to.be.revertedWith(
expected
)
})
})
describe('from an asynchronous function', () => {
it('should be able to revert with a string value', async () => {
const expected = async () => {
return 'this is a revert string'
}
mock.smocked.getUint256.will.revert.with(async () => {
return expected()
})
await expect(mock.callStatic.getUint256()).to.be.revertedWith(
await expected()
)
})
})
describe.skip('resetting function behavior', async () => {
describe('for a boolean', () => {
it('should return false after resetting', async () => {
const expected1 = 'this is a revert string'
mock.smocked.getBoolean.will.revert.with(expected1)
await expect(mock.callStatic.getBoolean()).to.be.revertedWith(
expected1
)
const expected2 = false
mock.smocked.getBoolean.reset()
expect(await mock.callStatic.getBoolean()).to.equal(expected2)
})
it('should be able to reset and change behaviors', async () => {
const expected1 = 'this is a revert string'
mock.smocked.getBoolean.will.revert.with(expected1)
await expect(mock.callStatic.getBoolean()).to.be.revertedWith(
expected1
)
const expected2 = false
mock.smocked.getBoolean.reset()
expect(await mock.callStatic.getBoolean()).to.equal(expected2)
const expected3 = true
mock.smocked.getBoolean.will.return.with(expected3)
expect(await mock.callStatic.getBoolean()).to.equal(expected3)
})
})
describe('for a uint256', () => {
it('should return zero after resetting', async () => {
const expected1 = 'this is a revert string'
mock.smocked.getUint256.will.revert.with(expected1)
await expect(mock.callStatic.getUint256()).to.be.revertedWith(
expected1
)
const expected2 = 0
mock.smocked.getUint256.reset()
expect(await mock.callStatic.getUint256()).to.equal(expected2)
})
it('should be able to reset and change behaviors', async () => {
const expected1 = 'this is a revert string'
mock.smocked.getUint256.will.revert.with(expected1)
await expect(mock.callStatic.getUint256()).to.be.revertedWith(
expected1
)
const expected2 = 0
mock.smocked.getUint256.reset()
expect(await mock.callStatic.getUint256()).to.equal(expected2)
const expected3 = 1234
mock.smocked.getUint256.will.return.with(expected3)
expect(await mock.callStatic.getUint256()).to.equal(expected3)
})
})
describe('for a bytes32', () => {
it('should return 32 zero bytes after resetting', async () => {
const expected1 = 'this is a revert string'
mock.smocked.getBytes32.will.revert.with(expected1)
await expect(mock.callStatic.getBytes32()).to.be.revertedWith(
expected1
)
const expected2 =
'0x0000000000000000000000000000000000000000000000000000000000000000'
mock.smocked.getBytes32.reset()
expect(await mock.callStatic.getBytes32()).to.equal(expected2)
})
it('should be able to reset and change behaviors', async () => {
const expected1 = 'this is a revert string'
mock.smocked.getBytes32.will.revert.with(expected1)
await expect(mock.callStatic.getBytes32()).to.be.revertedWith(
expected1
)
const expected2 =
'0x0000000000000000000000000000000000000000000000000000000000000000'
mock.smocked.getBytes32.reset()
expect(await mock.callStatic.getBytes32()).to.equal(expected2)
const expected3 =
'0x4321432143214321432143214321432143214321432143214321432143214321'
mock.smocked.getBytes32.will.return.with(expected3)
expect(await mock.callStatic.getBytes32()).to.equal(expected3)
})
})
})
})
})
})
/* Imports: External */
import { ethers, artifacts } from 'hardhat'
import { expect } from 'chai'
/* Imports: Internal */
import { smockit, isMockContract } from '../../src'
describe('[smock]: initialization tests', () => {
describe('initialization: ethers objects', () => {
it('should be able to create a SmockContract from an ethers ContractFactory', async () => {
const spec = await ethers.getContractFactory('TestHelpers_EmptyContract')
const mock = await smockit(spec)
expect(isMockContract(mock)).to.be.true
})
it('should be able to create a SmockContract from an ethers Contract', async () => {
const factory = await ethers.getContractFactory(
'TestHelpers_EmptyContract'
)
const spec = await factory.deploy()
const mock = await smockit(spec)
expect(isMockContract(mock)).to.be.true
})
it('should be able to create a SmockContract from an ethers Interface', async () => {
const factory = await ethers.getContractFactory(
'TestHelpers_EmptyContract'
)
const spec = factory.interface
const mock = await smockit(spec)
expect(isMockContract(mock)).to.be.true
})
})
describe('initialization: other', () => {
it('should be able to create a SmockContract from a contract name', async () => {
const spec = 'TestHelpers_EmptyContract'
const mock = await smockit(spec)
expect(isMockContract(mock)).to.be.true
})
it('should be able to create a SmockContract from a JSON contract artifact object', async () => {
const artifact = await artifacts.readArtifact(
'TestHelpers_BasicReturnContract'
)
const spec = artifact
const mock = await smockit(spec)
expect(isMockContract(mock)).to.be.true
})
it('should be able to create a SmockContract from a JSON contract ABI object', async () => {
const artifact = await artifacts.readArtifact(
'TestHelpers_BasicReturnContract'
)
const spec = artifact.abi
const mock = await smockit(spec)
expect(isMockContract(mock)).to.be.true
})
it('should be able to create a SmockContract from a JSON contract ABI string', async () => {
const artifact = await artifacts.readArtifact(
'TestHelpers_BasicReturnContract'
)
const spec = JSON.stringify(artifact.abi)
const mock = await smockit(spec)
expect(isMockContract(mock)).to.be.true
})
})
})
/* Imports: External */
import { expect } from 'chai'
import { BigNumber } from 'ethers'
import _ from 'lodash'
/* Imports: Internal */
import {
ModifiableContractFactory,
ModifiableContract,
smoddit,
} from '../../src/smoddit'
describe('smoddit', () => {
describe('via contract factory', () => {
describe('for functions with a single fixed return value', () => {
let SmodFactory: ModifiableContractFactory
before(async () => {
SmodFactory = await smoddit('SimpleStorageGetter')
})
let smod: ModifiableContract
beforeEach(async () => {
smod = await SmodFactory.deploy(4321)
})
it('should be able to return a uint256', async () => {
const ret = 1234
smod.smodify.put({
_uint256: ret,
})
expect(await smod.getUint256()).to.equal(ret)
})
it('should be able to return a boolean', async () => {
const ret = true
smod.smodify.put({
_bool: ret,
})
expect(await smod.getBool()).to.equal(ret)
})
it('should be able to return a simple struct', async () => {
const ret = {
valueA: BigNumber.from(1234),
valueB: true,
}
smod.smodify.put({
_SimpleStruct: ret,
})
const result = _.toPlainObject(await smod.getSimpleStruct())
expect(result.valueA).to.deep.equal(ret.valueA)
expect(result.valueB).to.deep.equal(ret.valueB)
})
it('should be able to return a simple uint256 => uint256 mapping value', async () => {
const retKey = 1234
const retVal = 5678
smod.smodify.put({
_uint256Map: {
[retKey]: retVal,
},
})
expect(await smod.getUint256MapValue(retKey)).to.equal(retVal)
})
it('should be able to return a nested uint256 => uint256 mapping value', async () => {
const retKeyA = 1234
const retKeyB = 4321
const retVal = 5678
smod.smodify.put({
_uint256NestedMap: {
[retKeyA]: {
[retKeyB]: retVal,
},
},
})
expect(await smod.getNestedUint256MapValue(retKeyA, retKeyB)).to.equal(
retVal
)
})
it('should not return the set value if the value has been changed by the contract', async () => {
const ret = 1234
smod.smodify.put({
_uint256: ret,
})
await smod.setUint256(4321)
expect(await smod.getUint256()).to.equal(4321)
})
it('should return the set value if it was set in the constructor', async () => {
const ret = 1234
smod.smodify.put({
_constructorUint256: ret,
})
expect(await smod.getConstructorUint256()).to.equal(1234)
})
})
})
})
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"src/**/*"
],
"files": [
"./hardhat.config.ts"
]
}
{
"extends": "../../tsconfig.json",
"files": [
"./hardhat.config.ts"
]
}
{
"extends": "../../tslint.base.json"
}
{
"$schema": "http://json.schemastore.org/prettierrc",
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"arrowParens": "always"
}
{
"compilerOptions": {
"composite": true,
"module": "commonjs",
"target": "es2017",
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"outDir": "./build",
"lib": [
"es7"
],
"esModuleInterop": true,
"typeRoots": [
"node_modules/@types"
]
}
}
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"sourceMap": true,
"esModuleInterop": true,
"composite": true,
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"typeRoots": [
"node_modules/@types"
]
},
"exclude": [
"node_modules",
"dist"
]
}
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@eth-optimism/*": ["packages/*/src"]
},
"skipLibCheck": true
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment