From 412bf48829d53a44a3ccaea385249273c6ed82ea Mon Sep 17 00:00:00 2001 From: Krishang Date: Thu, 18 Apr 2024 17:53:11 +0530 Subject: [PATCH 1/8] Update important interfaces naming away from Hooks to Extension + Callbacks --- core/src/interface/ICoreContract.sol | 37 +++++++++++++++++++++++ core/src/interface/IExtensionContract.sol | 15 +++++++++ core/src/interface/IExtensionTypes.sol | 29 ++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 core/src/interface/ICoreContract.sol create mode 100644 core/src/interface/IExtensionContract.sol create mode 100644 core/src/interface/IExtensionTypes.sol diff --git a/core/src/interface/ICoreContract.sol b/core/src/interface/ICoreContract.sol new file mode 100644 index 00000000..439d92d0 --- /dev/null +++ b/core/src/interface/ICoreContract.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {IExtensionTypes} from "./IExtensionTypes.sol"; + +interface ICoreContract is IExtensionTypes { + /*////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + struct CallbackFunction { + uint256 callbackFunctionBitflag; + string callbackFunctionSignature; + address extensionContractImplementation; + } + + struct InstalledExtension { + address extensionContract; + ExtensionFunction[] extensionABI; + uint256 implementedCallbackFunctionsBitmask; + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function getSupportedCallbackFunctionsBitmask() external view returns (uint256); + function getSupportedCallbackFunctions() external view returns (CallbackFunction[] memory); + function getInstalledExtensions() external view returns (InstalledExtension[] memory); + + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function installExtension(address _extensionContract, uint256 _value, bytes calldata _data) external; + function uninstallExtension(address _extensionContract, uint256 _value, bytes calldata _data) external; +} diff --git a/core/src/interface/IExtensionContract.sol b/core/src/interface/IExtensionContract.sol new file mode 100644 index 00000000..9024b01a --- /dev/null +++ b/core/src/interface/IExtensionContract.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {IExtensionTypes} from "./IExtensionTypes.sol"; + +interface IExtensionContract is IExtensionTypes { + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns all extension functions and supported callback functions of an extension contract. + */ + function getExtensionConfig() external pure returns (ExtensionConfig memory); +} diff --git a/core/src/interface/IExtensionTypes.sol b/core/src/interface/IExtensionTypes.sol new file mode 100644 index 00000000..74e15346 --- /dev/null +++ b/core/src/interface/IExtensionTypes.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +interface IExtensionTypes { + /*////////////////////////////////////////////////////////////// + STRUCTS & ENUMS + //////////////////////////////////////////////////////////////*/ + + /// @dev Enum for the type of call to be made to an extension function. + enum CallType { + STATICCALL, + CALL, + DELEGATECALL + } + + /// @dev Struct for an extension function. Installing an extension in a core adds its extension functions to the core's ABI. + struct ExtensionFunction { + bytes4 fnSelector; + string fnSignature; + CallType callType; + bool permissioned; + } + + /// @notice All extension functions and supported callback functions of an extension contract. + struct ExtensionConfig { + uint256 callbackFunctionsBitmask; + ExtensionFunction[] extensionABI; + } +} From a0a752523488bf602d710e6f3e947ba55d63cb7f Mon Sep 17 00:00:00 2001 From: Krishang Date: Fri, 19 Apr 2024 14:56:37 +0530 Subject: [PATCH 2/8] rename fn -> function --- core/src/interface/IExtensionTypes.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/interface/IExtensionTypes.sol b/core/src/interface/IExtensionTypes.sol index 74e15346..e216671f 100644 --- a/core/src/interface/IExtensionTypes.sol +++ b/core/src/interface/IExtensionTypes.sol @@ -15,8 +15,8 @@ interface IExtensionTypes { /// @dev Struct for an extension function. Installing an extension in a core adds its extension functions to the core's ABI. struct ExtensionFunction { - bytes4 fnSelector; - string fnSignature; + bytes4 functionSelector; + string functionSignature; CallType callType; bool permissioned; } From 2d7a1346170e90c29c98b1f1b0cbb58608338e9b Mon Sep 17 00:00:00 2001 From: Krishang Date: Fri, 19 Apr 2024 15:22:11 +0530 Subject: [PATCH 3/8] cleanup redundancy in names --- core/src/interface/ICoreContract.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/interface/ICoreContract.sol b/core/src/interface/ICoreContract.sol index 439d92d0..22a28f84 100644 --- a/core/src/interface/ICoreContract.sol +++ b/core/src/interface/ICoreContract.sol @@ -9,13 +9,13 @@ interface ICoreContract is IExtensionTypes { //////////////////////////////////////////////////////////////*/ struct CallbackFunction { - uint256 callbackFunctionBitflag; - string callbackFunctionSignature; - address extensionContractImplementation; + uint256 bitflag; + string functionSignature; + address implementation; } struct InstalledExtension { - address extensionContract; + address implementation; ExtensionFunction[] extensionABI; uint256 implementedCallbackFunctionsBitmask; } From b89b6ade68f8aebb5582a26c536f990e79d75232 Mon Sep 17 00:00:00 2001 From: Krishang Date: Fri, 19 Apr 2024 21:17:23 +0530 Subject: [PATCH 4/8] WIP no bitmasks --- core/src/interface/IExtensionTypes.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/interface/IExtensionTypes.sol b/core/src/interface/IExtensionTypes.sol index e216671f..3add75e7 100644 --- a/core/src/interface/IExtensionTypes.sol +++ b/core/src/interface/IExtensionTypes.sol @@ -21,9 +21,14 @@ interface IExtensionTypes { bool permissioned; } + struct CallbackFunction { + bytes4 functionSelector; + string functionSignature; + } + /// @notice All extension functions and supported callback functions of an extension contract. struct ExtensionConfig { - uint256 callbackFunctionsBitmask; + CallbackFunction[] supportedCallbackFunctions; ExtensionFunction[] extensionABI; } } From 03e4b705fd6db66b51867dd0532ef5be57d03bf5 Mon Sep 17 00:00:00 2001 From: Krishang Date: Sat, 20 Apr 2024 01:54:17 +0530 Subject: [PATCH 5/8] update interfaces: replace bitmask with bytes4[] --- core/src/interface/ICoreContract.sol | 12 ++---------- core/src/interface/IExtensionTypes.sol | 10 ++-------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/core/src/interface/ICoreContract.sol b/core/src/interface/ICoreContract.sol index 22a28f84..2c001762 100644 --- a/core/src/interface/ICoreContract.sol +++ b/core/src/interface/ICoreContract.sol @@ -8,24 +8,16 @@ interface ICoreContract is IExtensionTypes { STRUCTS //////////////////////////////////////////////////////////////*/ - struct CallbackFunction { - uint256 bitflag; - string functionSignature; - address implementation; - } - struct InstalledExtension { address implementation; - ExtensionFunction[] extensionABI; - uint256 implementedCallbackFunctionsBitmask; + ExtensionConfig config; } /*////////////////////////////////////////////////////////////// VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ - function getSupportedCallbackFunctionsBitmask() external view returns (uint256); - function getSupportedCallbackFunctions() external view returns (CallbackFunction[] memory); + function getSupportedCallbackFunctions() external view returns (bytes4[] memory); function getInstalledExtensions() external view returns (InstalledExtension[] memory); /*////////////////////////////////////////////////////////////// diff --git a/core/src/interface/IExtensionTypes.sol b/core/src/interface/IExtensionTypes.sol index 3add75e7..4a960bd9 100644 --- a/core/src/interface/IExtensionTypes.sol +++ b/core/src/interface/IExtensionTypes.sol @@ -15,20 +15,14 @@ interface IExtensionTypes { /// @dev Struct for an extension function. Installing an extension in a core adds its extension functions to the core's ABI. struct ExtensionFunction { - bytes4 functionSelector; - string functionSignature; + bytes4 selector; CallType callType; bool permissioned; } - struct CallbackFunction { - bytes4 functionSelector; - string functionSignature; - } - /// @notice All extension functions and supported callback functions of an extension contract. struct ExtensionConfig { - CallbackFunction[] supportedCallbackFunctions; + bytes4[] supportedCallbackFunctions; ExtensionFunction[] extensionABI; } } From 43b3ac1f1860f0cbbdcfefde33bf246ad4c36552 Mon Sep 17 00:00:00 2001 From: Krishang Date: Sat, 20 Apr 2024 02:53:00 +0530 Subject: [PATCH 6/8] abstract CoreContract inherits ICoreContract --- .gitmodules | 2 +- core/src/core/CoreContract.sol | 297 +++++++++++++++++++++++++++ core/src/interface/ICoreContract.sol | 2 +- 3 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 core/src/core/CoreContract.sol diff --git a/.gitmodules b/.gitmodules index 948f5070..3439674a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -21,4 +21,4 @@ url = https://github.com/chiru-labs/erc721a [submodule "hooks/lib/murky"] path = hooks/lib/murky - url = https://github.com/dmfxyz/murky + url = https://github.com/dmfxyz/murky \ No newline at end of file diff --git a/core/src/core/CoreContract.sol b/core/src/core/CoreContract.sol new file mode 100644 index 00000000..1ba9fc46 --- /dev/null +++ b/core/src/core/CoreContract.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../interface/ICoreContract.sol"; +import "../interface/IExtensionContract.sol"; + +abstract contract CoreContract is ICoreContract { + /*////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + struct InstalledExtensionFunction { + address implementation; + ExtensionFunction data; + } + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + address[] private extensionImplementation_; + mapping(address => bool) private extensionInstalled_; + mapping(bytes4 => address) private callbackFunctionImplementation_; + mapping(bytes4 => InstalledExtensionFunction) private extensionFunctionData_; + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error UnauthorizedInstall(); + error UnauthorizedExtensionCall(); + error ExtensionInitializationFailed(); + error ExtensionAlreadyInstalled(); + error ExtensionNotInstalled(); + error ExtensionFunctionAlreadyInstalled(); + error CallbackFunctionAlreadyInstalled(); + + /*////////////////////////////////////////////////////////////// + FALLBACK FUNCTION + //////////////////////////////////////////////////////////////*/ + + fallback() external payable { + // Get extension function data. + InstalledExtensionFunction memory extensionFunction = extensionFunctionData_[msg.sig]; + + // Check: extension function data exists. + if (extensionFunction.implementation == address(0)) { + revert ExtensionNotInstalled(); + } + + // Check: authorized to call permissioned extension function + if (extensionFunction.data.permissioned && !_isAuthorizedToCallExtensionFunctions(msg.sender)) { + revert UnauthorizedExtensionCall(); + } + + // Call extension function. + CallType callType = extensionFunction.data.callType; + + if (callType == CallType.CALL) { + _callAndReturn(extensionFunction.implementation, msg.value); + } else if (callType == CallType.DELEGATECALL) { + _delegateAndReturn(extensionFunction.implementation); + } else if (callType == CallType.STATICCALL) { + _staticcallAndReturn(extensionFunction.implementation); + } + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function getInstalledExtensions() + external + view + virtual + returns (InstalledExtension[] memory _installedExtensions) + { + uint256 totalInstalled = extensionImplementation_.length; + + for (uint256 i = 0; i < totalInstalled; i++) { + address implementation = extensionImplementation_[i]; + _installedExtensions[i] = InstalledExtension({ + implementation: implementation, + config: IExtensionContract(implementation).getExtensionConfig() + }); + } + } + + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function installExtension(address _extensionContract, uint256 _value, bytes calldata _data) external { + // Check: authorized to install extensions. + if (!_isAuthorizedToInstallExtensions(msg.sender)) { + revert UnauthorizedInstall(); + } + + // Install extension. + _installExtension(_extensionContract); + + // Initialize extension with external call. + if (_data.length > 0) { + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = _extensionContract.call{value: _value}(_data); + if (!success) { + _revert(returndata, ExtensionInitializationFailed.selector); + } + } + } + + function uninstallExtension(address _extensionContract, uint256 _value, bytes calldata _data) external { + // Check: authorized to install extensions. + if (!_isAuthorizedToInstallExtensions(msg.sender)) { + revert UnauthorizedInstall(); + } + + // Uninstall extension. + uninstallExtension(_extensionContract); + + // Update extension with external call. + if (_data.length > 0) { + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = _extensionContract.call{value: _value}(_data); + if (!success) { + _revert(returndata, ExtensionInitializationFailed.selector); + } + } + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _isAuthorizedToInstallExtensions(address _target) internal view virtual returns (bool); + function _isAuthorizedToCallExtensionFunctions(address _target) internal view virtual returns (bool); + + function _getCallbackFunctionImplementation(bytes4 _selector) internal view virtual returns (address) { + return callbackFunctionImplementation_[_selector]; + } + + function _getExtensionFunctionData(bytes4 _selector) + internal + view + virtual + returns (InstalledExtensionFunction memory) + { + return extensionFunctionData_[_selector]; + } + + function _installExtension(address _extension) internal virtual { + // Check: extension not already installed. + if (extensionInstalled_[_extension]) { + revert ExtensionAlreadyInstalled(); + } + extensionInstalled_[_extension] = true; + extensionImplementation_.push(_extension); + + // Get extension config. + ExtensionConfig memory config = IExtensionContract(_extension).getExtensionConfig(); + + // Store extension function data. + uint256 totalFunctions = config.extensionABI.length; + for (uint256 i = 0; i < totalFunctions; i++) { + ExtensionFunction memory data = config.extensionABI[i]; + + // Check: extension function data not already stored. + if (extensionFunctionData_[data.selector].implementation != address(0)) { + revert ExtensionFunctionAlreadyInstalled(); + } + + extensionFunctionData_[data.selector] = InstalledExtensionFunction({implementation: _extension, data: data}); + } + + // Store callback function data. + uint256 totalCallbacks = config.supportedCallbackFunctions.length; + for (uint256 i = 0; i < totalCallbacks; i++) { + bytes4 callbackFunction = config.supportedCallbackFunctions[i]; + + // Check: callback function data not already stored. + if (callbackFunctionImplementation_[callbackFunction] != address(0)) { + revert CallbackFunctionAlreadyInstalled(); + } + + callbackFunctionImplementation_[callbackFunction] = _extension; + } + } + + function uninstallExtension(address _extension) internal virtual { + // Check: extension installed. + if (!extensionInstalled_[_extension]) { + revert ExtensionNotInstalled(); + } + delete extensionInstalled_[_extension]; + + // Get extension config. + ExtensionConfig memory config = IExtensionContract(_extension).getExtensionConfig(); + + // Remove extension function data. + uint256 totalFunctions = config.extensionABI.length; + for (uint256 i = 0; i < totalFunctions; i++) { + ExtensionFunction memory data = config.extensionABI[i]; + delete extensionFunctionData_[data.selector]; + } + + // Remove callback function data. + uint256 totalCallbacks = config.supportedCallbackFunctions.length; + for (uint256 i = 0; i < totalCallbacks; i++) { + bytes4 callbackFunction = config.supportedCallbackFunctions[i]; + delete callbackFunctionImplementation_[callbackFunction]; + } + } + + /// @dev delegateCalls an `implementation` smart contract. + function _delegateAndReturn(address implementation) internal virtual { + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } + + /// @dev calls an `implementation` smart contract and returns data. + function _staticcallAndReturn(address implementation) internal virtual { + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Staticcall the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := staticcall(gas(), implementation, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // staticcall returns 0 on error. + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } + + /// @dev calls an `implementation` smart contract and returns data. + function _callAndReturn(address implementation, uint256 _value) internal virtual { + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Staticcall the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := call(gas(), implementation, _value, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // staticcall returns 0 on error. + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } + + /// @dev Reverts with the given return data / error message. + function _revert(bytes memory _returndata, bytes4 _errorSignature) internal pure { + // Look for revert reason and bubble it up if present + if (_returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(_returndata) + revert(add(32, _returndata), returndata_size) + } + } else { + assembly { + mstore(0x00, _errorSignature) + revert(0x1c, 0x04) + } + } + } +} diff --git a/core/src/interface/ICoreContract.sol b/core/src/interface/ICoreContract.sol index 2c001762..2d2a842e 100644 --- a/core/src/interface/ICoreContract.sol +++ b/core/src/interface/ICoreContract.sol @@ -17,7 +17,7 @@ interface ICoreContract is IExtensionTypes { VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ - function getSupportedCallbackFunctions() external view returns (bytes4[] memory); + function getSupportedCallbackFunctions() external pure returns (bytes4[] memory); function getInstalledExtensions() external view returns (InstalledExtension[] memory); /*////////////////////////////////////////////////////////////// From 12a787610155e6fe8c40d9adf2b2e2c4b6ea0048 Mon Sep 17 00:00:00 2001 From: Krishang Date: Sat, 20 Apr 2024 04:24:44 +0530 Subject: [PATCH 7/8] ERC20CoreContract inherits ERC20Core --- core/src/core/CoreContract.sol | 72 ++-- core/src/core/token/ERC20CoreContract.sol | 271 ++++++++++++ core/src/interface/IExtensionTypes.sol | 2 +- core/test/core/ERC20CoreContract.t.sol | 503 ++++++++++++++++++++++ core/test/mocks/MockExtension.sol | 95 ++++ 5 files changed, 914 insertions(+), 29 deletions(-) create mode 100644 core/src/core/token/ERC20CoreContract.sol create mode 100644 core/test/core/ERC20CoreContract.t.sol create mode 100644 core/test/mocks/MockExtension.sol diff --git a/core/src/core/CoreContract.sol b/core/src/core/CoreContract.sol index 1ba9fc46..76cd970c 100644 --- a/core/src/core/CoreContract.sol +++ b/core/src/core/CoreContract.sol @@ -29,6 +29,7 @@ abstract contract CoreContract is ICoreContract { error UnauthorizedInstall(); error UnauthorizedExtensionCall(); + error ExtensionUnsupportedCallbackFunction(); error ExtensionInitializationFailed(); error ExtensionAlreadyInstalled(); error ExtensionNotInstalled(); @@ -69,6 +70,8 @@ abstract contract CoreContract is ICoreContract { VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ + function getSupportedCallbackFunctions() public pure virtual returns (bytes4[] memory); + function getInstalledExtensions() external view @@ -86,6 +89,19 @@ abstract contract CoreContract is ICoreContract { } } + function getCallbackFunctionImplementation(bytes4 _selector) public view virtual returns (address) { + return callbackFunctionImplementation_[_selector]; + } + + function getExtensionFunctionData(bytes4 _selector) + public + view + virtual + returns (InstalledExtensionFunction memory) + { + return extensionFunctionData_[_selector]; + } + /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -135,19 +151,6 @@ abstract contract CoreContract is ICoreContract { function _isAuthorizedToInstallExtensions(address _target) internal view virtual returns (bool); function _isAuthorizedToCallExtensionFunctions(address _target) internal view virtual returns (bool); - function _getCallbackFunctionImplementation(bytes4 _selector) internal view virtual returns (address) { - return callbackFunctionImplementation_[_selector]; - } - - function _getExtensionFunctionData(bytes4 _selector) - internal - view - virtual - returns (InstalledExtensionFunction memory) - { - return extensionFunctionData_[_selector]; - } - function _installExtension(address _extension) internal virtual { // Check: extension not already installed. if (extensionInstalled_[_extension]) { @@ -159,6 +162,32 @@ abstract contract CoreContract is ICoreContract { // Get extension config. ExtensionConfig memory config = IExtensionContract(_extension).getExtensionConfig(); + // Store callback function data. Only install supported callback functions + uint256 totalCallbacks = config.callbackFunctions.length; + bytes4[] memory supportedCallbacks = getSupportedCallbackFunctions(); + + for (uint256 i = 0; i < totalCallbacks; i++) { + bytes4 callbackFunction = config.callbackFunctions[i]; + + // Check: callback function data not already stored. + if (callbackFunctionImplementation_[callbackFunction] != address(0)) { + revert CallbackFunctionAlreadyInstalled(); + } + + bool supported = false; + for (uint256 j = 0; j < supportedCallbacks.length; j++) { + if (callbackFunction == supportedCallbacks[j]) { + supported = true; + break; + } + } + if (!supported) { + revert ExtensionUnsupportedCallbackFunction(); + } + + callbackFunctionImplementation_[callbackFunction] = _extension; + } + // Store extension function data. uint256 totalFunctions = config.extensionABI.length; for (uint256 i = 0; i < totalFunctions; i++) { @@ -171,19 +200,6 @@ abstract contract CoreContract is ICoreContract { extensionFunctionData_[data.selector] = InstalledExtensionFunction({implementation: _extension, data: data}); } - - // Store callback function data. - uint256 totalCallbacks = config.supportedCallbackFunctions.length; - for (uint256 i = 0; i < totalCallbacks; i++) { - bytes4 callbackFunction = config.supportedCallbackFunctions[i]; - - // Check: callback function data not already stored. - if (callbackFunctionImplementation_[callbackFunction] != address(0)) { - revert CallbackFunctionAlreadyInstalled(); - } - - callbackFunctionImplementation_[callbackFunction] = _extension; - } } function uninstallExtension(address _extension) internal virtual { @@ -204,9 +220,9 @@ abstract contract CoreContract is ICoreContract { } // Remove callback function data. - uint256 totalCallbacks = config.supportedCallbackFunctions.length; + uint256 totalCallbacks = config.callbackFunctions.length; for (uint256 i = 0; i < totalCallbacks; i++) { - bytes4 callbackFunction = config.supportedCallbackFunctions[i]; + bytes4 callbackFunction = config.callbackFunctions[i]; delete callbackFunctionImplementation_[callbackFunction]; } } diff --git a/core/src/core/token/ERC20CoreContract.sol b/core/src/core/token/ERC20CoreContract.sol new file mode 100644 index 00000000..36626fd3 --- /dev/null +++ b/core/src/core/token/ERC20CoreContract.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {Ownable} from "@solady/auth/Ownable.sol"; +import {Multicallable} from "@solady/utils/Multicallable.sol"; +import {ERC20} from "@solady/tokens/ERC20.sol"; + +import {CoreContract} from "../CoreContract.sol"; + +import {BeforeMintHookERC20} from "../../hook/BeforeMintHookERC20.sol"; +import {BeforeApproveHookERC20} from "../../hook/BeforeApproveHookERC20.sol"; +import {BeforeTransferHookERC20} from "../../hook/BeforeTransferHookERC20.sol"; +import {BeforeBurnHookERC20} from "../../hook/BeforeBurnHookERC20.sol"; + +contract ERC20CoreContract is ERC20, CoreContract, Ownable, Multicallable { + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @notice The name of the token. + string private name_; + + /// @notice The symbol of the token. + string private symbol_; + + /// @notice The contract metadata URI of the contract. + string private contractURI_; + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when the on initialize call fails. + error ERC20CoreInitCallFailed(); + + /// @notice Emitted when a hook call fails. + error ERC20CoreCallbackFailed(); + + /// @notice Emitted on an attempt to mint tokens when no beforeMint hook is installed. + error ERC20CoreMintDisabled(); + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when the contract URI is updated. + event ContractURIUpdated(); + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor( + string memory _name, + string memory _symbol, + string memory _contractURI, + address _owner, + address[] memory _extensionsToInstall, + address _initCallTarget, + bytes memory _initCalldata + ) payable { + // Set contract metadata + name_ = _name; + symbol_ = _symbol; + _setupContractURI(_contractURI); + + // Set contract owner + _setOwner(_owner); + + // External call upon core core contract initialization. + if (_initCallTarget != address(0) && _initCalldata.length > 0) { + (bool success, bytes memory returndata) = _initCallTarget.call{value: msg.value}(_initCalldata); + if (!success) _revert(returndata, ERC20CoreInitCallFailed.selector); + } + + // Install and initialize hooks + for (uint256 i = 0; i < _extensionsToInstall.length; i++) { + _installExtension(_extensionsToInstall[i]); + } + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns the name of the token. + function name() public view override returns (string memory) { + return name_; + } + + /// @notice Returns the symbol of the token. + function symbol() public view override returns (string memory) { + return symbol_; + } + + /** + * @notice Returns the contract URI of the contract. + * @return uri The contract URI of the contract. + */ + function contractURI() external view returns (string memory) { + return contractURI_; + } + + function getSupportedCallbackFunctions() + public + pure + override + returns (bytes4[] memory supportedCallbackFunctions) + { + supportedCallbackFunctions = new bytes4[](4); + + supportedCallbackFunctions[0] = BeforeMintHookERC20.beforeMintERC20.selector; + supportedCallbackFunctions[1] = BeforeTransferHookERC20.beforeTransferERC20.selector; + supportedCallbackFunctions[2] = BeforeBurnHookERC20.beforeBurnERC20.selector; + supportedCallbackFunctions[3] = BeforeApproveHookERC20.beforeApproveERC20.selector; + } + + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Sets the contract URI of the contract. + * @dev Only callable by contract admin. + * @param _contractURI The contract URI to set. + */ + function setContractURI(string memory _contractURI) external onlyOwner { + _setupContractURI(_contractURI); + } + + /** + * @notice Mints tokens. Calls the beforeMint hook. + * @dev Reverts if beforeMint hook is absent or unsuccessful. + * @param _to The address to mint the tokens to. + * @param _amount The amount of tokens to mint. + * @param _data ABI encoded data to pass to the beforeMintERC20 hook. + */ + function mint(address _to, uint256 _amount, bytes calldata _data) external payable { + _beforeMint(_to, _amount, _data); + _mint(_to, _amount); + } + + /** + * @notice Burns tokens. + * @dev Calls the beforeBurn hook. Skips calling the hook if it doesn't exist. + * @param _amount The amount of tokens to burn. + * @param _data ABI encoded arguments to pass to the beforeBurnERC20 hook. + */ + function burn(uint256 _amount, bytes calldata _data) external { + _beforeBurn(msg.sender, _amount, _data); + _burn(msg.sender, _amount); + } + + /** + * @notice Transfers tokens from a sender to a recipient. + * @param _from The address to transfer tokens from. + * @param _to The address to transfer tokens to. + * @param _amount The quantity of tokens to transfer. + */ + function transferFrom(address _from, address _to, uint256 _amount) public override returns (bool) { + _beforeTransfer(_from, _to, _amount); + return super.transferFrom(_from, _to, _amount); + } + + /** + * @notice Approves a spender to spend tokens on behalf of an owner. + * @param _spender The address to approve spending on behalf of the token owner. + * @param _amount The quantity of tokens to approve. + */ + function approve(address _spender, uint256 _amount) public override returns (bool) { + _beforeApprove(msg.sender, _spender, _amount); + return super.approve(_spender, _amount); + } + + /** + * @notice Sets allowance based on token owner's signed approval. + * + * See https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + * + * @param _owner The account approving the tokens + * @param _spender The address to approve + * @param _value Amount of tokens to approve + */ + function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) public override { + _beforeApprove(_owner, _spender, _value); + super.permit(_owner, _spender, _value, _deadline, _v, _r, _s); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _isAuthorizedToInstallExtensions(address _target) internal view override returns (bool) { + return _target == owner(); + } + + function _isAuthorizedToCallExtensionFunctions(address _target) internal view override returns (bool) { + return _target == owner(); + } + + /// @dev Sets contract URI + function _setupContractURI(string memory _contractURI) internal { + contractURI_ = _contractURI; + emit ContractURIUpdated(); + } + + /*////////////////////////////////////////////////////////////// + CALLBACK INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Calls the beforeMint hook. + function _beforeMint(address _to, uint256 _amount, bytes calldata _data) internal virtual { + address extension = getCallbackFunctionImplementation(BeforeMintHookERC20.beforeMintERC20.selector); + + if (extension != address(0)) { + (bool success, bytes memory returndata) = extension.call{value: msg.value}( + abi.encodeWithSelector(BeforeMintHookERC20.beforeMintERC20.selector, _to, _amount, _data) + ); + + if (!success) _revert(returndata, ERC20CoreCallbackFailed.selector); + } else { + // Revert if beforeMint hook is not installed to disable un-permissioned minting. + revert ERC20CoreMintDisabled(); + } + } + + /// @dev Calls the beforeTransfer hook, if installed. + function _beforeTransfer(address _from, address _to, uint256 _amount) internal virtual { + address extension = getCallbackFunctionImplementation(BeforeTransferHookERC20.beforeTransferERC20.selector); + + if (extension != address(0)) { + (bool success, bytes memory returndata) = extension.call( + abi.encodeWithSelector(BeforeTransferHookERC20.beforeTransferERC20.selector, _from, _to, _amount) + ); + if (!success) _revert(returndata, ERC20CoreCallbackFailed.selector); + } + } + + /// @dev Calls the beforeBurn hook, if installed. + function _beforeBurn(address _from, uint256 _amount, bytes calldata _data) internal virtual { + address extension = getCallbackFunctionImplementation(BeforeBurnHookERC20.beforeBurnERC20.selector); + + if (extension != address(0)) { + (bool success, bytes memory returndata) = extension.call{value: msg.value}( + abi.encodeWithSelector(BeforeBurnHookERC20.beforeBurnERC20.selector, _from, _amount, _data) + ); + if (!success) _revert(returndata, ERC20CoreCallbackFailed.selector); + } + } + + /// @dev Calls the beforeApprove hook, if installed. + function _beforeApprove(address _from, address _to, uint256 _amount) internal virtual { + address extension = getCallbackFunctionImplementation(BeforeApproveHookERC20.beforeApproveERC20.selector); + + if (extension != address(0)) { + (bool success, bytes memory returndata) = extension.call( + abi.encodeWithSelector(BeforeApproveHookERC20.beforeApproveERC20.selector, _from, _to, _amount) + ); + if (!success) _revert(returndata, ERC20CoreCallbackFailed.selector); + } + } +} diff --git a/core/src/interface/IExtensionTypes.sol b/core/src/interface/IExtensionTypes.sol index 4a960bd9..436e08fc 100644 --- a/core/src/interface/IExtensionTypes.sol +++ b/core/src/interface/IExtensionTypes.sol @@ -22,7 +22,7 @@ interface IExtensionTypes { /// @notice All extension functions and supported callback functions of an extension contract. struct ExtensionConfig { - bytes4[] supportedCallbackFunctions; + bytes4[] callbackFunctions; ExtensionFunction[] extensionABI; } } diff --git a/core/test/core/ERC20CoreContract.t.sol b/core/test/core/ERC20CoreContract.t.sol new file mode 100644 index 00000000..b6b99796 --- /dev/null +++ b/core/test/core/ERC20CoreContract.t.sol @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {TestPlus} from "../utils/TestPlus.sol"; +import { + MockExtensionERC20, + MockExtensionWithOnTokenURICallback, + MockExtensionWithPermissionedFallback +} from "../mocks/MockExtension.sol"; + +import {EIP1967Proxy} from "test/utils/EIP1967Proxy.sol"; + +import {ERC20} from "@solady/tokens/ERC20.sol"; + +import {ERC20CoreContract} from "src/core/token/ERC20CoreContract.sol"; +import {CoreContract, ICoreContract} from "src/core/CoreContract.sol"; +import {IHook} from "src/interface/IHook.sol"; + +contract ERC20CoreContractTest is Test, TestPlus { + bytes32 constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + event Transfer(address indexed from, address indexed to, uint256 amount); + + event Approval(address indexed owner, address indexed spender, uint256 amount); + + struct _TestTemps { + address owner; + address to; + uint256 amount; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + uint256 privateKey; + uint256 nonce; + } + + function _testTemps() internal returns (_TestTemps memory t) { + (t.owner, t.privateKey) = _randomSigner(); + t.to = _randomNonZeroAddress(); + t.amount = _random(); + t.deadline = _random(); + } + + // Participants + address public admin = address(0x123); + + // Target test contracts + address public hookProxyAddress; + + ERC20CoreContract public token; + + function setUp() public { + bytes memory hookInitData = abi.encodeWithSelector( + MockExtensionERC20.initialize.selector, + address(0x123) // upgradeAdmin + ); + hookProxyAddress = address(new EIP1967Proxy(address(new MockExtensionERC20()), hookInitData)); + + vm.startPrank(admin); + + address[] memory extensionsToInstall = new address[](1); + extensionsToInstall[0] = hookProxyAddress; + + token = new ERC20CoreContract( + "Token", + "TKN", + "ipfs://QmPVMvePSWfYXTa8haCbFavYx4GM4kBPzvdgBw7PTGUByp/0", + admin, // core contract owner, + extensionsToInstall, + address(0), + bytes("") + ); + vm.stopPrank(); + + vm.label(address(token), "ERC20CoreContract"); + vm.label(admin, "Admin"); + } + + function testPermissionedFallbackFunctionCall() public { + vm.startPrank(admin); + address permissionedCallHook = address(new MockExtensionWithPermissionedFallback()); + + token.installExtension(permissionedCallHook, 0, ""); + vm.stopPrank(); + + vm.expectRevert(abi.encodeWithSelector(CoreContract.UnauthorizedExtensionCall.selector)); + MockExtensionWithPermissionedFallback(address(token)).permissionedFunction(); + + vm.prank(admin); + uint256 result = MockExtensionWithPermissionedFallback(address(token)).permissionedFunction(); + assertEq(result, 1); + } + + function testIncompatibleHookInstall() public { + vm.startPrank(admin); + address mockHook = address(new MockExtensionWithOnTokenURICallback()); + + vm.expectRevert(abi.encodeWithSelector(CoreContract.ExtensionUnsupportedCallbackFunction.selector)); + token.installExtension(address(mockHook), 0, ""); + vm.stopPrank(); + } + + function testMetadata() public { + assertEq(token.name(), "Token"); + assertEq(token.symbol(), "TKN"); + assertEq(token.decimals(), 18); + } + + function testMint() public { + address minter = address(0xBEEF); + uint256 quantity = 1e18; + + vm.expectEmit(true, true, true, true); + emit Transfer(address(0), address(0xBEEF), 1e18); + token.mint(minter, quantity, ""); + + assertEq(token.totalSupply(), 1e18); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testBurn() public { + address minter = address(0xBEEF); + uint256 quantity = 1e18; + + token.mint(minter, quantity, ""); + + address burner = address(0xBEEF); + uint256 burnQuantity = 0.9e18; + + vm.startPrank(address(0xBEEF)); + vm.expectEmit(true, true, true, true); + emit Transfer(address(0xBEEF), address(0), 0.9e18); + token.burn(burnQuantity, ""); + vm.stopPrank(); + + assertEq(token.totalSupply(), 1e18 - 0.9e18); + assertEq(token.balanceOf(address(0xBEEF)), 0.1e18); + } + + function testApprove() public { + vm.expectEmit(true, true, true, true); + emit Approval(address(this), address(0xBEEF), 1e18); + assertTrue(token.approve(address(0xBEEF), 1e18)); + + assertEq(token.allowance(address(this), address(0xBEEF)), 1e18); + } + + function testTransfer() public { + address minter = address(this); + uint256 quantity = 1e18; + + token.mint(minter, quantity, ""); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(this), address(0xBEEF), 1e18); + assertTrue(token.transfer(address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testTransferFrom() public { + address from = address(0xABCD); + address minter = from; + uint256 quantity = 1e18; + + token.mint(minter, quantity, ""); + + vm.prank(from); + token.approve(address(this), 1e18); + + vm.expectEmit(true, true, true, true); + emit Transfer(from, address(0xBEEF), 1e18); + assertTrue(token.transferFrom(from, address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(from, address(this)), 0); + + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testInfiniteApproveTransferFrom() public { + address from = address(0xABCD); + address minter = from; + uint256 quantity = 1e18; + + token.mint(minter, quantity, ""); + + vm.prank(from); + token.approve(address(this), type(uint256).max); + + assertTrue(token.transferFrom(from, address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(from, address(this)), type(uint256).max); + + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testPermit() public { + _TestTemps memory t = _testTemps(); + t.deadline = block.timestamp; + + _signPermit(t); + + _expectPermitEmitApproval(t); + _permit(t); + + _checkAllowanceAndNonce(t); + } + + function testTransferInsufficientBalanceReverts() public { + address minter = address(this); + uint256 quantity = 0.9e18; + + token.mint(minter, quantity, ""); + vm.expectRevert(abi.encodeWithSelector(ERC20.InsufficientBalance.selector)); + token.transfer(address(0xBEEF), 1e18); + } + + function testTransferFromInsufficientAllowanceReverts() public { + address from = address(0xABCD); + address minter = from; + uint256 quantity = 1e18; + + token.mint(minter, quantity, ""); + + vm.prank(from); + token.approve(address(this), 0.9e18); + + vm.expectRevert(abi.encodeWithSelector(ERC20.InsufficientAllowance.selector)); + token.transferFrom(from, address(0xBEEF), 1e18); + } + + function testTransferFromInsufficientBalanceReverts() public { + address from = address(0xABCD); + address minter = from; + uint256 quantity = 0.9e18; + + token.mint(minter, quantity, ""); + + vm.prank(from); + token.approve(address(this), 1e18); + + vm.expectRevert(abi.encodeWithSelector(ERC20.InsufficientBalance.selector)); + token.transferFrom(from, address(0xBEEF), 1e18); + } + + function testMint(address to, uint256 amount) public { + address minter = to; + uint256 quantity = amount; + + vm.assume(to != address(0)); + vm.expectEmit(true, true, true, true); + emit Transfer(address(0), to, amount); + token.mint(minter, quantity, ""); + + assertEq(token.totalSupply(), amount); + assertEq(token.balanceOf(to), amount); + } + + function testBurn(address from, uint256 mintAmount, uint256 burnAmount) public { + vm.assume(from != address(0)); + burnAmount = _bound(burnAmount, 0, mintAmount); + + address minter = from; + uint256 quantity = mintAmount; + + token.mint(minter, quantity, ""); + + address burner = from; + uint256 burnQuantity = burnAmount; + + vm.startPrank(from); + vm.expectEmit(true, true, true, true); + emit Transfer(from, address(0), burnAmount); + token.burn(burnQuantity, ""); + vm.stopPrank(); + + assertEq(token.totalSupply(), mintAmount - burnAmount); + assertEq(token.balanceOf(from), mintAmount - burnAmount); + } + + function testApprove(address to, uint256 amount) public { + vm.assume(to != address(0)); + assertTrue(token.approve(to, amount)); + + assertEq(token.allowance(address(this), to), amount); + } + + function testTransfer(address to, uint256 amount) public { + address minter = address(this); + uint256 quantity = amount; + + vm.assume(to != address(0)); + token.mint(minter, quantity, ""); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(this), to, amount); + assertTrue(token.transfer(to, amount)); + assertEq(token.totalSupply(), amount); + + if (address(this) == to) { + assertEq(token.balanceOf(address(this)), amount); + } else { + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(to), amount); + } + } + + function testTransferFrom(address spender, address from, address to, uint256 approval, uint256 amount) public { + vm.assume(spender != address(0) && from != address(0) && to != address(0)); + amount = _bound(amount, 0, approval); + + address minter = from; + uint256 quantity = amount; + + token.mint(minter, quantity, ""); + assertEq(token.balanceOf(from), amount); + + vm.prank(from); + token.approve(spender, approval); + + vm.expectEmit(true, true, true, true); + emit Transfer(from, to, amount); + vm.prank(spender); + assertTrue(token.transferFrom(from, to, amount)); + assertEq(token.totalSupply(), amount); + + if (approval == type(uint256).max) { + assertEq(token.allowance(from, spender), approval); + } else { + assertEq(token.allowance(from, spender), approval - amount); + } + + if (from == to) { + assertEq(token.balanceOf(from), amount); + } else { + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(to), amount); + } + } + + function testPermit(uint256) public { + _TestTemps memory t = _testTemps(); + if (t.deadline < block.timestamp) t.deadline = block.timestamp; + + _signPermit(t); + + _expectPermitEmitApproval(t); + _permit(t); + + _checkAllowanceAndNonce(t); + } + + function _checkAllowanceAndNonce(_TestTemps memory t) internal { + assertEq(token.allowance(t.owner, t.to), t.amount); + assertEq(token.nonces(t.owner), t.nonce + 1); + } + + function testBurnInsufficientBalanceReverts(address to, uint256 mintAmount, uint256 burnAmount) public { + vm.assume(to != address(0)); + if (mintAmount == type(uint256).max) mintAmount--; + burnAmount = _bound(burnAmount, mintAmount + 1, type(uint256).max); + + address minter = to; + uint256 quantity = mintAmount; + + token.mint(minter, quantity, ""); + + address burner = to; + uint256 burnQuantity = burnAmount; + + vm.expectRevert(abi.encodeWithSelector(ERC20.InsufficientBalance.selector)); + vm.prank(to); + token.burn(burnQuantity, ""); + } + + function testTransferInsufficientBalanceReverts(address to, uint256 mintAmount, uint256 sendAmount) public { + vm.assume(to != address(0)); + if (mintAmount == type(uint256).max) mintAmount--; + sendAmount = _bound(sendAmount, mintAmount + 1, type(uint256).max); + + address minter = address(this); + uint256 quantity = mintAmount; + + token.mint(minter, quantity, ""); + vm.expectRevert(abi.encodeWithSelector(ERC20.InsufficientBalance.selector)); + token.transfer(to, sendAmount); + } + + function testTransferFromInsufficientAllowanceReverts(address to, uint256 approval, uint256 amount) public { + vm.assume(to != address(0)); + if (approval == type(uint256).max) approval--; + amount = _bound(amount, approval + 1, type(uint256).max); + + address from = address(0xABCD); + + address minter = from; + uint256 quantity = amount; + + token.mint(minter, quantity, ""); + + vm.prank(from); + token.approve(address(this), approval); + + vm.expectRevert(abi.encodeWithSelector(ERC20.InsufficientAllowance.selector)); + token.transferFrom(from, to, amount); + } + + function testTransferFromInsufficientBalanceReverts(address to, uint256 mintAmount, uint256 sendAmount) public { + vm.assume(to != address(0)); + if (mintAmount == type(uint256).max) mintAmount--; + sendAmount = _bound(sendAmount, mintAmount + 1, type(uint256).max); + + address from = address(0xABCD); + + address minter = from; + uint256 quantity = mintAmount; + + token.mint(minter, quantity, ""); + + vm.prank(from); + token.approve(address(this), sendAmount); + + vm.expectRevert(abi.encodeWithSelector(ERC20.InsufficientBalance.selector)); + token.transferFrom(from, to, sendAmount); + } + + function testPermitBadNonceReverts() public { + _TestTemps memory t = _testTemps(); + + t.nonce = _random(); + + _signPermit(t); + + vm.expectRevert(abi.encodeWithSelector(ERC20.InvalidPermit.selector)); + token.permit(t.owner, t.to, t.amount, t.deadline, t.v, t.r, t.s); + } + + function testPermitBadDeadlineReverts() public { + _TestTemps memory t = _testTemps(); + if (t.deadline == type(uint256).max) t.deadline--; + if (t.deadline < block.timestamp) t.deadline = block.timestamp; + + _signPermit(t); + + vm.expectRevert(abi.encodeWithSelector(ERC20.InvalidPermit.selector)); + t.deadline += 1; + token.permit(t.owner, t.to, t.amount, t.deadline, t.v, t.r, t.s); + } + + function testPermitPastDeadlineReverts() public { + _TestTemps memory t = _testTemps(); + t.deadline = _bound(t.deadline, 0, block.timestamp - 1); + + _signPermit(t); + + vm.expectRevert(abi.encodeWithSelector(ERC20.PermitExpired.selector)); + token.permit(t.owner, t.to, t.amount, t.deadline, t.v, t.r, t.s); + } + + function testPermitReplayReverts() public { + _TestTemps memory t = _testTemps(); + if (t.deadline < block.timestamp) t.deadline = block.timestamp; + + _signPermit(t); + + _expectPermitEmitApproval(t); + token.permit(t.owner, t.to, t.amount, t.deadline, t.v, t.r, t.s); + vm.expectRevert(abi.encodeWithSelector(ERC20.InvalidPermit.selector)); + token.permit(t.owner, t.to, t.amount, t.deadline, t.v, t.r, t.s); + } + + function _signPermit(_TestTemps memory t) internal view { + bytes32 innerHash = keccak256(abi.encode(PERMIT_TYPEHASH, t.owner, t.to, t.amount, t.nonce, t.deadline)); + bytes32 domainSeparator = token.DOMAIN_SEPARATOR(); + bytes32 outerHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, innerHash)); + (t.v, t.r, t.s) = vm.sign(t.privateKey, outerHash); + } + + function _expectPermitEmitApproval(_TestTemps memory t) internal { + vm.expectEmit(true, true, true, true); + emit Approval(t.owner, t.to, t.amount); + } + + function _permit(_TestTemps memory t) internal { + address token_ = address(token); + /// @solidity memory-safe-assembly + assembly { + let m := mload(sub(t, 0x20)) + mstore(sub(t, 0x20), 0xd505accf) + pop(call(gas(), token_, 0, sub(t, 0x04), 0xe4, 0x00, 0x00)) + mstore(sub(t, 0x20), m) + } + } +} diff --git a/core/test/mocks/MockExtension.sol b/core/test/mocks/MockExtension.sol new file mode 100644 index 00000000..5717ca59 --- /dev/null +++ b/core/test/mocks/MockExtension.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {BeforeMintHookERC20} from "src/hook/BeforeMintHookERC20.sol"; +import {OnTokenURIHook} from "src/hook/OnTokenURIHook.sol"; +import {IExtensionContract} from "src/interface/IExtensionContract.sol"; + +import "@solady/utils/Initializable.sol"; +import "@solady/utils/UUPSUpgradeable.sol"; + +contract MockExtensionERC20 is BeforeMintHookERC20, IExtensionContract, Initializable, UUPSUpgradeable { + address public upgradeAdmin; + + function initialize(address _upgradeAdmin) public initializer { + upgradeAdmin = _upgradeAdmin; + } + + error UnauthorizedUpgrade(); + + function _authorizeUpgrade(address) internal view override { + if (msg.sender != upgradeAdmin) { + revert UnauthorizedUpgrade(); + } + } + + function getExtensionConfig() external pure override returns (ExtensionConfig memory) { + bytes4[] memory callbackFunctions = new bytes4[](1); + callbackFunctions[0] = this.beforeMintERC20.selector; + ExtensionFunction[] memory extensionABI = new ExtensionFunction[](0); + return ExtensionConfig(callbackFunctions, extensionABI); + } + + function beforeMintERC20(address _to, uint256 _amount, bytes memory _data) + external + payable + override + returns (bytes memory) + { + return abi.encode(_amount); + } +} + +contract MockExtensionWithOnTokenURICallback is OnTokenURIHook, IExtensionContract, Initializable, UUPSUpgradeable { + address public upgradeAdmin; + + function initialize(address _upgradeAdmin) public initializer { + upgradeAdmin = _upgradeAdmin; + } + + error UnauthorizedUpgrade(); + + function _authorizeUpgrade(address) internal view override { + if (msg.sender != upgradeAdmin) { + revert UnauthorizedUpgrade(); + } + } + + function getExtensionConfig() external pure override returns (ExtensionConfig memory) { + bytes4[] memory callbackFunctions = new bytes4[](1); + callbackFunctions[0] = this.onTokenURI.selector; + ExtensionFunction[] memory extensionABI = new ExtensionFunction[](0); + return ExtensionConfig(callbackFunctions, extensionABI); + } + + function onTokenURI(uint256 _id) public view override returns (string memory) { + return "mockURI/0"; + } +} + +contract MockExtensionWithPermissionedFallback is IExtensionContract, Initializable, UUPSUpgradeable { + address public upgradeAdmin; + + function initialize(address _upgradeAdmin) public initializer { + upgradeAdmin = _upgradeAdmin; + } + + error UnauthorizedUpgrade(); + + function _authorizeUpgrade(address) internal view override { + if (msg.sender != upgradeAdmin) { + revert UnauthorizedUpgrade(); + } + } + + function getExtensionConfig() external pure override returns (ExtensionConfig memory) { + bytes4[] memory callbackFunctions = new bytes4[](0); + ExtensionFunction[] memory extensionABI = new ExtensionFunction[](1); + extensionABI[0] = ExtensionFunction(this.permissionedFunction.selector, CallType.CALL, true); + return ExtensionConfig(callbackFunctions, extensionABI); + } + + function permissionedFunction() external pure virtual returns (uint256) { + return 1; + } +} From 8ab548ab67bccc3e7848ac8874a77d4187f58f3a Mon Sep 17 00:00:00 2001 From: Krishang Date: Sat, 20 Apr 2024 12:05:35 +0530 Subject: [PATCH 8/8] Benchmark for ERC20CoreContract --- .../benchmark/ER20CoreContractBenchmark.t.sol | 246 ++++++++++++++++++ core/test/benchmark/ERC20CoreBenchmark.t.sol | 27 +- core/test/mocks/MockExtension.sol | 66 +++++ 3 files changed, 325 insertions(+), 14 deletions(-) create mode 100644 core/test/benchmark/ER20CoreContractBenchmark.t.sol diff --git a/core/test/benchmark/ER20CoreContractBenchmark.t.sol b/core/test/benchmark/ER20CoreContractBenchmark.t.sol new file mode 100644 index 00000000..ba019d87 --- /dev/null +++ b/core/test/benchmark/ER20CoreContractBenchmark.t.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; + +import {EIP1967Proxy} from "test/utils/EIP1967Proxy.sol"; + +import {IExtensionContract} from "src/interface/IExtensionContract.sol"; +import {CoreContract, ICoreContract} from "src/core/CoreContract.sol"; + +import { + MockExtensionERC20, + MockExtensionWithOneCallbackERC20, + MockExtensionWithFourCallbacksERC20 +} from "test/mocks/MockExtension.sol"; + +import {ERC20CoreContract} from "src/core/token/ERC20CoreContract.sol"; + +contract ERC20CoreContractBenchmarkTest is Test { + /*////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////*/ + + // Participants + address public platformAdmin = address(0x123); + address public platformUser = address(0x456); + address public claimer = 0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd; + + // Target test contracts + ERC20CoreContract public erc20; + address public hookProxyAddress; + + function setUp() public { + // Setup: minting on ERC-20 contract. + + // Platform contracts: gas incurred by platform. + vm.startPrank(platformAdmin); + + hookProxyAddress = address( + new EIP1967Proxy( + address(new MockExtensionERC20()), + abi.encodeWithSelector( + MockExtensionERC20.initialize.selector, + platformAdmin // upgradeAdmin + ) + ) + ); + + vm.stopPrank(); + + // Developer contract: gas incurred by developer. + vm.startPrank(platformUser); + + address[] memory extensionsToInstall = new address[](0); + + erc20 = new ERC20CoreContract( + "Token", + "TKN", + "ipfs://QmPVMvePSWfYXTa8haCbFavYx4GM4kBPzvdgBw7PTGUByp/0", + platformUser, // core contract owner, + extensionsToInstall, + address(0), + bytes("") + ); + + vm.stopPrank(); + + vm.label(address(erc20), "ERC20CoreContract"); + vm.label(hookProxyAddress, "MockExtensionERC20"); + vm.label(platformAdmin, "Admin"); + vm.label(platformUser, "Developer"); + vm.label(claimer, "Claimer"); + } + + /*////////////////////////////////////////////////////////////// + DEPLOY END-USER CONTRACT + //////////////////////////////////////////////////////////////*/ + + function test_deployEndUserContract() public { + // Deploy a minimal proxy to the ERC20CoreContract implementation contract. + + vm.pauseGasMetering(); + + address[] memory extensionsToInstall = new address[](1); + extensionsToInstall[0] = hookProxyAddress; + + vm.resumeGasMetering(); + + ERC20CoreContract core = new ERC20CoreContract( + "Token", + "TKN", + "ipfs://QmPVMvePSWfYXTa8haCbFavYx4GM4kBPzvdgBw7PTGUByp/0", + platformUser, // core contract owner, + extensionsToInstall, + address(0), + bytes("") + ); + } + + /*////////////////////////////////////////////////////////////// + MINT 1 TOKEN AND 10 TOKENS + //////////////////////////////////////////////////////////////*/ + + function test_mintOneToken() public { + vm.pauseGasMetering(); + + vm.prank(platformUser); + erc20.installExtension(hookProxyAddress, 0, ""); + + // Check pre-mint state + address claimerAddress = claimer; + uint256 quantity = 1 ether; + ERC20CoreContract core = erc20; + + vm.prank(claimer); + + vm.resumeGasMetering(); + + // Claim token + core.mint(claimerAddress, quantity, ""); + } + + function test_mintTenTokens() public { + vm.pauseGasMetering(); + + vm.prank(platformUser); + erc20.installExtension(hookProxyAddress, 0, ""); + + // Check pre-mint state + address claimerAddress = claimer; + uint256 quantity = 10 ether; + ERC20CoreContract core = erc20; + + vm.prank(claimer); + + vm.resumeGasMetering(); + + // Claim token + core.mint(claimerAddress, quantity, ""); + } + + /*////////////////////////////////////////////////////////////// + TRANSFER 1 TOKEN + //////////////////////////////////////////////////////////////*/ + + function test_transferOneToken() public { + vm.pauseGasMetering(); + + vm.prank(platformUser); + erc20.installExtension(hookProxyAddress, 0, ""); + + // Check pre-mint state + address claimerAddress = claimer; + uint256 quantity = 10 ether; + ERC20CoreContract core = erc20; + core.mint(claimerAddress, quantity, ""); + + address to = address(0x121212); + vm.prank(claimer); + + vm.resumeGasMetering(); + + // Transfer token + core.transfer(to, 1); + } + + /*////////////////////////////////////////////////////////////// + PERFORM A BEACON UPGRADE + //////////////////////////////////////////////////////////////*/ + + function test_beaconUpgrade() public { + vm.pauseGasMetering(); + + address newImpl = address(new MockExtensionERC20()); + address proxyAdmin = platformAdmin; + MockExtensionERC20 proxy = MockExtensionERC20(payable(hookProxyAddress)); + + vm.prank(proxyAdmin); + + vm.resumeGasMetering(); + + // Perform upgrade + proxy.upgradeToAndCall(address(newImpl), bytes("")); + } + + /*////////////////////////////////////////////////////////////// + ADD NEW FUNCTIONALITY AND UPDATE FUNCTIONALITY + //////////////////////////////////////////////////////////////*/ + + function test_installOneHook() public { + vm.pauseGasMetering(); + + address mockHook = address(new MockExtensionWithOneCallbackERC20()); + ERC20CoreContract hookConsumer = erc20; + + vm.prank(platformUser); + + vm.resumeGasMetering(); + + hookConsumer.installExtension(mockHook, 0, ""); + } + + function test_installFourHooks() public { + vm.pauseGasMetering(); + + address mockHook = address(new MockExtensionWithFourCallbacksERC20()); + ERC20CoreContract hookConsumer = erc20; + + vm.prank(platformUser); + + vm.resumeGasMetering(); + + hookConsumer.installExtension(mockHook, 0, ""); + } + + function test_uninstallOneHook() public { + vm.pauseGasMetering(); + + ERC20CoreContract hookConsumer = erc20; + + vm.prank(platformUser); + hookConsumer.installExtension(hookProxyAddress, 0, ""); + + vm.prank(platformUser); + + vm.resumeGasMetering(); + + hookConsumer.uninstallExtension(hookProxyAddress, 0, ""); + } + + function test_uninstallFourHooks() public { + vm.pauseGasMetering(); + + address mockHook = address(new MockExtensionWithFourCallbacksERC20()); + ERC20CoreContract hookConsumer = erc20; + + vm.prank(platformUser); + hookConsumer.installExtension(mockHook, 0, ""); + + vm.prank(platformUser); + + vm.resumeGasMetering(); + + hookConsumer.uninstallExtension(mockHook, 0, ""); + } +} diff --git a/core/test/benchmark/ERC20CoreBenchmark.t.sol b/core/test/benchmark/ERC20CoreBenchmark.t.sol index 62264945..c947c717 100644 --- a/core/test/benchmark/ERC20CoreBenchmark.t.sol +++ b/core/test/benchmark/ERC20CoreBenchmark.t.sol @@ -60,9 +60,6 @@ contract ERC20CoreBenchmarkTest is Test, HookFlagsDirectory { hooksToInstallOnInit ); - // Developer installs `MockHookERC20` hook - erc20.installHook(IHookInstaller.InstallHookParams(hookProxyAddress, 0, bytes(""))); - vm.stopPrank(); vm.label(address(erc20), "ERC20Core"); @@ -103,6 +100,9 @@ contract ERC20CoreBenchmarkTest is Test, HookFlagsDirectory { function test_mintOneToken() public { vm.pauseGasMetering(); + vm.prank(platformUser); + erc20.installHook(IHookInstaller.InstallHookParams(hookProxyAddress, 0, bytes(""))); + // Check pre-mint state address claimerAddress = claimer; uint256 quantity = 1 ether; @@ -119,6 +119,9 @@ contract ERC20CoreBenchmarkTest is Test, HookFlagsDirectory { function test_mintTenTokens() public { vm.pauseGasMetering(); + vm.prank(platformUser); + erc20.installHook(IHookInstaller.InstallHookParams(hookProxyAddress, 0, bytes(""))); + // Check pre-mint state address claimerAddress = claimer; uint256 quantity = 10 ether; @@ -139,6 +142,9 @@ contract ERC20CoreBenchmarkTest is Test, HookFlagsDirectory { function test_transferOneToken() public { vm.pauseGasMetering(); + vm.prank(platformUser); + erc20.installHook(IHookInstaller.InstallHookParams(hookProxyAddress, 0, bytes(""))); + // Check pre-mint state address claimerAddress = claimer; uint256 quantity = 10 ether; @@ -183,9 +189,6 @@ contract ERC20CoreBenchmarkTest is Test, HookFlagsDirectory { IHook mockHook = IHook(address(new MockOneHookImplERC20())); ERC20Core hookConsumer = erc20; - vm.prank(platformUser); - hookConsumer.uninstallHook(hookProxyAddress); - vm.prank(platformUser); vm.resumeGasMetering(); @@ -199,9 +202,6 @@ contract ERC20CoreBenchmarkTest is Test, HookFlagsDirectory { IHook mockHook = IHook(address(new MockFourHookImplERC20())); ERC20Core hookConsumer = erc20; - vm.prank(platformUser); - hookConsumer.uninstallHook(hookProxyAddress); - vm.prank(platformUser); vm.resumeGasMetering(); @@ -212,14 +212,16 @@ contract ERC20CoreBenchmarkTest is Test, HookFlagsDirectory { function test_uninstallOneHook() public { vm.pauseGasMetering(); - IHook mockHook = IHook(address(new MockOneHookImplERC20())); ERC20Core hookConsumer = erc20; + vm.prank(platformUser); + erc20.installHook(IHookInstaller.InstallHookParams(hookProxyAddress, 0, bytes(""))); + vm.prank(platformUser); vm.resumeGasMetering(); - hookConsumer.uninstallHook(address(mockHook)); + hookConsumer.uninstallHook(hookProxyAddress); } function test_uninstallFourHooks() public { @@ -228,9 +230,6 @@ contract ERC20CoreBenchmarkTest is Test, HookFlagsDirectory { IHook mockHook = IHook(address(new MockFourHookImplERC20())); ERC20Core hookConsumer = erc20; - vm.prank(platformUser); - hookConsumer.uninstallHook(hookProxyAddress); - vm.prank(platformUser); hookConsumer.installHook(IHookInstaller.InstallHookParams(address(mockHook), 0, "")); diff --git a/core/test/mocks/MockExtension.sol b/core/test/mocks/MockExtension.sol index 5717ca59..b628814c 100644 --- a/core/test/mocks/MockExtension.sol +++ b/core/test/mocks/MockExtension.sol @@ -2,7 +2,11 @@ pragma solidity ^0.8.0; import {BeforeMintHookERC20} from "src/hook/BeforeMintHookERC20.sol"; +import {BeforeTransferHookERC20} from "src/hook/BeforeTransferHookERC20.sol"; +import {BeforeBurnHookERC20} from "src/hook/BeforeBurnHookERC20.sol"; +import {BeforeApproveHookERC20} from "src/hook/BeforeApproveHookERC20.sol"; import {OnTokenURIHook} from "src/hook/OnTokenURIHook.sol"; + import {IExtensionContract} from "src/interface/IExtensionContract.sol"; import "@solady/utils/Initializable.sol"; @@ -93,3 +97,65 @@ contract MockExtensionWithPermissionedFallback is IExtensionContract, Initializa return 1; } } + +contract MockExtensionWithOneCallbackERC20 is + BeforeMintHookERC20, + IExtensionContract, + Initializable, + UUPSUpgradeable +{ + address public upgradeAdmin; + + function initialize(address _upgradeAdmin) public initializer { + upgradeAdmin = _upgradeAdmin; + } + + error UnauthorizedUpgrade(); + + function _authorizeUpgrade(address) internal view override { + if (msg.sender != upgradeAdmin) { + revert UnauthorizedUpgrade(); + } + } + + function getExtensionConfig() external pure override returns (ExtensionConfig memory) { + bytes4[] memory callbackFunctions = new bytes4[](1); + callbackFunctions[0] = this.beforeMintERC20.selector; + ExtensionFunction[] memory extensionABI = new ExtensionFunction[](0); + return ExtensionConfig(callbackFunctions, extensionABI); + } +} + +contract MockExtensionWithFourCallbacksERC20 is + IExtensionContract, + BeforeMintHookERC20, + BeforeTransferHookERC20, + BeforeBurnHookERC20, + BeforeApproveHookERC20, + Initializable, + UUPSUpgradeable +{ + address public upgradeAdmin; + + function initialize(address _upgradeAdmin) public initializer { + upgradeAdmin = _upgradeAdmin; + } + + error UnauthorizedUpgrade(); + + function _authorizeUpgrade(address) internal view override { + if (msg.sender != upgradeAdmin) { + revert UnauthorizedUpgrade(); + } + } + + function getExtensionConfig() external pure override returns (ExtensionConfig memory) { + bytes4[] memory callbackFunctions = new bytes4[](4); + callbackFunctions[0] = this.beforeMintERC20.selector; + callbackFunctions[1] = this.beforeTransferERC20.selector; + callbackFunctions[2] = this.beforeBurnERC20.selector; + callbackFunctions[3] = this.beforeApproveERC20.selector; + ExtensionFunction[] memory extensionABI = new ExtensionFunction[](0); + return ExtensionConfig(callbackFunctions, extensionABI); + } +}