From b94ea74f1d5e9104208bc0468e8a4dc7330f519b Mon Sep 17 00:00:00 2001 From: Foivos Date: Thu, 19 Oct 2023 08:59:48 +0300 Subject: [PATCH] feat: add ITS utils (#84) * Add an implementation contract that can protect it's function unleass proxied. * Added a multicall file. * Added a contract that can prevent re-entrancy * feat(utils): more utils * test(utils): most utils coverage * style(utils): prettier * test(utils): more utils coverage * refactor(Implementation): dependencies * style(solidity): prettier * refactor(utils): naming convention and tests * refactor(Paused): param name * Update test/utils/ReentrancyGuard.js * move setup to IImplementation --------- Co-authored-by: Kiryl Yermakou Co-authored-by: Milap Sheth --- .gitignore | 1 + contracts/interfaces/IAxelarGateway.sol | 6 +- contracts/interfaces/IImplementation.sol | 11 +++ contracts/interfaces/IMulticall.sol | 21 ++++++ contracts/interfaces/IPausable.sol | 22 ++++++ contracts/interfaces/IReentrancyGuard.sol | 12 ++++ contracts/interfaces/IUpgradable.sol | 7 +- contracts/libs/AddressBytes.sol | 38 ++++++++++ contracts/test/libs/TestAddressBytes.sol | 18 +++++ contracts/test/mocks/MockGateway.sol | 4 ++ .../test/upgradable/TestImplementation.sol | 17 +++++ contracts/test/utils/TestMulticall.sol | 32 +++++++++ contracts/test/utils/TestPausable.sol | 21 ++++++ contracts/test/utils/TestReentrancyGuard.sol | 21 ++++++ contracts/upgradable/Implementation.sol | 38 ++++++++++ contracts/upgradable/Upgradable.sol | 20 ++---- contracts/utils/Multicall.sol | 38 ++++++++++ contracts/utils/Pausable.sol | 69 +++++++++++++++++++ contracts/utils/ReentrancyGuard.sol | 48 +++++++++++++ test/utils.js | 9 +++ test/utils/AddressBytes.js | 40 +++++++++++ test/utils/Implementation.js | 44 ++++++++++++ test/utils/Multicall.js | 69 +++++++++++++++++++ test/utils/Pausable.js | 44 ++++++++++++ test/utils/ReentrancyGuard.js | 25 +++++++ 25 files changed, 651 insertions(+), 24 deletions(-) create mode 100644 contracts/interfaces/IImplementation.sol create mode 100644 contracts/interfaces/IMulticall.sol create mode 100644 contracts/interfaces/IPausable.sol create mode 100644 contracts/interfaces/IReentrancyGuard.sol create mode 100644 contracts/libs/AddressBytes.sol create mode 100644 contracts/test/libs/TestAddressBytes.sol create mode 100644 contracts/test/upgradable/TestImplementation.sol create mode 100644 contracts/test/utils/TestMulticall.sol create mode 100644 contracts/test/utils/TestPausable.sol create mode 100644 contracts/test/utils/TestReentrancyGuard.sol create mode 100644 contracts/upgradable/Implementation.sol create mode 100644 contracts/utils/Multicall.sol create mode 100644 contracts/utils/Pausable.sol create mode 100644 contracts/utils/ReentrancyGuard.sol create mode 100644 test/utils/AddressBytes.js create mode 100644 test/utils/Implementation.js create mode 100644 test/utils/Multicall.js create mode 100644 test/utils/Pausable.js create mode 100644 test/utils/ReentrancyGuard.js diff --git a/.gitignore b/.gitignore index 4e1d2755..9cb11a31 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,4 @@ dist # Build artifacts /interfaces +temp-arguments.js diff --git a/contracts/interfaces/IAxelarGateway.sol b/contracts/interfaces/IAxelarGateway.sol index 2d9a672e..f7474132 100644 --- a/contracts/interfaces/IAxelarGateway.sol +++ b/contracts/interfaces/IAxelarGateway.sol @@ -3,14 +3,14 @@ pragma solidity ^0.8.0; import { IGovernable } from './IGovernable.sol'; +import { IImplementation } from './IImplementation.sol'; -interface IAxelarGateway is IGovernable { +interface IAxelarGateway is IImplementation, IGovernable { /**********\ |* Errors *| \**********/ error NotSelf(); - error NotProxy(); error InvalidCodeHash(); error SetupFailed(); error InvalidAuthModule(); @@ -194,7 +194,5 @@ interface IAxelarGateway is IGovernable { |* External Functions *| \**********************/ - function setup(bytes calldata params) external; - function execute(bytes calldata input) external; } diff --git a/contracts/interfaces/IImplementation.sol b/contracts/interfaces/IImplementation.sol new file mode 100644 index 00000000..037b3ce8 --- /dev/null +++ b/contracts/interfaces/IImplementation.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IContractIdentifier } from './IContractIdentifier.sol'; + +interface IImplementation is IContractIdentifier { + error NotProxy(); + + function setup(bytes calldata data) external; +} diff --git a/contracts/interfaces/IMulticall.sol b/contracts/interfaces/IMulticall.sol new file mode 100644 index 00000000..65aaec77 --- /dev/null +++ b/contracts/interfaces/IMulticall.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @title IMulticall + * @notice This contract is a multi-functional smart contract which allows for multiple + * contract calls in a single transaction. + */ +interface IMulticall { + error MulticallFailed(); + + /** + * @notice Performs multiple delegate calls and returns the results of all calls as an array + * @dev This function requires that the contract has sufficient balance for the delegate calls. + * If any of the calls fail, the function will revert with the failure message. + * @param data An array of encoded function calls + * @return results An bytes array with the return data of each function call + */ + function multicall(bytes[] calldata data) external payable returns (bytes[] memory results); +} diff --git a/contracts/interfaces/IPausable.sol b/contracts/interfaces/IPausable.sol new file mode 100644 index 00000000..d7ee104d --- /dev/null +++ b/contracts/interfaces/IPausable.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @title Pausable + * @notice This contract provides a mechanism to halt the execution of specific functions + * if a pause condition is activated. + */ +interface IPausable { + event Paused(address indexed account); + event Unpaused(address indexed account); + + error Pause(); + error NotPaused(); + + /** + * @notice Check if the contract is paused + * @return paused A boolean representing the pause status. True if paused, false otherwise. + */ + function paused() external view returns (bool); +} diff --git a/contracts/interfaces/IReentrancyGuard.sol b/contracts/interfaces/IReentrancyGuard.sol new file mode 100644 index 00000000..b7b4dae7 --- /dev/null +++ b/contracts/interfaces/IReentrancyGuard.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @title ReentrancyGuard + * @notice This contract provides a mechanism to halt the execution of specific functions + * if a pause condition is activated. + */ +interface IReentrancyGuard { + error ReentrantCall(); +} diff --git a/contracts/interfaces/IUpgradable.sol b/contracts/interfaces/IUpgradable.sol index c30e7e07..0ef082fb 100644 --- a/contracts/interfaces/IUpgradable.sol +++ b/contracts/interfaces/IUpgradable.sol @@ -3,14 +3,13 @@ pragma solidity ^0.8.0; import { IOwnable } from './IOwnable.sol'; -import { IContractIdentifier } from './IContractIdentifier.sol'; +import { IImplementation } from './IImplementation.sol'; // General interface for upgradable contracts -interface IUpgradable is IOwnable, IContractIdentifier { +interface IUpgradable is IOwnable, IImplementation { error InvalidCodeHash(); error InvalidImplementation(); error SetupFailed(); - error NotProxy(); event Upgraded(address indexed newImplementation); @@ -21,6 +20,4 @@ interface IUpgradable is IOwnable, IContractIdentifier { bytes32 newImplementationCodeHash, bytes calldata params ) external; - - function setup(bytes calldata data) external; } diff --git a/contracts/libs/AddressBytes.sol b/contracts/libs/AddressBytes.sol new file mode 100644 index 00000000..2ae2c530 --- /dev/null +++ b/contracts/libs/AddressBytes.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @title AddressBytesUtils + * @dev This library provides utility functions to convert between `address` and `bytes`. + */ +library AddressBytes { + error InvalidBytesLength(bytes bytesAddress); + + /** + * @dev Converts a bytes address to an address type. + * @param bytesAddress The bytes representation of an address + * @return addr The converted address + */ + function toAddress(bytes memory bytesAddress) internal pure returns (address addr) { + if (bytesAddress.length != 20) revert InvalidBytesLength(bytesAddress); + + assembly { + addr := mload(add(bytesAddress, 20)) + } + } + + /** + * @dev Converts an address to bytes. + * @param addr The address to be converted + * @return bytesAddress The bytes representation of the address + */ + function toBytes(address addr) internal pure returns (bytes memory bytesAddress) { + bytesAddress = new bytes(20); + // we can test if using a single 32 byte variable that is the address with the length together and using one mstore would be slightly cheaper. + assembly { + mstore(add(bytesAddress, 20), addr) + mstore(bytesAddress, 20) + } + } +} diff --git a/contracts/test/libs/TestAddressBytes.sol b/contracts/test/libs/TestAddressBytes.sol new file mode 100644 index 00000000..dcb34cdb --- /dev/null +++ b/contracts/test/libs/TestAddressBytes.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { AddressBytes } from '../../libs/AddressBytes.sol'; + +contract TestAddressBytes { + using AddressBytes for address; + using AddressBytes for bytes; + + function toAddress(bytes memory bytesAddress) external pure returns (address addr) { + return bytesAddress.toAddress(); + } + + function toBytes(address addr) external pure returns (bytes memory bytesAddress) { + return addr.toBytes(); + } +} diff --git a/contracts/test/mocks/MockGateway.sol b/contracts/test/mocks/MockGateway.sol index 659a75f4..56323643 100644 --- a/contracts/test/mocks/MockGateway.sol +++ b/contracts/test/mocks/MockGateway.sol @@ -571,4 +571,8 @@ contract MockGateway is IAxelarGateway { bytes32 newImplementationCodeHash, bytes calldata setupParams ) external override {} + + function contractId() external pure override returns (bytes32) { + return keccak256('MockGateway'); + } } diff --git a/contracts/test/upgradable/TestImplementation.sol b/contracts/test/upgradable/TestImplementation.sol new file mode 100644 index 00000000..5f58bbcb --- /dev/null +++ b/contracts/test/upgradable/TestImplementation.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { Implementation } from '../../upgradable/Implementation.sol'; + +contract TestImplementation is Implementation { + uint256 public val; + + function setup(bytes calldata params) external override onlyProxy { + val = abi.decode(params, (uint256)); + } + + function contractId() external pure override returns (bytes32) { + return keccak256('TestImplementation'); + } +} diff --git a/contracts/test/utils/TestMulticall.sol b/contracts/test/utils/TestMulticall.sol new file mode 100644 index 00000000..74d05e0f --- /dev/null +++ b/contracts/test/utils/TestMulticall.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { Multicall } from '../../utils/Multicall.sol'; + +contract TestMulticall is Multicall { + uint256 public nonce; + bytes[] public lastMulticallReturns; + event Function1Called(uint256 nonce_); + event Function2Called(uint256 nonce_); + + function function1() external returns (uint256) { + uint256 nonce_ = nonce++; + emit Function1Called(nonce_); + return nonce_; + } + + function function2() external returns (uint256) { + uint256 nonce_ = nonce++; + emit Function2Called(nonce_); + return nonce_; + } + + function multicallTest(bytes[] calldata data) external { + lastMulticallReturns = multicall(data); + } + + function getLastMulticallReturns() external view returns (bytes[] memory r) { + return lastMulticallReturns; + } +} diff --git a/contracts/test/utils/TestPausable.sol b/contracts/test/utils/TestPausable.sol new file mode 100644 index 00000000..1677fb4c --- /dev/null +++ b/contracts/test/utils/TestPausable.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { Pausable } from '../../utils/Pausable.sol'; + +contract TestPausable is Pausable { + event TestEvent(); + + function pause() external { + _pause(); + } + + function unpause() external { + _unpause(); + } + + function testPaused() external whenNotPaused { + emit TestEvent(); + } +} diff --git a/contracts/test/utils/TestReentrancyGuard.sol b/contracts/test/utils/TestReentrancyGuard.sol new file mode 100644 index 00000000..f5c6cb0b --- /dev/null +++ b/contracts/test/utils/TestReentrancyGuard.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { ReentrancyGuard } from '../../utils/ReentrancyGuard.sol'; + +contract TestReentrancyGuard is ReentrancyGuard { + uint256 public value; + + constructor() { + require(ENTERED_SLOT == uint256(keccak256('ReentrancyGuard:entered')) - 1, 'invalid constant'); + } + + function testFunction() external noReEntrancy { + value = 1; + this.callback(); + value = 2; + } + + function callback() external noReEntrancy {} +} diff --git a/contracts/upgradable/Implementation.sol b/contracts/upgradable/Implementation.sol new file mode 100644 index 00000000..2ae3fd09 --- /dev/null +++ b/contracts/upgradable/Implementation.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IImplementation } from '../interfaces/IImplementation.sol'; + +/** + * @title Implementation + * @notice This contract serves as a base for other contracts and enforces a proxy-first access restriction. + * @dev Derived contracts must implement the setup function. + */ +abstract contract Implementation is IImplementation { + address private immutable implementationAddress; + + /** + * @dev Contract constructor that sets the implementation address to the address of this contract. + */ + constructor() { + implementationAddress = address(this); + } + + /** + * @dev Modifier to require the caller to be the proxy contract. + * Reverts if the caller is the current contract (i.e., the implementation contract itself). + */ + modifier onlyProxy() { + if (implementationAddress == address(this)) revert NotProxy(); + _; + } + + /** + * @notice Initializes contract parameters. + * This function is intended to be overridden by derived contracts. + * The overriding function must have the onlyProxy modifier. + * @param params The parameters to be used for initialization + */ + function setup(bytes calldata params) external virtual; +} diff --git a/contracts/upgradable/Upgradable.sol b/contracts/upgradable/Upgradable.sol index 0267422a..d3468a7d 100644 --- a/contracts/upgradable/Upgradable.sol +++ b/contracts/upgradable/Upgradable.sol @@ -2,17 +2,18 @@ pragma solidity ^0.8.0; +import { IImplementation } from '../interfaces/IImplementation.sol'; import { IUpgradable } from '../interfaces/IUpgradable.sol'; import { Ownable } from '../utils/Ownable.sol'; +import { Implementation } from './Implementation.sol'; /** * @title Upgradable Contract * @notice This contract provides an interface for upgradable smart contracts and includes the functionality to perform upgrades. */ -abstract contract Upgradable is Ownable, IUpgradable { +abstract contract Upgradable is Ownable, Implementation, IUpgradable { // bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - address internal immutable implementationAddress; /** * @notice Constructor sets the implementation address to the address of the contract itself @@ -21,18 +22,7 @@ abstract contract Upgradable is Ownable, IUpgradable { * @dev The owner is initially set as address(1) because the actual owner is set within the proxy. It is not * set as the zero address because Ownable is designed to throw an error for ownership transfers to the zero address. */ - constructor() Ownable(address(1)) { - implementationAddress = address(this); - } - - /** - * @notice Modifier to ensure that a function can only be called by the proxy - */ - modifier onlyProxy() { - // Prevent setup from being called on the implementation - if (address(this) == implementationAddress) revert NotProxy(); - _; - } + constructor() Ownable(address(1)) {} /** * @notice Returns the address of the current implementation @@ -80,7 +70,7 @@ abstract contract Upgradable is Ownable, IUpgradable { * @param data Initialization data for the contract * @dev This function is only callable by the proxy contract. */ - function setup(bytes calldata data) external override onlyProxy { + function setup(bytes calldata data) external override(IImplementation, Implementation) onlyProxy { _setup(data); } diff --git a/contracts/utils/Multicall.sol b/contracts/utils/Multicall.sol new file mode 100644 index 00000000..c97a3ba4 --- /dev/null +++ b/contracts/utils/Multicall.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IMulticall } from '../interfaces/IMulticall.sol'; + +/** + * @title Multicall + * @notice This contract is a multi-functional smart contract which allows for multiple + * contract calls in a single transaction. + */ +contract Multicall is IMulticall { + /** + * @notice Performs multiple delegate calls and returns the results of all calls as an array + * @dev This function requires that the contract has sufficient balance for the delegate calls. + * If any of the calls fail, the function will revert with the failure message. + * @param data An array of encoded function calls + * @return results An bytes array with the return data of each function call + */ + function multicall(bytes[] calldata data) public payable returns (bytes[] memory results) { + results = new bytes[](data.length); + bool success; + bytes memory result; + for (uint256 i = 0; i < data.length; ++i) { + // slither-disable-next-line calls-loop,delegatecall-loop + (success, result) = address(this).delegatecall(data[i]); + + if (!success) { + if (result.length == 0) revert MulticallFailed(); + assembly { + revert(add(32, result), mload(result)) + } + } + + results[i] = result; + } + } +} diff --git a/contracts/utils/Pausable.sol b/contracts/utils/Pausable.sol new file mode 100644 index 00000000..407a2bf3 --- /dev/null +++ b/contracts/utils/Pausable.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IPausable } from '../interfaces/IPausable.sol'; + +/** + * @title Pausable + * @notice This contract provides a mechanism to halt the execution of specific functions + * if a pause condition is activated. + */ +contract Pausable is IPausable { + // uint256(keccak256('paused')) - 1 + uint256 internal constant PAUSE_SLOT = 0xee35723ac350a69d2a92d3703f17439cbaadf2f093a21ba5bf5f1a53eb2a14d8; + + /** + * @notice A modifier that throws a Paused custom error if the contract is paused + * @dev This modifier should be used with functions that can be paused + */ + modifier whenNotPaused() { + if (paused()) revert Pause(); + _; + } + + modifier whenPaused() { + if (!paused()) revert NotPaused(); + _; + } + + /** + * @notice Check if the contract is paused + * @return paused_ A boolean representing the pause status. True if paused, false otherwise. + */ + function paused() public view returns (bool paused_) { + assembly { + paused_ := sload(PAUSE_SLOT) + } + } + + /** + * @notice Pauses the contract + * @dev This function should be callable by the owner/governance. + */ + function _pause() internal { + _setPaused(true); + emit Paused(msg.sender); + } + + /** + * @notice Unpauses the contract + * @dev This function should be callable by the owner/governance. + */ + function _unpause() internal { + _setPaused(false); + emit Unpaused(msg.sender); + } + + /** + * @notice Sets the pause status of the contract + * @dev This is an internal function, meaning it can only be called from within the contract itself + * or from derived contracts. + * @param paused_ The new pause status + */ + function _setPaused(bool paused_) internal { + assembly { + sstore(PAUSE_SLOT, paused_) + } + } +} diff --git a/contracts/utils/ReentrancyGuard.sol b/contracts/utils/ReentrancyGuard.sol new file mode 100644 index 00000000..298c8bf8 --- /dev/null +++ b/contracts/utils/ReentrancyGuard.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IReentrancyGuard } from '../interfaces/IReentrancyGuard.sol'; + +/** + * @title ReentrancyGuard + * @notice This contract provides a mechanism to halt the execution of specific functions + * if a pause condition is activated. + */ +contract ReentrancyGuard is IReentrancyGuard { + // uint256(keccak256('ReentrancyGuard:entered')) - 1 + uint256 internal constant ENTERED_SLOT = 0x1a771c70cada93a906f955a7dec24a83d7954ba2f75256be4febcf62b395d532; + uint256 internal constant NOT_ENTERED = 1; + uint256 internal constant ENTERED = 2; + + /** + * @notice A modifier that throws a ReEntrancy custom error if the contract is entered + * @dev This modifier should be used with functions that can be entered twice + */ + modifier noReEntrancy() { + if (_hasEntered()) revert ReentrantCall(); + _setEntered(ENTERED); + _; + _setEntered(NOT_ENTERED); + } + + /** + * @notice Check if the contract is already executing. + * @return entered A boolean representing the entered status. True if already executing, false otherwise. + */ + function _hasEntered() internal view returns (bool entered) { + assembly { + entered := eq(sload(ENTERED_SLOT), ENTERED) + } + } + + /** + * @notice Sets the entered status of the contract + * @param entered A boolean representing the entered status. True if already executing, false otherwise. + */ + function _setEntered(uint256 entered) internal { + assembly { + sstore(ENTERED_SLOT, entered) + } + } +} diff --git a/test/utils.js b/test/utils.js index 46d4b322..cab1b896 100644 --- a/test/utils.js +++ b/test/utils.js @@ -63,6 +63,13 @@ const waitFor = async (timeDelay) => { } }; +async function deployContract(wallet, contractName, args = []) { + const factory = await ethers.getContractFactory(contractName, wallet); + const contract = await factory.deploy(...args); + await contract.deployTransaction.wait(network.config.confirmations); + return contract; +} + const expectRevert = async (txFunc, contract, error, args) => { if (network.config.skipRevertTests) { await expect(txFunc(getGasOptions())).to.be.reverted; @@ -111,5 +118,7 @@ module.exports = { waitFor, + deployContract, + expectRevert, }; diff --git a/test/utils/AddressBytes.js b/test/utils/AddressBytes.js new file mode 100644 index 00000000..6fad8b4e --- /dev/null +++ b/test/utils/AddressBytes.js @@ -0,0 +1,40 @@ +'use strict'; + +const { ethers } = require('hardhat'); +const chai = require('chai'); +const { deployContract } = require('../utils'); +const { defaultAbiCoder, arrayify, toUtf8Bytes, hexlify } = ethers.utils; +const { expect } = chai; + +describe('AddressBytes', () => { + let addressBytes; + let ownerWallet; + + before(async () => { + const wallets = await ethers.getSigners(); + ownerWallet = wallets[0]; + + addressBytes = await deployContract(ownerWallet, 'TestAddressBytes'); + }); + + it('Should convert bytes address to address', async () => { + const bytesAddress = arrayify(ownerWallet.address); + const convertedAddress = await addressBytes.toAddress(bytesAddress); + expect(convertedAddress).to.eq(ownerWallet.address); + }); + + it('Should revert on invalid bytes length', async () => { + const bytesAddress = defaultAbiCoder.encode( + ['bytes'], + [toUtf8Bytes(ownerWallet.address)], + ); + await expect( + addressBytes.toAddress(bytesAddress), + ).to.be.revertedWithCustomError(addressBytes, 'InvalidBytesLength'); + }); + + it('Should convert address to bytes address', async () => { + const convertedAddress = await addressBytes.toBytes(ownerWallet.address); + expect(convertedAddress).to.eq(hexlify(ownerWallet.address)); + }); +}); diff --git a/test/utils/Implementation.js b/test/utils/Implementation.js new file mode 100644 index 00000000..237cc091 --- /dev/null +++ b/test/utils/Implementation.js @@ -0,0 +1,44 @@ +'use strict'; + +const chai = require('chai'); +const { ethers, network } = require('hardhat'); +const { defaultAbiCoder } = ethers.utils; +const { expect } = chai; +const { deployContract } = require('../utils.js'); + +describe('Implementation', () => { + let implementation, proxy; + let ownerWallet; + + const val = 123; + + before(async () => { + const wallets = await ethers.getSigners(); + ownerWallet = wallets[0]; + + implementation = await deployContract(ownerWallet, 'TestImplementation'); + proxy = await deployContract(ownerWallet, 'InitProxy', []); + + const params = defaultAbiCoder.encode(['uint256'], [val]); + await proxy + .init(implementation.address, ownerWallet.address, params) + .then((d) => d.wait(network.config.confirmations)); + + const factory = await ethers.getContractFactory( + 'TestImplementation', + ownerWallet, + ); + + proxy = factory.attach(proxy.address); + }); + + it('Should test the implementation contract', async () => { + expect(await proxy.val()).to.equal(val); + + const params = defaultAbiCoder.encode(['uint256'], [val]); + await expect(implementation.setup(params)).to.be.revertedWithCustomError( + implementation, + 'NotProxy', + ); + }); +}); diff --git a/test/utils/Multicall.js b/test/utils/Multicall.js new file mode 100644 index 00000000..384373bb --- /dev/null +++ b/test/utils/Multicall.js @@ -0,0 +1,69 @@ +'use strict'; + +const chai = require('chai'); +const { ethers } = require('hardhat'); +const { defaultAbiCoder } = ethers.utils; +const { expect } = chai; +const { deployContract } = require('../utils.js'); + +describe('Mutlicall', () => { + let test; + let function1Data; + let function2Data; + let ownerWallet; + + before(async () => { + const wallets = await ethers.getSigners(); + ownerWallet = wallets[0]; + + test = await deployContract(ownerWallet, 'TestMulticall'); + function1Data = (await test.populateTransaction.function1()).data; + function2Data = (await test.populateTransaction.function2()).data; + }); + + it('Shoult test the multicall', async () => { + const nonce = Number(await test.nonce()); + await expect( + test.multicall([ + function1Data, + function2Data, + function2Data, + function1Data, + ]), + ) + .to.emit(test, 'Function1Called') + .withArgs(nonce) + .and.to.emit(test, 'Function2Called') + .withArgs(nonce + 1) + .and.to.emit(test, 'Function2Called') + .withArgs(nonce + 2) + .and.to.emit(test, 'Function1Called') + .withArgs(nonce + 3); + }); + + it('Shoult test the multicall returns', async () => { + const nonce = Number(await test.nonce()); + await expect( + test.multicallTest([ + function2Data, + function1Data, + function2Data, + function2Data, + ]), + ) + .to.emit(test, 'Function2Called') + .withArgs(nonce) + .and.to.emit(test, 'Function1Called') + .withArgs(nonce + 1) + .and.to.emit(test, 'Function2Called') + .withArgs(nonce + 2) + .and.to.emit(test, 'Function2Called') + .withArgs(nonce + 3); + const lastReturns = await test.getLastMulticallReturns(); + + for (let i = 0; i < lastReturns.length; i++) { + const val = Number(defaultAbiCoder.decode(['uint256'], lastReturns[i])); + expect(val).to.equal(nonce + i); + } + }); +}); diff --git a/test/utils/Pausable.js b/test/utils/Pausable.js new file mode 100644 index 00000000..dd54a2c7 --- /dev/null +++ b/test/utils/Pausable.js @@ -0,0 +1,44 @@ +'use strict'; + +const chai = require('chai'); +const { ethers } = require('hardhat'); +const { expect } = chai; +const { deployContract } = require('../utils.js'); + +describe('Pausable', () => { + let test; + let ownerWallet; + + before(async () => { + const wallets = await ethers.getSigners(); + ownerWallet = wallets[0]; + + test = await deployContract(ownerWallet, 'TestPausable'); + }); + + it('Should be able to set paused to true or false', async () => { + await expect(test.pause()) + .to.emit(test, 'Paused') + .withArgs(ownerWallet.address); + expect(await test.paused()).to.equal(true); + await expect(test.unpause()) + .to.emit(test, 'Unpaused') + .withArgs(ownerWallet.address); + expect(await test.paused()).to.equal(false); + }); + + it('Should be able to execute notPaused functions only when not paused', async () => { + await expect(test.pause()) + .to.emit(test, 'Paused') + .withArgs(ownerWallet.address); + await expect(test.testPaused()).to.be.revertedWithCustomError( + test, + 'Pause', + ); + + await expect(test.unpause()) + .to.emit(test, 'Unpaused') + .withArgs(ownerWallet.address); + await expect(test.testPaused()).to.emit(test, 'TestEvent'); + }); +}); diff --git a/test/utils/ReentrancyGuard.js b/test/utils/ReentrancyGuard.js new file mode 100644 index 00000000..1f5fc603 --- /dev/null +++ b/test/utils/ReentrancyGuard.js @@ -0,0 +1,25 @@ +'use strict'; + +const chai = require('chai'); +const { ethers } = require('hardhat'); +const { deployContract } = require('../utils'); +const { expect } = chai; + +describe('ReentrancyGuard', () => { + let guard; + let ownerWallet; + + before(async () => { + const wallets = await ethers.getSigners(); + ownerWallet = wallets[0]; + + guard = await deployContract(ownerWallet, 'TestReentrancyGuard'); + }); + + it('Should revert on reentrancy', async function () { + await expect(guard.testFunction()).to.be.revertedWithCustomError( + guard, + 'ReentrantCall', + ); + }); +});