Commit a0fe4354 authored by Kelvin Fichter's avatar Kelvin Fichter

chore: deprecate smock v1

parent 8c3e8e11
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
# packages/specs/protocol/ @smartcontracts @ben-chain @maurelian # packages/specs/protocol/ @smartcontracts @ben-chain @maurelian
# ops/ @tynes @karlfloersch # ops/ @tynes @karlfloersch
# packages/hardhat-ovm/ @smartcontracts # packages/hardhat-ovm/ @smartcontracts
# packages/smock/ @smartcontracts @maurelian
# packages/core-utils/ @smartcontracts @annieke @ben-chain # packages/core-utils/ @smartcontracts @annieke @ben-chain
# packages/common-ts/ @annieke # packages/common-ts/ @annieke
# packages/core-utils/src/watcher.ts @K-Ho # packages/core-utils/src/watcher.ts @K-Ho
......
...@@ -42,7 +42,3 @@ M-hardhat-ovm: ...@@ -42,7 +42,3 @@ M-hardhat-ovm:
M-ops: M-ops:
- any: ['ops/**/*'] - any: ['ops/**/*']
M-smock:
- any: ['packages/smock/**/*']
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
{"directory": "packages/core-utils", "changeProcessCWD": true }, {"directory": "packages/core-utils", "changeProcessCWD": true },
{"directory": "packages/common-ts", "changeProcessCWD": true }, {"directory": "packages/common-ts", "changeProcessCWD": true },
{"directory": "packages/hardhat-ovm", "changeProcessCWD": true }, {"directory": "packages/hardhat-ovm", "changeProcessCWD": true },
{"directory": "packages/smock", "changeProcessCWD": true },
{"directory": "packages/contracts", "changeProcessCWD": true }, {"directory": "packages/contracts", "changeProcessCWD": true },
{"directory": "packages/data-transport-layer", "changeProcessCWD": true }, {"directory": "packages/data-transport-layer", "changeProcessCWD": true },
{"directory": "packages/batch-submitter", "changeProcessCWD": true }, {"directory": "packages/batch-submitter", "changeProcessCWD": true },
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
These packages only require 1 reviewer (all other packages require 2 reviewers, unless the changes do not affect production or test code). These packages only require 1 reviewer (all other packages require 2 reviewers, unless the changes do not affect production or test code).
- packages/smock
- packages/core-utils - packages/core-utils
- packages/hardhat-ovm - packages/hardhat-ovm
- packages/common-ts - packages/common-ts
......
...@@ -28,7 +28,6 @@ Extensive documentation is available [here](http://community.optimism.io/docs/). ...@@ -28,7 +28,6 @@ Extensive documentation is available [here](http://community.optimism.io/docs/).
* [`core-utils`](./packages/core-utils): Low-level utilities and encoding packages * [`core-utils`](./packages/core-utils): Low-level utilities and encoding packages
* [`common-ts`](./packages/common-ts): Common tools for TypeScript code that runs in Node * [`common-ts`](./packages/common-ts): Common tools for TypeScript code that runs in Node
* [`hardhat-ovm`](./packages/hardhat-ovm): Hardhat plugin which enables the [OVM Compiler](https://github.com/ethereum-optimism/solidity) * [`hardhat-ovm`](./packages/hardhat-ovm): Hardhat plugin which enables the [OVM Compiler](https://github.com/ethereum-optimism/solidity)
* [`smock`](./packages/smock): Testing utility for mocking smart contract return values and storage
* [`data-transport-layer`](./packages/data-transport-layer): Event indexer, allowing the `l2geth` node to access L1 data * [`data-transport-layer`](./packages/data-transport-layer): Event indexer, allowing the `l2geth` node to access L1 data
* [`batch-submitter`](./packages/batch-submitter): Daemon for submitting L2 transaction and state root batches to L1 * [`batch-submitter`](./packages/batch-submitter): Daemon for submitting L2 transaction and state root batches to L1
* [`message-relayer`](./packages/message-relayer): Service for relaying L2 messages to L1 * [`message-relayer`](./packages/message-relayer): Service for relaying L2 messages to L1
......
...@@ -27,7 +27,6 @@ COPY *.json yarn.lock ./ ...@@ -27,7 +27,6 @@ COPY *.json yarn.lock ./
COPY packages/core-utils/package.json ./packages/core-utils/package.json COPY packages/core-utils/package.json ./packages/core-utils/package.json
COPY packages/common-ts/package.json ./packages/common-ts/package.json COPY packages/common-ts/package.json ./packages/common-ts/package.json
COPY packages/hardhat-ovm/package.json ./packages/hardhat-ovm/package.json COPY packages/hardhat-ovm/package.json ./packages/hardhat-ovm/package.json
COPY packages/smock/package.json ./packages/smock/package.json
COPY packages/contracts/package.json ./packages/contracts/package.json COPY packages/contracts/package.json ./packages/contracts/package.json
COPY packages/data-transport-layer/package.json ./packages/data-transport-layer/package.json COPY packages/data-transport-layer/package.json ./packages/data-transport-layer/package.json
COPY packages/batch-submitter/package.json ./packages/batch-submitter/package.json COPY packages/batch-submitter/package.json ./packages/batch-submitter/package.json
......
...@@ -45,6 +45,7 @@ ...@@ -45,6 +45,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eth-optimism/smock": "^1.1.9", "@eth-optimism/smock": "^1.1.9",
"@nomiclabs/ethereumjs-vm": "^4",
"@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1", "@nomiclabs/hardhat-waffle": "^2.0.1",
"@types/chai": "^4.2.18", "@types/chai": "^4.2.18",
...@@ -52,24 +53,24 @@ ...@@ -52,24 +53,24 @@
"@types/mocha": "^8.2.2", "@types/mocha": "^8.2.2",
"@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0", "@typescript-eslint/parser": "^4.26.0",
"babel-eslint": "^10.1.0",
"chai": "^4.3.4", "chai": "^4.3.4",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"eslint-plugin-prettier": "^3.4.0", "eslint": "^7.27.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-ban": "^1.5.2", "eslint-plugin-ban": "^1.5.2",
"eslint-plugin-import": "^2.23.4", "eslint-plugin-import": "^2.23.4",
"eslint-plugin-jsdoc": "^35.1.2", "eslint-plugin-jsdoc": "^35.1.2",
"eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.24.0",
"eslint-plugin-unicorn": "^32.0.1", "eslint-plugin-unicorn": "^32.0.1",
"ethereum-waffle": "^3.3.0", "ethereum-waffle": "^3.3.0",
"hardhat": "^2.3.0", "hardhat": "^2.3.0",
"lint-staged": "11.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mocha": "^8.4.0", "mocha": "^8.4.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.27.0",
"lint-staged": "11.0.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"typescript": "^4.2.3" "typescript": "^4.2.3"
} }
} }
\ No newline at end of file
module.exports = {
extends: '../../.eslintrc.js',
}
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
module.exports = {
...require('../../.prettierrc.js'),
};
\ No newline at end of file
# @eth-optimism/smock
## 1.1.10
### Patch Changes
- 269b0788: Adds naive support for packed storage slots
## 1.1.9
### Patch Changes
- c73c3939: Update the typescript version to `4.3.5`
- Updated dependencies [c73c3939]
- @eth-optimism/core-utils@0.5.1
## 1.1.8
### Patch Changes
- d1da05be: Add a test and a doc section for returning multiple uint256 arrays
## 1.1.7
### Patch Changes
- Updated dependencies [049200f4]
- @eth-optimism/core-utils@0.5.0
## 1.1.6
### Patch Changes
- 71349a4e: Minor smock patch to add support for hardhat 2.4.0 and up
- Updated dependencies [d9644c34]
- Updated dependencies [df5ff890]
- @eth-optimism/core-utils@0.4.6
## 1.1.5
### Patch Changes
- 5e3c5d1c: Fixes a bug that would break call assertions for overloaded smocked functions
- e6e87ae1: Fix a bug where overloaded functions would not be handled correctly
- Updated dependencies [a64f8161]
- Updated dependencies [750a5021]
- Updated dependencies [c2b6e14b]
- @eth-optimism/core-utils@0.4.5
## 1.1.4
### Patch Changes
- a656f06: Adds a wallet object to smock contracts that can be used to send transactions
## 1.1.3
### Patch Changes
- 1d40586: Removed various unused dependencies
- Updated dependencies [1d40586]
- Updated dependencies [ce7fa52]
- @eth-optimism/core-utils@0.4.1
## 1.1.2
### Patch Changes
- Updated dependencies [28dc442]
- Updated dependencies [a0a0052]
- @eth-optimism/core-utils@0.4.0
## 1.1.1
### Patch Changes
- 6daa408: update hardhat versions so that solc is resolved correctly
- Updated dependencies [6daa408]
- Updated dependencies [dee74ef]
- Updated dependencies [d64b66d]
- @eth-optimism/core-utils@0.3.2
## 1.1.0
### Minor Changes
- 79f812d: Adds support for hardhat ^2.2.0, required because of move to ethereumjs-vm v5.
## 1.0.2
### Patch Changes
- Updated dependencies [91460d9]
- Updated dependencies [a0a7956]
- Updated dependencies [0497d7d]
- @eth-optimism/core-utils@0.3.0
## 1.0.1
### Patch Changes
- 5362d38: adds build files which were not published before to npm
- Updated dependencies [5362d38]
- @eth-optimism/core-utils@0.2.1
## 1.0.0
### Patch Changes
- Updated dependencies [6cbc54d]
- @eth-optimism/core-utils@0.2.0
(The MIT License)
Copyright 2020-2021 Optimism
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# @eth-optimism/smock # @eth-optimism/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. ## NOTICE
Some nice benefits of hooking in at the VM level: Smock v1 is being deprecated.
* Don't need to deploy any special contracts just for mocking! Please migrate to [Smock v2](https://github.com/defi-wonderland/smock).
* All of the calls are synchronous. You can find an archive of the Smock v1 codebase at [optimism@d337713c91](https://github.com/ethereum-optimism/optimism/tree/d337713c91c6634f546b8d6572392c0784ab8217/packages/smock).
* 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
```
## 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 ethers.Contract {
smocked: {
[functionName: string]: MockContractFunction
}
// A wallet you can use to send transactions *from* a smocked contract
wallet: ethers.Signer
}
```
##### `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 ethers.ContractFactory {
deploy: (...args: any[]) => Promise<ModifiableContract>
}
```
##### `ModifiableContract`
```typescript
interface ModifiableContract extends ethers.Contract {
smodify: {
put: (storage: any) => Promise<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 Multiple Arrays
```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([
[1234, 5678],
[4321, 8765]
])
console.log(await MyMockContract.myFunction()) // [ [1234, 5678], [4321, 8765] ]
```
### 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!
```
### Sending transactions from a smocked 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 mock = await smockit('AnotherContract')
await myContract.connect(mock.wallet).doSomeFunction() // msg.sender == mock.address
```
## 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(...)
await 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(...)
await 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(...)
await 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(...)
await 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",
"files": [
"dist/src/*"
],
"version": "1.1.10",
"main": "dist/src/index",
"types": "dist/src/index",
"author": "Optimism PBC",
"license": "MIT",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "hardhat test --show-stack-traces",
"lint": "yarn lint:fix && yarn lint:check",
"pre-commit": "lint-staged",
"lint:fix": "yarn lint:check --fix",
"lint:check": "eslint .",
"clean": "rimraf ./artifacts ./cache ./dist ./tsconfig.build.tsbuildinfo"
},
"peerDependencies": {
"@ethersproject/abi": "^5",
"@ethersproject/abstract-provider": "^5",
"@ethersproject/abstract-signer": "^5",
"@nomiclabs/ethereumjs-vm": "^4",
"@nomiclabs/hardhat-ethers": "^2",
"ethers": "^5",
"hardhat": "^2"
},
"dependencies": {
"@eth-optimism/core-utils": "^0.5.1",
"bn.js": "^5.2.0"
},
"devDependencies": {
"@ethersproject/abi": "^5.1.2",
"@ethersproject/abstract-provider": "^5.1.0",
"@ethersproject/abstract-signer": "^5.1.0",
"@nomiclabs/ethereumjs-vm": "^4.2.2",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.2.17",
"@types/glob": "^7.1.3",
"@types/lodash": "^4.14.161",
"@types/prettier": "^2.2.3",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"chai": "^4.3.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.27.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-ban": "^1.5.2",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-jsdoc": "^35.1.2",
"eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-unicorn": "^32.0.1",
"ethereum-waffle": "^3.3.0",
"ethers": "^5.0.31",
"hardhat": "^2.4.0",
"lodash": "^4.17.20",
"prettier": "^2.2.1",
"lint-staged": "11.0.0",
"typescript": "^4.2.3"
}
}
/* Imports: External */
import { HardhatRuntimeEnvironment } from 'hardhat/types'
import { HardhatNetworkProvider } from 'hardhat/internal/hardhat-network/provider/provider'
import { fromHexString, toHexString } from '@eth-optimism/core-utils'
/**
* 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
}
/**
* Converts a string into the fancy new address thing that ethereumjs-vm v5 expects while also
* maintaining backwards compatibility with ethereumjs-vm v4.
*
* @param address String address to convert into the fancy new address type.
* @returns Fancified address.
*/
export const toFancyAddress = (address: string): any => {
const fancyAddress = fromHexString(address)
;(fancyAddress as any).buf = fromHexString(address)
;(fancyAddress as any).toString = (encoding?: any) => {
if (encoding === undefined) {
return address.toLowerCase()
} else {
return fromHexString(address).toString(encoding)
}
}
return fancyAddress
}
/**
* Same as toFancyAddress but in the opposite direction.
*
* @param fancyAddress Fancy address to turn into a string.
* @returns Way more boring address.
*/
export const fromFancyAddress = (fancyAddress: any): string => {
if (fancyAddress.buf) {
return toHexString(fancyAddress.buf)
} else {
return toHexString(fancyAddress)
}
}
export * from './hardhat-common'
export * from './smockit'
export * from './smoddit'
/* Imports: External */
import { HardhatNetworkProvider } from 'hardhat/internal/hardhat-network/provider/provider'
import { VmError } from '@nomiclabs/ethereumjs-vm/dist/exceptions'
import BN from 'bn.js'
/* eslint-disable @typescript-eslint/no-var-requires */
// Handle hardhat ^2.4.0
let decodeRevertReason: (value: Buffer) => string
try {
decodeRevertReason =
require('hardhat/internal/hardhat-network/stack-traces/revert-reasons').decodeRevertReason
} catch (err) {
const {
ReturnData,
} = require('hardhat/internal/hardhat-network/provider/return-data')
decodeRevertReason = (value: Buffer) => {
const returnData = new ReturnData(value)
if (returnData.isErrorReturnData()) {
return returnData.decodeError()
} else {
return ''
}
}
}
// Handle hardhat ^2.2.0
let TransactionExecutionError: any
try {
TransactionExecutionError =
require('hardhat/internal/hardhat-network/provider/errors').TransactionExecutionError
} catch (err) {
TransactionExecutionError =
require('hardhat/internal/core/providers/errors').TransactionExecutionError
}
/* eslint-enable @typescript-eslint/no-var-requires */
/* Imports: Internal */
import { MockContract, SmockedVM } from './types'
import { fromFancyAddress, toFancyAddress } from '../common'
/**
* Checks to see if smock has been initialized already. Basically just checking to see if we've
* attached smock state to the VM already.
*
* @param provider Base hardhat network provider to check.
* @return Whether or not the provider has already been modified to support smock.
*/
const isSmockInitialized = (provider: HardhatNetworkProvider): boolean => {
return (provider as any)._node._vm._smockState !== undefined
}
/**
* Modifies a hardhat provider to be compatible with smock.
*
* @param provider Base hardhat network provider to modify.
*/
const initializeSmock = (provider: HardhatNetworkProvider): void => {
if (isSmockInitialized(provider)) {
return
}
// Will need to reference these things.
const node = (provider as any)._node
const vm: SmockedVM = node._vm
// Attach some extra state to the VM.
vm._smockState = {
mocks: {},
calls: {},
messages: [],
}
// Wipe out our list of calls before each transaction.
vm.on('beforeTx', () => {
vm._smockState.calls = {}
})
// Watch for new EVM messages (call frames).
vm.on('beforeMessage', (message: any) => {
// Happens with contract creations. If the current message is a contract creation then it can't
// be a call to a smocked contract.
if (!message.to) {
return
}
let target: string
if (message.delegatecall) {
target = fromFancyAddress(message._codeAddress)
} else {
target = fromFancyAddress(message.to)
}
// Check if the target address is a smocked contract.
if (!(target in vm._smockState.mocks)) {
return
}
// Initialize the array of calls to this smock if not done already.
if (!(target in vm._smockState.calls)) {
vm._smockState.calls[target] = []
}
// Record this message for later.
vm._smockState.calls[target].push(message.data)
vm._smockState.messages.push(message)
})
// Now *this* is a hack.
// Ethereumjs-vm passes `result` by *reference* into the `afterMessage` event. Mutating the
// `result` object here will actually mutate the result in the VM. Magic.
vm.on('afterMessage', async (result: any) => {
// We currently defer to contract creations, meaning we'll "unsmock" an address if a user
// later creates a contract at that address. Not sure how to handle this case. Very open to
// ideas.
if (result.createdAddress) {
const created = fromFancyAddress(result.createdAddress)
if (created in vm._smockState.mocks) {
delete vm._smockState.mocks[created]
}
}
// Check if we have messages that need to be handled.
if (vm._smockState.messages.length === 0) {
return
}
// Handle the last message that was pushed to the array of messages. This works because smock
// contracts never create new sub-calls (meaning this `afterMessage` event corresponds directly
// to a `beforeMessage` event emitted during a call to a smock contract).
const message = vm._smockState.messages.pop()
let target: string
if (message.delegatecall) {
target = fromFancyAddress(message._codeAddress)
} else {
target = fromFancyAddress(message.to)
}
// Not sure if this can ever actually happen? Just being safe.
if (!(target in vm._smockState.mocks)) {
return
}
// Compute the mock return data.
const mock: MockContract = vm._smockState.mocks[target]
const { resolve, functionName, rawReturnValue, returnValue, gasUsed } =
await mock._smockit(message.data)
// Set the mock return data, potentially set the `exceptionError` field if the user requested
// a revert.
result.gasUsed = new BN(gasUsed)
result.execResult.returnValue = returnValue
result.execResult.gasUsed = new BN(gasUsed)
result.execResult.exceptionError =
resolve === 'revert' ? new VmError('smocked revert' as any) : undefined
})
// Here we're fixing with hardhat's internal error management. Smock is a bit weird and messes
// with stack traces so we need to help hardhat out a bit when it comes to smock-specific
// errors.
const originalManagerErrorsFn = node._manageErrors.bind(node)
node._manageErrors = async (
vmResult: any,
vmTrace: any,
vmTracerError?: any
): Promise<any> => {
if (
vmResult.exceptionError &&
vmResult.exceptionError.error === 'smocked revert'
) {
return new TransactionExecutionError(
`VM Exception while processing transaction: revert ${decodeRevertReason(
vmResult.returnValue
)}`
)
}
return originalManagerErrorsFn(vmResult, vmTrace, vmTracerError)
}
}
/**
* Attaches a smocked contract to a hardhat network provider. Will also modify the provider to be
* compatible with smock if not done already.
*
* @param mock Smocked contract to attach to a provider.
* @param provider Hardhat network provider to attach the contract to.
*/
export const bindSmock = async (
mock: MockContract,
provider: HardhatNetworkProvider
): Promise<void> => {
if (!isSmockInitialized(provider)) {
initializeSmock(provider)
}
const vm: SmockedVM = (provider as any)._node._vm
const pStateManager = vm.pStateManager || vm.stateManager
// Add mock to our list of mocks currently attached to the VM.
vm._smockState.mocks[mock.address.toLowerCase()] = mock
// Set the contract code for our mock to 0x00 == STOP. Need some non-empty contract code because
// Solidity will sometimes throw if it's calling something without code (I forget the exact
// scenario that causes this throw).
await pStateManager.putContractCode(
toFancyAddress(mock.address),
Buffer.from('00', 'hex')
)
}
/**
* Detaches a smocked contract from a hardhat network provider.
*
* @param mock Smocked contract to detach to a provider, or an address.
* @param provider Hardhat network provider to detatch the contract from.
*/
export const unbindSmock = async (
mock: MockContract | string,
provider: HardhatNetworkProvider
): Promise<void> => {
if (!isSmockInitialized(provider)) {
initializeSmock(provider)
}
const vm: SmockedVM = (provider as any)._node._vm
const pStateManager = vm.pStateManager || vm.stateManager
// Add mock to our list of mocks currently attached to the VM.
const address = typeof mock === 'string' ? mock : mock.address.toLowerCase()
delete vm._smockState.mocks[address]
// Set the contract code for our mock to 0x00 == STOP. Need some non-empty contract code because
// Solidity will sometimes throw if it's calling something without code (I forget the exact
// scenario that causes this throw).
await pStateManager.putContractCode(
toFancyAddress(address),
Buffer.from('', 'hex')
)
}
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,
isContract,
isContractFactory,
isInterface,
MockContract,
MockContractFunction,
MockReturnValue,
SmockedVM,
SmockOptions,
SmockSpec,
} from './types'
import { bindSmock, unbindSmock } 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 (isInterface(spec)) {
return spec as any
} else if (isContractFactory(spec)) {
return (spec as any).interface
} else if (isContract(spec)) {
return (spec as any).interface
} 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 as any).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.format(),
data
)
} catch (e) {
console.error(e)
}
return {
functionName: fragment.name,
functionSignature: fragment.format(),
data,
}
})
.filter((functionResult: any) => {
return (
functionResult.functionName === functionName ||
functionResult.functionSignature === 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 as any).ethers.provider // TODO: Probably check that this exists.
) as MockContract
// We attach a wallet to the contract so that users can send transactions *from* a smock.
await hre.network.provider.request({
method: 'hardhat_impersonateAccount',
params: [contract.address],
})
// Now we actually get the signer and attach it to the mock.
contract.wallet = await (hre as any).ethers.getSigner(contract.address)
// 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] || this.smocked[fn.format()]
} 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
}
/**
* Unbinds a mock contract (meaning the contract will no longer behave as a mock).
*
* @param mock Mock contract or address to unbind.
*/
export const unbind = async (mock: MockContract | string): Promise<void> => {
// 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)
// Unbind the contract.
await unbindSmock(mock, provider)
}
/* Imports: External */
import { Artifact } from 'hardhat/types'
import { Contract, ContractFactory, ethers } from 'ethers'
import { Signer } from '@ethersproject/abstract-signer'
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
}
wallet: Signer
}
export interface SmockedVM {
_smockState: {
mocks: {
[address: string]: MockContract
}
calls: {
[address: string]: any[]
}
messages: any[]
}
on: (event: string, callback: Function) => void
stateManager?: {
putContractCode: (address: Buffer, code: Buffer) => Promise<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 isInterface = (obj: any): boolean => {
return (
obj &&
obj.functions !== undefined &&
obj.errors !== undefined &&
obj.structs !== undefined &&
obj.events !== undefined &&
Array.isArray(obj.fragments)
)
}
export const isContract = (obj: any): boolean => {
return (
obj &&
obj.functions !== undefined &&
obj.estimateGas !== undefined &&
obj.callStatic !== undefined
)
}
export const isContractFactory = (obj: any): boolean => {
return obj && obj.interface !== undefined && obj.deploy !== undefined
}
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
)
}
export * from './smoddit'
export * from './types'
/* External Imports */
import hre from 'hardhat'
import { fromHexString } from '@eth-optimism/core-utils'
/* Internal Imports */
import { ModifiableContract, ModifiableContractFactory } from './types'
import { getStorageLayout, getStorageSlots } from './storage'
import { toHexString32 } from '../utils'
import { findBaseHardhatProvider, toFancyAddress } 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 vm: any = (provider as any)._node._vm
const pStateManager = vm.pStateManager || vm.stateManager
const layout = await getStorageLayout(name)
const factory = (await (hre as any).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 = async (storage: any) => {
if (!storage) {
return
}
const slots = getStorageSlots(layout, storage)
for (const slot of slots) {
await pStateManager.putContractStorage(
toFancyAddress(contract.address),
fromHexString(slot.hash.toLowerCase()),
fromHexString(slot.value)
)
}
}
const check = async (storage: any) => {
if (!storage) {
return true
}
const slots = getStorageSlots(layout, storage)
for (const slot of slots) {
if (
toHexString32(
await pStateManager.getContractStorage(
toFancyAddress(contract.address),
fromHexString(slot.hash.toLowerCase())
)
) !== slot.value
) {
return false
}
}
return true
}
contract.smodify = {
put,
check,
}
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)
)
const slotValue = toHexString32(
`0x` + toHexString32(flat[key]).slice(2 + variableDef.offset * 2)
)
slots.push({
label: key,
hash: slotHash,
value: slotValue,
})
}
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) => Promise<void>
check: (storage: any) => Promise<boolean>
}
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')) {
// Known bug here is that bytes20 and address are indistinguishable but have to be treated
// differently. Address gets padded on the right, bytes20 gets padded on the left. Address is
// way more common so I'm going with the strategy of treating all bytes20 like addresses.
// Sorry to anyone who wants to smodify bytes20 values :-/ requires a bit of rewrite to fix.
if (value.length === 42) {
return '0x' + remove0x(value).padStart(64, '0').toLowerCase()
} else {
return '0x' + remove0x(value).padEnd(64, '0').toLowerCase()
}
} else if (typeof value === 'boolean') {
return '0x' + `${value ? 1 : 0}`.padStart(64, '0')
} else {
return (
'0x' +
remove0x(BigNumber.from(value).toHexString())
.padStart(64, '0')
.toLowerCase()
)
}
}
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;
}
address internal _address;
uint256 internal _constructorUint256;
uint256 internal _uint256;
bool internal _bool;
SimpleStruct internal _SimpleStruct;
mapping (uint256 => uint256) _uint256Map;
mapping (uint256 => mapping (uint256 => uint256)) _uint256NestedMap;
mapping (bytes5 => bool) _bytes5ToBoolMap;
mapping (address => bool) _addressToBoolMap;
mapping (address => address) _addressToAddressMap;
// Testing storage slot packing.
bool internal _packedA;
address internal _packedB;
// Regression for #1275.
uint256 internal __packingSpacerUnused1; // Spacer to avoid packing with the above two.
bool public booleanOne = true;
bool public booleanTwo = true;
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 getAddress()
public
view
returns (
address _out
)
{
return _address;
}
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];
}
function getBytes5ToBoolMapValue(
bytes5 _key
)
public
view
returns (
bool _out
)
{
return _bytes5ToBoolMap[_key];
}
function getAddressToBoolMapValue(
address _key
)
public
view
returns (
bool _out
)
{
return _addressToBoolMap[_key];
}
function getAddressToAddressMapValue(
address _key
)
public
view
returns (
address _out
)
{
return _addressToAddressMap[_key];
}
function getPackedAddress()
public
view
returns (
address
)
{
return _packedB;
}
}
// 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
)
{}
function getMultipleUint256Arrays()
public
returns (
uint256[] memory,
uint256[] memory
)
{}
function overloadedFunction(
uint256 _paramA,
uint256 _paramB
)
public
returns (
uint256
)
{}
function overloadedFunction(
uint256
)
public
returns (
uint256
)
{}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract TestHelpers_EmptyContract {}
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract TestHelpers_MockCaller {
function callMock(address _target, bytes memory _data) public {
_target.call(_data);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract TestHelpers_SenderAssertions {
function getSender()
public
view
returns (
address
)
{
return msg.sender;
}
}
/* Imports: External */
import hre from 'hardhat'
import { expect } from 'chai'
import { Contract } from 'ethers'
/* Imports: Internal */
import { MockContract, smockit } from '../../src'
describe('[smock]: call assertion tests', () => {
const ethers = (hre as any).ethers
let mock: MockContract
beforeEach(async () => {
mock = await smockit('TestHelpers_BasicReturnContract')
})
let mockCaller: Contract
before(async () => {
const mockCallerFactory = await ethers.getContractFactory(
'TestHelpers_MockCaller'
)
mockCaller = await mockCallerFactory.deploy()
})
describe('call assertions for functions', () => {
it('should be able to make assertions about a non-overloaded function', async () => {
mock.smocked.getInputtedUint256.will.return.with(0)
const expected1 = ethers.BigNumber.from(1234)
await mockCaller.callMock(
mock.address,
mock.interface.encodeFunctionData('getInputtedUint256(uint256)', [
expected1,
])
)
expect(mock.smocked.getInputtedUint256.calls[0]).to.deep.equal([
expected1,
])
})
it('should be able to make assertions about both versions of an overloaded function', async () => {
mock.smocked['overloadedFunction(uint256)'].will.return.with(0)
mock.smocked['overloadedFunction(uint256,uint256)'].will.return.with(0)
const expected1 = ethers.BigNumber.from(1234)
await mockCaller.callMock(
mock.address,
mock.interface.encodeFunctionData('overloadedFunction(uint256)', [
expected1,
])
)
expect(
mock.smocked['overloadedFunction(uint256)'].calls[0]
).to.deep.equal([expected1])
const expected2 = ethers.BigNumber.from(5678)
await mockCaller.callMock(
mock.address,
mock.interface.encodeFunctionData(
'overloadedFunction(uint256,uint256)',
[expected2, expected2]
)
)
expect(
mock.smocked['overloadedFunction(uint256,uint256)'].calls[0]
).to.deep.equal([expected2, expected2])
})
})
})
/* Imports: External */
import hre 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', () => {
const ethers = (hre as any).ethers
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('overloaded functions', () => {
it('should be able to modify both versions of an overloaded function', async () => {
const expected1 = 1234
const expected2 = 5678
mock.smocked['overloadedFunction(uint256)'].will.return.with(expected1)
mock.smocked['overloadedFunction(uint256,uint256)'].will.return.with(
expected2
)
expect(
await mock.callStatic['overloadedFunction(uint256)'](0)
).to.equal(expected1)
expect(
await mock.callStatic['overloadedFunction(uint256,uint256)'](0, 0)
).to.equal(expected2)
})
})
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])
}
})
it('should be able to return multiple arrays of uint256 values', async () => {
const expected = [
[1234, 2345, 3456, 4567, 5678, 6789].map((n) => {
return BigNumber.from(n)
}),
[1234, 2345, 3456, 4567, 5678, 6789].map((n) => {
return BigNumber.from(n)
}),
]
mock.smocked.getMultipleUint256Arrays.will.return.with(expected)
const result = await mock.callStatic.getMultipleUint256Arrays()
for (let i = 0; i < result.length; i++) {
for (let j = 0; j < result[i].length; j++) {
expect(result[i][j]).to.deep.equal(expected[i][j])
}
}
})
})
})
})
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 hre from 'hardhat'
import { expect } from 'chai'
import { Contract } from 'ethers'
/* Imports: Internal */
import { smockit } from '../../src'
describe('[smock]: sending transactions from smock contracts', () => {
const ethers = (hre as any).ethers
let TestHelpers_SenderAssertions: Contract
before(async () => {
TestHelpers_SenderAssertions = await (
await ethers.getContractFactory('TestHelpers_SenderAssertions')
).deploy()
})
it('should attach a signer for a mock with a random address', async () => {
const mock = await smockit('TestHelpers_BasicReturnContract')
expect(
await TestHelpers_SenderAssertions.connect(mock.wallet).getSender()
).to.equal(mock.address)
})
it('should attach a signer for a mock with a fixed address', async () => {
const mock = await smockit('TestHelpers_BasicReturnContract', {
address: '0x1234123412341234123412341234123412341234',
})
expect(
await TestHelpers_SenderAssertions.connect(mock.wallet).getSender()
).to.equal(mock.address)
})
})
/* Imports: External */
import hre from 'hardhat'
import { expect } from 'chai'
/* Imports: Internal */
import { smockit, isMockContract } from '../../src'
describe('[smock]: initialization tests', () => {
const ethers = (hre as any).ethers
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 hre.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 hre.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 hre.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
await smod.smodify.put({
_uint256: ret,
})
expect(await smod.getUint256()).to.equal(ret)
})
it('should be able to return a boolean', async () => {
const ret = true
await smod.smodify.put({
_bool: ret,
})
expect(await smod.getBool()).to.equal(ret)
})
it('should be able to return an address', async () => {
const ret = '0x558ba9b8d78713fbf768c1f8a584485B4003f43F'
await smod.smodify.put({
_address: ret,
})
expect(await smod.getAddress()).to.equal(ret)
})
it('should be able to return an address in a packed storage slot', async () => {
const ret = '0x558ba9b8d78713fbf768c1f8a584485B4003f43F'
await smod.smodify.put({
_packedB: ret,
})
expect(await smod.getPackedAddress()).to.equal(ret)
})
it('should be able to return a simple struct', async () => {
const ret = {
valueA: BigNumber.from(1234),
valueB: true,
}
await 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
await 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
await 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
await 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
await smod.smodify.put({
_constructorUint256: ret,
})
expect(await smod.getConstructorUint256()).to.equal(1234)
})
it('should be able to set values in a bytes5 => bool mapping', async () => {
const key = '0x0000005678'
const val = true
await smod.smodify.put({
_bytes5ToBoolMap: {
[key]: val,
},
})
expect(await smod.getBytes5ToBoolMapValue(key)).to.equal(val)
})
it('should be able to set values in a address => bool mapping', async () => {
const key = '0x558ba9b8d78713fbf768c1f8a584485B4003f43F'
const val = true
await smod.smodify.put({
_addressToBoolMap: {
[key]: val,
},
})
expect(await smod.getAddressToBoolMapValue(key)).to.equal(val)
})
it('should be able to set values in a address => address mapping', async () => {
const key = '0x558ba9b8d78713fbf768c1f8a584485B4003f43F'
const val = '0x063bE0Af9711a170BE4b07028b320C90705fec7C'
await smod.smodify.put({
_addressToAddressMap: {
[key]: val,
},
})
expect(await smod.getAddressToAddressMapValue(key)).to.equal(val)
})
it('should be able to pack two booleans', async () => {
const ret = true
expect(await smod.booleanTwo()).to.equal(ret)
await smod.smodify.put({
booleanTwo: ret,
})
expect(await smod.booleanTwo()).to.equal(ret)
})
})
})
})
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"src/**/*"
],
"files": [
"./hardhat.config.ts"
]
}
{
"extends": "../../tsconfig.json"
}
...@@ -500,6 +500,14 @@ ...@@ -500,6 +500,14 @@
minimatch "^3.0.4" minimatch "^3.0.4"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@eth-optimism/smock@^1.1.10", "@eth-optimism/smock@^1.1.9":
version "1.1.10"
resolved "https://registry.yarnpkg.com/@eth-optimism/smock/-/smock-1.1.10.tgz#98a6eefc994ccf707f52ab06849468f3cc57bdb7"
integrity sha512-XPx1x9odF/noTBHzIhRgL9ihhr769WgUhf9dOm6X7bjSWRAVsII3IqbdB4ssPycaoSuNSmv8HG1xTLgfgcyOYw==
dependencies:
"@eth-optimism/core-utils" "^0.5.1"
bn.js "^5.2.0"
"@eth-optimism/solc@^0.6.12-alpha.1": "@eth-optimism/solc@^0.6.12-alpha.1":
version "0.6.12-alpha.1" version "0.6.12-alpha.1"
resolved "https://registry.yarnpkg.com/@eth-optimism/solc/-/solc-0.6.12-alpha.1.tgz#041876f83b34c6afe2f19dfe9626568df6ed8590" resolved "https://registry.yarnpkg.com/@eth-optimism/solc/-/solc-0.6.12-alpha.1.tgz#041876f83b34c6afe2f19dfe9626568df6ed8590"
...@@ -1876,7 +1884,7 @@ ...@@ -1876,7 +1884,7 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@nomiclabs/ethereumjs-vm@^4.2.2": "@nomiclabs/ethereumjs-vm@^4":
version "4.2.2" version "4.2.2"
resolved "https://registry.yarnpkg.com/@nomiclabs/ethereumjs-vm/-/ethereumjs-vm-4.2.2.tgz#2f8817113ca0fb6c44c1b870d0a809f0e026a6cc" resolved "https://registry.yarnpkg.com/@nomiclabs/ethereumjs-vm/-/ethereumjs-vm-4.2.2.tgz#2f8817113ca0fb6c44c1b870d0a809f0e026a6cc"
integrity sha512-8WmX94mMcJaZ7/m7yBbyuS6B+wuOul+eF+RY9fBpGhNaUpyMR/vFIcDojqcWQ4Yafe1tMKY5LDu2yfT4NZgV4Q== integrity sha512-8WmX94mMcJaZ7/m7yBbyuS6B+wuOul+eF+RY9fBpGhNaUpyMR/vFIcDojqcWQ4Yafe1tMKY5LDu2yfT4NZgV4Q==
...@@ -8006,7 +8014,7 @@ hardhat-gas-reporter@^1.0.4: ...@@ -8006,7 +8014,7 @@ hardhat-gas-reporter@^1.0.4:
eth-gas-reporter "^0.2.20" eth-gas-reporter "^0.2.20"
sha1 "^1.1.1" sha1 "^1.1.1"
hardhat@^2.2.1, hardhat@^2.3.0, hardhat@^2.4.0: hardhat@^2.2.1, hardhat@^2.3.0:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.4.1.tgz#2cd1e86ee6ca3a6a473eeb0f55bd3124c8c59250" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.4.1.tgz#2cd1e86ee6ca3a6a473eeb0f55bd3124c8c59250"
integrity sha512-vwllrFypukeE/Q+4ZfWj7j7nUo4ncUhRpsAYUM0Ruuuk6pQlKmRa0A6c0kxRSvvVgQsMud6j+/weYhbMX1wPmQ== integrity sha512-vwllrFypukeE/Q+4ZfWj7j7nUo4ncUhRpsAYUM0Ruuuk6pQlKmRa0A6c0kxRSvvVgQsMud6j+/weYhbMX1wPmQ==
......
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