diff --git a/foundry.toml b/foundry.toml index 5902201..f38db52 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,7 +9,7 @@ multiline_func_header = 'params_first_multi' sort_imports = true [profile.default] -solc_version = '0.8.19' +solc_version = '0.8.20' src = 'solidity' test = 'solidity/test' out = 'out' diff --git a/package.json b/package.json index 3b374ee..605b753 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ }, "dependencies": { "@defi-wonderland/prophet-core": "0.0.0-3afab791", - "@openzeppelin/contracts": "4.9.5", + "@openzeppelin/contracts": "5.0.0", "solmate": "https://github.com/transmissions11/solmate.git#bfc9c25865a274a7827fea5abf6e4fb64fc64e6c" }, "devDependencies": { diff --git a/solidity/contracts/modules/accessControl/PermitAccessModule.sol b/solidity/contracts/modules/accessControl/PermitAccessModule.sol new file mode 100644 index 0000000..15ae20e --- /dev/null +++ b/solidity/contracts/modules/accessControl/PermitAccessModule.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IModule, Module} from '@defi-wonderland/prophet-core/solidity/contracts/Module.sol'; +import {IOracle} from '@defi-wonderland/prophet-core/solidity/interfaces/IOracle.sol'; + +import { + IAccessControlModule, IPermitAccessModule +} from '../../../interfaces/modules/accessControl/IPermitAccessModule.sol'; + +import {Nonces} from '@openzeppelin/contracts/utils/Nonces.sol'; +import {ECDSA} from '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; +import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; + +contract PermitAccessModule is Module, IPermitAccessModule, EIP712, Nonces { + constructor(IOracle _oracle) Module(_oracle) EIP712('PermitAccessModule', '1') {} + + /// @inheritdoc IPermitAccessModule + function decodeAccessControlData(bytes memory _data) + public + pure + returns (IAccessControlModule.AccessControlParameters memory _accessControlData) + { + _accessControlData = abi.decode(_data, (IAccessControlModule.AccessControlParameters)); + } + + /// @inheritdoc IPermitAccessModule + function decodePermitParametersData(bytes memory _data) public pure returns (PermitParameters memory _permitData) { + _permitData = abi.decode(_data, (PermitParameters)); + } + + /// @inheritdoc IModule + function moduleName() external pure returns (string memory _moduleName) { + _moduleName = 'PermitAccessModule'; + } + + /// @inheritdoc IPermitAccessModule + function getDomainSeparator() external view returns (bytes32 _domainSeparator) { + _domainSeparator = _domainSeparatorV4(); + } + + /// @inheritdoc IAccessControlModule + function hasAccess(bytes memory _data) external returns (bool _hasAccess) { + IAccessControlModule.AccessControlParameters memory _accessControlData = decodeAccessControlData(_data); + PermitParameters memory _permitData = decodePermitParametersData(_accessControlData.accessControl.data); + + if (block.timestamp > _permitData.deadline) { + revert PermitAccessModule_InvalidDeadline(); + } + + bytes32 _structHash = keccak256( + abi.encode( + _accessControlData.typehash, + _accessControlData.accessControl.user, + _accessControlData.sender, + _useNonce(_accessControlData.accessControl.user), + _permitData.deadline + ) + ); + + bytes32 _hash = _hashTypedDataV4(_structHash); + + address _signer = ECDSA.recover(_hash, _permitData.v, _permitData.r, _permitData.s); + if (_signer != _accessControlData.accessControl.user) { + revert PermitAccessModule_InvalidSignature(); + } + + _hasAccess = true; + } +} diff --git a/solidity/interfaces/modules/accessControl/IPermitAccessModule.sol b/solidity/interfaces/modules/accessControl/IPermitAccessModule.sol new file mode 100644 index 0000000..9e5b769 --- /dev/null +++ b/solidity/interfaces/modules/accessControl/IPermitAccessModule.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IAccessControlModule} from + '@defi-wonderland/prophet-core/solidity/interfaces/modules/accessControl/IAccessControlModule.sol'; + +/** + * @title IPermitAccessModule + */ +interface IPermitAccessModule is IAccessControlModule { + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Thrown when the deadline is invalid + */ + error PermitAccessModule_InvalidDeadline(); + + /** + * @notice Thrown when the signature is invalid + */ + error PermitAccessModule_InvalidSignature(); + + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + /** + * @notice Decode permit parameters data + * @param deadline The deadline timestamp + * @param v The signature v + * @param r The signature r + * @param s The signature s + */ + struct PermitParameters { + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + /*/////////////////////////////////////////////////////////////// + VIEWS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Get the domain separator + * @return _domainSeparator The domain separator + */ + function getDomainSeparator() external view returns (bytes32 _domainSeparator); + + /** + * @notice Decode permit parameters data + * @param _permitData The permit data to decode + */ + function decodePermitParametersData(bytes memory _data) external pure returns (PermitParameters memory _permitData); + + /** + * @notice Decode access control data + * @param _data The data to decode + * @return _accessControlData The access control data + */ + function decodeAccessControlData(bytes memory _data) + external + pure + returns (IAccessControlModule.AccessControlParameters memory _accessControlData); + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ +} diff --git a/solidity/test/unit/modules/accessControl/PermitAccessModule.t.sol b/solidity/test/unit/modules/accessControl/PermitAccessModule.t.sol new file mode 100644 index 0000000..c5455b7 --- /dev/null +++ b/solidity/test/unit/modules/accessControl/PermitAccessModule.t.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; + +import {Helpers} from '../../../utils/Helpers.sol'; + +import {IModule} from '@defi-wonderland/prophet-core/solidity/interfaces/IModule.sol'; +import {IOracle} from '@defi-wonderland/prophet-core/solidity/interfaces/IOracle.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import { + IAccessControlModule, + IPermitAccessModule, + PermitAccessModule +} from '../../../../contracts/modules/accessControl/PermitAccessModule.sol'; + +import {IAccessController} from '@defi-wonderland/prophet-core/solidity/interfaces/IAccessController.sol'; + +/** + * @title Access Control Module Unit tests + */ +contract BaseTest is Test, Helpers { + // Mock Oracle + IOracle public oracle; + + // Access Control Module + PermitAccessModule public permitAccessModule; + + /** + * @notice Deploy the target and mock oracle + */ + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + vm.etch(address(oracle), hex'069420'); + + permitAccessModule = new PermitAccessModule(oracle); + } +} + +contract AccessControlModule_Unit_Constructor is BaseTest { + /** + * @notice Test the constructor + */ + function test_constructor() public view { + // Assert the oracle address + assertEq(address(permitAccessModule.ORACLE()), address(oracle)); + } +} + +contract AccessControlModule_Unit_DecodeAccessControlData is BaseTest { + /** + * @notice Test the decodeAccessControlData function + */ + function test_decodeAccessControlData() public view { + // Encode the access control data + IAccessControlModule.AccessControlParameters memory _accessControlData = IAccessControlModule + .AccessControlParameters({ + sender: address(0x1), + typehash: hex'1234', + accessControl: IAccessController.AccessControl({user: address(0x2), data: hex'5678'}), + params: hex'9abc' + }); + + // Decode the access control data + IAccessControlModule.AccessControlParameters memory _decodedAccessControlData = + permitAccessModule.decodeAccessControlData(abi.encode(_accessControlData)); + + // Assert the decoded access control data + assertEq(_decodedAccessControlData.sender, _accessControlData.sender); + assertEq(_decodedAccessControlData.typehash, _accessControlData.typehash); + assertEq(_decodedAccessControlData.accessControl.user, _accessControlData.accessControl.user); + assertEq(_decodedAccessControlData.accessControl.data, _accessControlData.accessControl.data); + assertEq(_decodedAccessControlData.params, _accessControlData.params); + } +} + +contract AccessControlModule_Unit_DecodePermitParametersData is BaseTest { + /** + * @notice Test the decodePermitParametersData function + */ + function test_decodePermitParametersData() public view { + // Encode the permit parameters data + IPermitAccessModule.PermitParameters memory _permitData = + IPermitAccessModule.PermitParameters({deadline: 1, v: 2, r: hex'1234', s: hex'5678'}); + + // Decode the permit parameters data + IPermitAccessModule.PermitParameters memory _decodedPermitData = + permitAccessModule.decodePermitParametersData(abi.encode(_permitData)); + + // Assert the decoded permit parameters data + assertEq(_decodedPermitData.deadline, _permitData.deadline); + assertEq(_decodedPermitData.v, _permitData.v); + assertEq(_decodedPermitData.r, _permitData.r); + assertEq(_decodedPermitData.s, _permitData.s); + } +} + +contract AccessControlModule_Unit_ModuleName is BaseTest { + /** + * @notice Test the moduleName function + */ + function test_ModuleName() public view { + // Assert the module name + assertEq(permitAccessModule.moduleName(), 'PermitAccessModule'); + } +} + +contract AccessControlModule_Unit_HasAccess is BaseTest { + /** + * @notice Test the hasAccess function + */ + function test_hasAccess_true() public { + bytes32 _permitHash = keccak256( + abi.encodePacked( + '\x19\x01', + permitAccessModule.getDomainSeparator(), + keccak256(abi.encode(keccak256('1234'), address(0x2), address(0x1), permitAccessModule.nonces(address(0x2)), 1)) + ) + ); + + (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(_permitHash); + + // Encode the permit parameters data + IPermitAccessModule.PermitParameters memory _permitData = + IPermitAccessModule.PermitParameters({deadline: 1, v: _v, r: _r, s: _s}); + + bytes memory _permitDataEncoded = abi.encode(_permitData); + + // Encode the access control data + IAccessControlModule.AccessControlParameters memory _accessControlData = IAccessControlModule + .AccessControlParameters({ + sender: address(0x1), + typehash: keccak256('1234'), + accessControl: IAccessController.AccessControl({user: address(0x2), data: _permitDataEncoded}), + params: hex'9abc' + }); + + bytes memory _accessControlDataEncoded = abi.encode(_accessControlData); + + // Assert the has access + assertEq(permitAccessModule.hasAccess(_accessControlDataEncoded), true); + } +} diff --git a/yarn.lock b/yarn.lock index e9f077b..c29ea01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -256,10 +256,10 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.2.tgz#1cb2d5e4d3360141a17dbc45094a8cad6aac16c1" integrity sha512-mO+y6JaqXjWeMh9glYVzVu8HYPGknAAnWyxTRhGeckOruyXQMNnlcW6w/Dx9ftLeIQk6N+ZJFuVmTwF7lEIFrg== -"@openzeppelin/contracts@4.9.5": - version "4.9.5" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.5.tgz#1eed23d4844c861a1835b5d33507c1017fa98de8" - integrity sha512-ZK+W5mVhRppff9BE6YdR8CC52C8zAvsVAiWhEtQ5+oNxFE6h1WdeWo+FJSF8KKvtxxVYZ7MTP/5KoVpAU3aSWg== +"@openzeppelin/contracts@5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.0.tgz#ee0e4b4564f101a5c4ee398cd4d73c0bd92b289c" + integrity sha512-bv2sdS6LKqVVMLI5+zqnNrNU/CA+6z6CmwFXm/MzmOPBRSO5reEJN7z0Gbzvs0/bv/MZZXNklubpwy3v2+azsw== "@solidity-parser/parser@^0.14.1": version "0.14.5"