From fb6863d0a795db091b925385e1c1c670aa53eedd Mon Sep 17 00:00:00 2001 From: Przemyslaw Rzad Date: Wed, 11 Jan 2023 12:28:34 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8D=B6=20Implement=20mocking=20receive=20?= =?UTF-8?q?function=20to=20revert=20(#807)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/quiet-bugs-jam.md | 5 ++ docs/source/mock-contract.rst | 64 +++++++++++++++++++ waffle-mock-contract/src/Doppelganger.sol | 11 ++++ waffle-mock-contract/src/index.ts | 7 ++ .../test/amirichalready.test.ts | 4 +- .../test/etherForward.test.ts | 64 +++++++++++++++++++ .../test/helpers/contracts/EtherForward.sol | 34 ++++++++++ 7 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 .changeset/quiet-bugs-jam.md create mode 100644 waffle-mock-contract/test/etherForward.test.ts create mode 100644 waffle-mock-contract/test/helpers/contracts/EtherForward.sol diff --git a/.changeset/quiet-bugs-jam.md b/.changeset/quiet-bugs-jam.md new file mode 100644 index 000000000..6c4a6ad5a --- /dev/null +++ b/.changeset/quiet-bugs-jam.md @@ -0,0 +1,5 @@ +--- +"@ethereum-waffle/mock-contract": patch +--- + +🍶 Implement mocking receive function to revert diff --git a/docs/source/mock-contract.rst b/docs/source/mock-contract.rst index ee16adabb..25e458b69 100644 --- a/docs/source/mock-contract.rst +++ b/docs/source/mock-contract.rst @@ -122,3 +122,67 @@ Mock contract will be used to mock exactly this call with values that are releva expect(await contract.connect(receiver.address).check()).to.equal(false); }); }); + +Mocking receive function +------------------------ + +The :code:`receive` function of the mocked Smart Contract can be mocked to revert. It cannot however be mocked to return a specified value, because of gas limitations when calling another contract using :code:`send` and :code:`transfer`. + +Receive mock example +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: solidity + + pragma solidity ^0.6.0; + + interface IERC20 { + function balanceOf(address account) external view returns (uint256); + fallback() external payable; + receive() external payable; + } + + contract EtherForward { + IERC20 private tokenContract; + + constructor (IERC20 _tokenContract) public { + tokenContract = _tokenContract; + } + + function forward() public payable { + payable(tokenContract).transfer(msg.value); + } + } + +.. code-block:: ts + + (...) + + it('use the receive function normally', async () => { + const {contract, mockERC20} = await setup(); + + expect ( + await mockERC20.provider.getBalance(mockERC20.address) + ).to.be.equal(0); + + await contract.forward({value: 7}) + + expect ( + await mockERC20.provider.getBalance(mockERC20.address) + ).to.be.equal(7); + }); + + it('can mock the receive function to revert', async () => { + const {contract, mockERC20} = await setup(); + + await mockERC20.mock.receive.revertsWithReason('Receive function rejected') + + await expect( + contract.forward({value: 7}) + ).to.be.revertedWith('Receive function rejected') + + expect ( + await mockERC20.provider.getBalance(mockERC20.address) + ).to.be.equal(0); + }); + + (...) diff --git a/waffle-mock-contract/src/Doppelganger.sol b/waffle-mock-contract/src/Doppelganger.sol index 066637fdf..25827bcc5 100644 --- a/waffle-mock-contract/src/Doppelganger.sol +++ b/waffle-mock-contract/src/Doppelganger.sol @@ -10,6 +10,8 @@ contract Doppelganger { } mapping(bytes32 => MockCall) mockConfig; + bool receiveReverts; + string receiveRevertReason; fallback() external payable { MockCall storage mockCall = __internal__getMockCall(); @@ -20,6 +22,10 @@ contract Doppelganger { __internal__mockReturn(mockCall.returnValue); } + receive() payable external { + require(receiveReverts == false, receiveRevertReason); + } + function __waffle__mockReverts(bytes memory data, string memory reason) public { mockConfig[keccak256(data)] = MockCall({ initialized: true, @@ -38,6 +44,11 @@ contract Doppelganger { }); } + function __waffle__receiveReverts(string memory reason) public { + receiveReverts = true; + receiveRevertReason = reason; + } + function __waffle__call(address target, bytes calldata data) external returns (bytes memory) { (bool succeeded, bytes memory returnValue) = target.call(data); require(succeeded, string(returnValue)); diff --git a/waffle-mock-contract/src/index.ts b/waffle-mock-contract/src/index.ts index d93b5d294..c037535ac 100644 --- a/waffle-mock-contract/src/index.ts +++ b/waffle-mock-contract/src/index.ts @@ -80,6 +80,13 @@ function createMock(abi: ABI, mockContractInstance: Contract) { }; }, {} as MockContract['mock']); + mockedAbi.receive = { + returns: async () => { throw new Error('Receive function return is not implemented.'); }, + withArgs: () => { throw new Error('Receive function return is not implemented.'); }, + reverts: async () => mockContractInstance.__waffle__receiveReverts('Mock Revert'), + revertsWithReason: async (reason: string) => mockContractInstance.__waffle__receiveReverts(reason) + }; + return mockedAbi; } diff --git a/waffle-mock-contract/test/amirichalready.test.ts b/waffle-mock-contract/test/amirichalready.test.ts index 418c0f7bc..5f248868c 100644 --- a/waffle-mock-contract/test/amirichalready.test.ts +++ b/waffle-mock-contract/test/amirichalready.test.ts @@ -2,7 +2,7 @@ import {use, expect} from 'chai'; import {Contract, ContractFactory, utils, Wallet} from 'ethers'; import {MockProvider} from '@ethereum-waffle/provider'; import {waffleChai} from '@ethereum-waffle/chai'; -import {deployMockContract} from '../src'; +import {deployMockContract, MockContract} from '../src'; import IERC20 from './helpers/interfaces/IERC20.json'; import AmIRichAlready from './helpers/interfaces/AmIRichAlready.json'; @@ -13,7 +13,7 @@ describe('Am I Rich Already', () => { let contractFactory: ContractFactory; let sender: Wallet; let receiver: Wallet; - let mockERC20: Contract; + let mockERC20: MockContract; let contract: Contract; beforeEach(async () => { diff --git a/waffle-mock-contract/test/etherForward.test.ts b/waffle-mock-contract/test/etherForward.test.ts new file mode 100644 index 000000000..14ea6a3d7 --- /dev/null +++ b/waffle-mock-contract/test/etherForward.test.ts @@ -0,0 +1,64 @@ +import {waffleChai} from '@ethereum-waffle/chai'; +import {MockProvider} from '@ethereum-waffle/provider'; +import {expect, use} from 'chai'; +import {Contract, ContractFactory, Wallet} from 'ethers'; +import {deployMockContract} from '../src'; + +import EtherForward from './helpers/interfaces/EtherForward.json'; +import IERC20 from './helpers/interfaces/IERC20.json'; + +use(waffleChai); + +describe('Ether Forwarded', () => { + let contractFactory: ContractFactory; + let sender: Wallet; + let mockERC20: Contract; + let contract: Contract; + let provider: MockProvider; + + beforeEach(async () => { + provider = new MockProvider() + ;[sender] = provider.getWallets(); + mockERC20 = await deployMockContract(sender, IERC20.abi); + contractFactory = new ContractFactory(EtherForward.abi, EtherForward.bytecode, sender); + contract = await contractFactory.deploy(mockERC20.address); + }); + + it('Can forward ether through call', async () => { + expect(await provider.getBalance(mockERC20.address)).to.be.equal(0); + await contract.forwardByCall({value: 7}); + expect(await provider.getBalance(mockERC20.address)).to.be.equal(7); + }); + + it('Can forward ether through send', async () => { + expect(await provider.getBalance(mockERC20.address)).to.be.equal(0); + await contract.forwardBySend({value: 7}); + expect(await provider.getBalance(mockERC20.address)).to.be.equal(7); + }); + + it('Can forward ether through transfer', async () => { + expect(await provider.getBalance(mockERC20.address)).to.be.equal(0); + await contract.forwardByTransfer({value: 7}); + expect(await provider.getBalance(mockERC20.address)).to.be.equal(7); + }); + + it('Can mock a revert on a receive function', async () => { + expect(await provider.getBalance(mockERC20.address)).to.be.equal(0); + + await mockERC20.mock.receive.revertsWithReason('Receive function rejected ether.'); + + await expect( + contract.forwardByCall({value: 7}) + ).to.be.revertedWith('Receive function rejected ether.'); + + await expect( + contract.forwardBySend({value: 7}) + ).to.be.revertedWith('forwardBySend failed'); + + await expect( + contract.forwardByTransfer({value: 7}) + ).to.be.reverted; + + expect(await provider.getBalance(mockERC20.address)).to.be.equal(0); + }); +}); diff --git a/waffle-mock-contract/test/helpers/contracts/EtherForward.sol b/waffle-mock-contract/test/helpers/contracts/EtherForward.sol new file mode 100644 index 000000000..efa03e59c --- /dev/null +++ b/waffle-mock-contract/test/helpers/contracts/EtherForward.sol @@ -0,0 +1,34 @@ +pragma solidity ^0.6.3; + +interface IERC20 { + function balanceOf(address account) external view returns (uint256); + fallback() external payable; + receive() external payable; +} + +contract EtherForward { + IERC20 private tokenContract; + + constructor (IERC20 _tokenContract) public { + tokenContract = _tokenContract; + } + + function forwardByCall() public payable { + (bool sent, bytes memory data) = payable(tokenContract).call{value: msg.value}(""); + if (!sent) { + // https://ethereum.stackexchange.com/a/114140/24330 + // Bubble up the revert from the call. + assembly { + revert(add(data, 32), data) + } + } + } + + function forwardBySend() public payable { + require(payable(tokenContract).send(msg.value), "forwardBySend failed"); + } + + function forwardByTransfer() public payable { + payable(tokenContract).transfer(msg.value); + } +}