Skip to content

Commit

Permalink
Merge pull request #135 from defi-wonderland/dev
Browse files Browse the repository at this point in the history
chore: version 2.1.1
  • Loading branch information
0xGorilla authored Jul 7, 2022
2 parents f09ddab + 823b0ea commit 66f6c6f
Show file tree
Hide file tree
Showing 17 changed files with 201 additions and 18 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

### [2.1.1](https://github.com/defi-wonderland/smock/compare/v2.1.0...v2.1.1) (2022-07-07)


### Features

* return hardhat errors on factory failures ([#125](https://github.com/defi-wonderland/smock/issues/125)) ([3b626a3](https://github.com/defi-wonderland/smock/commit/3b626a3891c07ba224d2f12386bae95522843e2b))


### Bug Fixes

* delegated calls support ([#132](https://github.com/defi-wonderland/smock/issues/132)) ([812c335](https://github.com/defi-wonderland/smock/commit/812c3354d77603d8a6db172f5297c8e49625a98b))

## [2.1.0](https://github.com/defi-wonderland/smock/compare/v2.0.7...v2.1.0) (2022-06-24)


Expand Down
14 changes: 14 additions & 0 deletions docs/source/fakes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,17 @@ Modifying the :code:`receive` function
.. code-block:: typescript
myFake.receive.returns();
Delegated calls
***************

Calls to a contract function via delegated calls do behave the same as a regular call, so you can enforce a return value, assert the calls details, etc...
In addition, you also have custom assertions for delegated calls.

Assert delegated caller
#######################

.. code-block:: typescript
expect(myFake.myFunction).to.be.delegatedFrom(myProxy.address);
2 changes: 1 addition & 1 deletion docs/source/mocks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Setting the value of a struct
Setting the value of a mapping (won't affect other keys)
#######################################################
########################################################

.. code-block:: typescript
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@defi-wonderland/smock",
"version": "2.1.0",
"version": "2.1.1",
"description": "The Solidity mocking library",
"keywords": [
"ethereum",
Expand Down Expand Up @@ -74,6 +74,7 @@
"@typechain/ethers-v5": "7.0.1",
"@typechain/hardhat": "2.0.2",
"@types/chai": "4.2.18",
"@types/chai-as-promised": "7.1.5",
"@types/diff": "^5.0.1",
"@types/lodash": "4.14.170",
"@types/lodash.isequal": "^4.5.5",
Expand All @@ -82,6 +83,7 @@
"@types/node": "15.12.2",
"@types/semver": "^7.3.8",
"chai": "4.3.4",
"chai-as-promised": "7.1.1",
"cross-env": "7.0.3",
"ethereum-waffle": "3.4.0",
"ethers": "5.4.1",
Expand Down
1 change: 1 addition & 0 deletions src/chai-plugin/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,5 @@ export const matchers: Chai.ChaiPlugin = (chai: Chai.ChaiStatic, utils: Chai.Cha
smockMethodWithWatchableContractArg('calledImmediatelyAfter', 'been called immediately after %1');
smockMethod('calledWith', 'been called with arguments %*', '%D');
smockMethod('calledOnceWith', 'been called exactly once with arguments %*', '%D');
smockMethod('delegatedFrom', 'been called via a delegated call by %*', '');
};
4 changes: 4 additions & 0 deletions src/chai-plugin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ declare global {
* Returns true when called at exactly once with the provided arguments.
*/
calledOnceWith(...args: any[]): Assertion;
/**
* Returns true when the function call was made via delegatecall by the given address.
*/
delegatedFrom(delegatorAddress: string): Assertion;
}
}
}
51 changes: 39 additions & 12 deletions src/factories/ethers-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,14 @@ import { FakeContractSpec } from '../types';
export async function ethersInterfaceFromSpec(spec: FakeContractSpec): Promise<ethers.utils.Interface> {
if (typeof spec === 'string') {
try {
return new ethers.utils.Interface(spec);
} catch {}

try {
return (await (hre as any).ethers.getContractFactory(spec)).interface;
} catch {}

try {
return (await (hre as any).ethers.getContractAt(spec, ethers.constants.AddressZero)).interface;
} catch {}

throw new Error(`unable to generate smock spec from string`);
if (isMaybeJsonObject(spec)) {
return await ethersInterfaceFromAbi(spec);
} else {
return await ethersInterfaceFromContractName(spec);
}
} catch (err) {
throw err;
}
}

let foundInterface: any = spec;
Expand All @@ -32,3 +28,34 @@ export async function ethersInterfaceFromSpec(spec: FakeContractSpec): Promise<e
return new ethers.utils.Interface(foundInterface);
}
}

async function ethersInterfaceFromAbi(abi: string): Promise<ethers.utils.Interface> {
try {
return new ethers.utils.Interface(abi);
} catch (err) {
const error: Error = err as Error;
throw new Error(`unable to generate smock spec from abi string.\n${error.message}`);
}
}

async function ethersInterfaceFromContractName(contractNameOrFullyQualifiedName: string): Promise<ethers.utils.Interface> {
let error: Error | null = null;
try {
return (await (hre as any).ethers.getContractFactory(contractNameOrFullyQualifiedName)).interface;
} catch (err) {
error = err as Error;
}

try {
return (await (hre as any).ethers.getContractAt(contractNameOrFullyQualifiedName, ethers.constants.AddressZero)).interface;
} catch (err) {
error = err as Error;
}

throw new Error(`unable to generate smock spec from contract name.\n${error.message}`);
}

function isMaybeJsonObject(str: string): boolean {
let strJson = str.trim();
return strJson.charAt(0) == '{' && strJson.charAt(strJson.length - 1) == '}';
}
8 changes: 6 additions & 2 deletions src/factories/smock-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ function parseAndFilterBeforeMessages(
}
}),
// Ensure the message is directed to this contract
filter((message) => message.to.toString().toLowerCase() === contractAddress.toLowerCase()),
filter((message) => {
const target = message.delegatecall ? message.codeAddress : message.to;
return target.toString().toLowerCase() === contractAddress.toLowerCase();
}),
map((message) => parseMessage(message, contractInterface, sighash)),
share()
);
Expand Down Expand Up @@ -199,7 +202,8 @@ function parseMessage(message: Message, contractInterface: Interface, sighash: s
return {
args: sighash === null ? toHexString(message.data) : getMessageArgs(message.data, contractInterface, sighash),
nonce: Sandbox.getNextNonce(),
target: fromFancyAddress(message.delegatecall ? message._codeAddress : message.to),
target: fromFancyAddress(message.delegatecall ? message.codeAddress : message.to),
delegatedFrom: message.delegatecall ? fromFancyAddress(message.to) : undefined,
};
}

Expand Down
4 changes: 4 additions & 0 deletions src/logic/watchable-function-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export class WatchableFunctionLogic {
return this.getCalledOnce() && this.calledWith(...expectedCallArgs);
}

delegatedFrom(delegatorAddress: string): boolean {
return !!this.callHistory.find((call) => call.delegatedFrom?.toLowerCase() === delegatorAddress.toLowerCase());
}

calledBefore(anotherWatchableContract: WatchableFunctionLogic): boolean {
return this.compareWatchableContractNonces(
this,
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface ContractCall {
args: unknown[] | string;
nonce: number;
target: string;
delegatedFrom?: string;
}

export interface SetVariablesType {
Expand Down
10 changes: 10 additions & 0 deletions test/contracts/programmable-function-logic/Delegator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract Delegator {
function delegateGetBoolean(address _addy) external returns (bool) {
// solhint-disable-next-line avoid-low-level-calls
(, bytes memory _data) = _addy.delegatecall(abi.encodeWithSignature('getBoolean()'));
return abi.decode(_data, (bool));
}
}
26 changes: 26 additions & 0 deletions test/contracts/watchable-function-logic/Storage.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract Storage {
uint256 number;

/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}

/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256) {
return number;
}
}
26 changes: 26 additions & 0 deletions test/contracts/watchable-function-logic/StorageDuplicate.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract Storage {
uint256 number;

/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}

/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256) {
return number;
}
}
20 changes: 19 additions & 1 deletion test/unit/fake/initialization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { makeRandomAddress } from '@src/utils';
import { Receiver, Receiver__factory, Returner } from '@typechained';
import receiverArtifact from 'artifacts/test/contracts/watchable-function-logic/Receiver.sol/Receiver.json';
import chai, { expect } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { ethers, network } from 'hardhat';
import storageArtifact from 'test/unit/fake/testdata/Storage.json';

chai.use(smock.matchers);
chai.use(chaiAsPromised);

describe('Fake: Initialization', () => {
it('should work with the contract name', async () => {
Expand Down Expand Up @@ -90,4 +91,21 @@ describe('Fake: Initialization', () => {
const fake = await smock.fake(storageArtifact);
expect(fake.store._watchable).not.to.be.undefined;
});

it('should throw error for invalid json string abi', async () => {
await expect(smock.fake(`{invalid}`)).to.be.rejectedWith(
Error,
`unable to generate smock spec from abi string.\nUnexpected token i in JSON at position 1`
);
});

it('should throw error for non existent contract', async () => {
let regex: RegExp = /unable to generate smock spec from contract name*/;
await expect(smock.fake('NonExistentContract')).to.be.rejectedWith(Error, regex);
});

it('should throw error if contract name is ambiguous', async () => {
let regex: RegExp = /unable to generate smock spec from contract name*/;
await expect(smock.fake('Storage')).to.be.rejectedWith(Error, regex);
});
});
16 changes: 15 additions & 1 deletion test/unit/programmable-function-logic/type-handling.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FakeContract, smock } from '@src';
import { convertStructToPojo } from '@src/utils';
import { Returner } from '@typechained';
import { Delegator__factory, Returner } from '@typechained';
import chai, { expect } from 'chai';
import { BigNumber, utils } from 'ethers';
import { ethers } from 'hardhat';
Expand Down Expand Up @@ -54,6 +54,20 @@ describe('ProgrammableFunctionLogic: Type Handling', () => {

expect(await fake.callStatic.getBytes32()).to.equal(expected);
});

it('should be able to return a boolean through a delegatecall', async () => {
const delegatorFactory = (await ethers.getContractFactory('Delegator')) as Delegator__factory;
const delegator = await delegatorFactory.deploy();

const expected = true;
fake.getBoolean.returns(expected);

const result = await delegator.callStatic.delegateGetBoolean(fake.address);

expect(result).to.equal(expected);
expect(fake.getBoolean).to.be.calledOnce;
expect(fake.getBoolean).to.be.delegatedFrom(delegator.address);
});
});
});

Expand Down
6 changes: 6 additions & 0 deletions test/unit/watchable-function-logic/misc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ describe('WatchableFunctionLogic: Miscellaneous', () => {
fake.receiveBoolean.should.always.have.been.calledOnceWith(true);
}).to.throw('always flag is not supported for method calledOnceWith');
});

it('should throw when expecting a delegatedFrom that did not happen', async () => {
expect(() => {
fake.receiveBoolean.should.be.delegatedFrom(fake.address);
}).to.throw(`expected receiveBoolean to have been called via a delegated call by '${fake.address}'`);
});
});
14 changes: 14 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,13 @@
dependencies:
"@types/node" "*"

"@types/[email protected]":
version "7.1.5"
resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz#6e016811f6c7a64f2eed823191c3a6955094e255"
integrity sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ==
dependencies:
"@types/chai" "*"

"@types/chai@*":
version "4.2.21"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.21.tgz#9f35a5643129df132cf3b5c1ec64046ea1af0650"
Expand Down Expand Up @@ -2417,6 +2424,13 @@ caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=

[email protected]:
version "7.1.1"
resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.1.tgz#08645d825deb8696ee61725dbf590c012eb00ca0"
integrity sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==
dependencies:
check-error "^1.0.2"

[email protected]:
version "4.3.4"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49"
Expand Down

0 comments on commit 66f6c6f

Please sign in to comment.