diff --git a/contracts/core/GuardableModifier.sol b/contracts/core/GuardableModifier.sol new file mode 100644 index 00000000..90c29b1a --- /dev/null +++ b/contracts/core/GuardableModifier.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.7.0 <0.9.0; + +import "../guard/Guardable.sol"; +import "./Modifier.sol"; + +abstract contract GuardableModifier is Module, Guardable, Modifier { + /// @dev Passes a transaction to be executed by the avatar. + /// @notice Can only be called by this contract. + /// @param to Destination address of module transaction. + /// @param value Ether value of module transaction. + /// @param data Data payload of module transaction. + /// @param operation Operation type of module transaction: 0 == call, 1 == delegate call. + function exec( + address to, + uint256 value, + bytes memory data, + Enum.Operation operation + ) internal virtual override returns (bool success) { + address currentGuard = guard; + if (currentGuard != address(0)) { + IGuard(currentGuard).checkTransaction( + /// Transaction info used by module transactions. + to, + value, + data, + operation, + /// Zero out the redundant transaction information only used for Safe multisig transctions. + 0, + 0, + 0, + address(0), + payable(0), + "", + sentOrSignedBy() + ); + } + success = IAvatar(target).execTransactionFromModule( + to, + value, + data, + operation + ); + if (currentGuard != address(0)) { + IGuard(currentGuard).checkAfterExecution(bytes32(0), success); + } + } + + /// @dev Passes a transaction to be executed by the target and returns data. + /// @notice Can only be called by this contract. + /// @param to Destination address of module transaction. + /// @param value Ether value of module transaction. + /// @param data Data payload of module transaction. + /// @param operation Operation type of module transaction: 0 == call, 1 == delegate call. + function execAndReturnData( + address to, + uint256 value, + bytes memory data, + Enum.Operation operation + ) + internal + virtual + override + returns (bool success, bytes memory returnData) + { + address currentGuard = guard; + if (currentGuard != address(0)) { + IGuard(currentGuard).checkTransaction( + /// Transaction info used by module transactions. + to, + value, + data, + operation, + /// Zero out the redundant transaction information only used for Safe multisig transctions. + 0, + 0, + 0, + address(0), + payable(0), + "", + sentOrSignedBy() + ); + } + + (success, returnData) = IAvatar(target) + .execTransactionFromModuleReturnData(to, value, data, operation); + + if (currentGuard != address(0)) { + IGuard(currentGuard).checkAfterExecution(bytes32(0), success); + } + } +} diff --git a/contracts/core/GuardableModule.sol b/contracts/core/GuardableModule.sol new file mode 100644 index 00000000..94fb3fb2 --- /dev/null +++ b/contracts/core/GuardableModule.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.7.0 <0.9.0; + +import "../guard/Guardable.sol"; +import "./Module.sol"; + +/// @title GuardableModule - A contract that can pass messages to a Module Manager contract if enabled by that contract. +abstract contract GuardableModule is Module, Guardable { + /// @dev Passes a transaction to be executed by the avatar. + /// @notice Can only be called by this contract. + /// @param to Destination address of module transaction. + /// @param value Ether value of module transaction. + /// @param data Data payload of module transaction. + /// @param operation Operation type of module transaction: 0 == call, 1 == delegate call. + function exec( + address to, + uint256 value, + bytes memory data, + Enum.Operation operation + ) internal override returns (bool success) { + address currentGuard = guard; + if (currentGuard != address(0)) { + IGuard(currentGuard).checkTransaction( + /// Transaction info used by module transactions. + to, + value, + data, + operation, + /// Zero out the redundant transaction information only used for Safe multisig transctions. + 0, + 0, + 0, + address(0), + payable(0), + "", + msg.sender + ); + } + success = IAvatar(target).execTransactionFromModule( + to, + value, + data, + operation + ); + if (currentGuard != address(0)) { + IGuard(currentGuard).checkAfterExecution(bytes32(0), success); + } + } + + /// @dev Passes a transaction to be executed by the target and returns data. + /// @notice Can only be called by this contract. + /// @param to Destination address of module transaction. + /// @param value Ether value of module transaction. + /// @param data Data payload of module transaction. + /// @param operation Operation type of module transaction: 0 == call, 1 == delegate call. + function execAndReturnData( + address to, + uint256 value, + bytes memory data, + Enum.Operation operation + ) + internal + virtual + override + returns (bool success, bytes memory returnData) + { + address currentGuard = guard; + if (currentGuard != address(0)) { + IGuard(currentGuard).checkTransaction( + /// Transaction info used by module transactions. + to, + value, + data, + operation, + /// Zero out the redundant transaction information only used for Safe multisig transctions. + 0, + 0, + 0, + address(0), + payable(0), + "", + msg.sender + ); + } + + (success, returnData) = IAvatar(target) + .execTransactionFromModuleReturnData(to, value, data, operation); + + if (currentGuard != address(0)) { + IGuard(currentGuard).checkAfterExecution(bytes32(0), success); + } + } +} diff --git a/contracts/core/Modifier.sol b/contracts/core/Modifier.sol index e992e552..f2ce79c3 100644 --- a/contracts/core/Modifier.sol +++ b/contracts/core/Modifier.sol @@ -1,12 +1,18 @@ // SPDX-License-Identifier: LGPL-3.0-only - -/// @title Modifier Interface - A contract that sits between a Module and an Avatar and enforce some additional logic. pragma solidity >=0.7.0 <0.9.0; import "../interfaces/IAvatar.sol"; +import "../signature/ExecutionTracker.sol"; +import "../signature/SignatureChecker.sol"; import "./Module.sol"; -abstract contract Modifier is Module, IAvatar { +/// @title Modifier Interface - A contract that sits between a Module and an Avatar and enforce some additional logic. +abstract contract Modifier is + Module, + ExecutionTracker, + SignatureChecker, + IAvatar +{ address internal constant SENTINEL_MODULES = address(0x1); /// Mapping of modules. mapping(address => address) internal modules; @@ -32,8 +38,10 @@ abstract contract Modifier is Module, IAvatar { /* -------------------------------------------------- - You must override at least one of following two virtual functions, + You must override both of the following virtual functions, execTransactionFromModule() and execTransactionFromModuleReturnData(). + It is recommended that implementations of both functions make use the + onlyModule modifier. */ /// @dev Passes a transaction to the modifier. @@ -47,7 +55,7 @@ abstract contract Modifier is Module, IAvatar { uint256 value, bytes calldata data, Enum.Operation operation - ) public virtual override moduleOnly returns (bool success) {} + ) public virtual returns (bool success); /// @dev Passes a transaction to the modifier, expects return data. /// @notice Can only be called by enabled modules. @@ -60,23 +68,46 @@ abstract contract Modifier is Module, IAvatar { uint256 value, bytes calldata data, Enum.Operation operation - ) - public - virtual - override - moduleOnly - returns (bool success, bytes memory returnData) - {} + ) public virtual returns (bool success, bytes memory returnData); /* -------------------------------------------------- */ modifier moduleOnly() { - if (modules[msg.sender] == address(0)) revert NotAuthorized(msg.sender); + if (modules[msg.sender] == address(0)) { + (bytes32 hash, address signer) = moduleTxSignedBy(); + + // is the signer a module? + if (modules[signer] == address(0)) { + revert NotAuthorized(msg.sender); + } + + // is the provided signature fresh? + if (consumed[signer][hash]) { + revert HashAlreadyConsumed(hash); + } + + consumed[signer][hash] = true; + emit HashExecuted(hash); + } + _; } + function sentOrSignedBy() internal view returns (address) { + if (modules[msg.sender] != address(0)) { + return msg.sender; + } + + (, address signer) = moduleTxSignedBy(); + if (modules[signer] != address(0)) { + return signer; + } + + return address(0); + } + /// @dev Disables a module on the modifier. /// @notice This can only be called by the owner. /// @param prevModule Module that pointed to the module to be removed in the linked list. diff --git a/contracts/core/Module.sol b/contracts/core/Module.sol index 04b292a8..dbf37f95 100644 --- a/contracts/core/Module.sol +++ b/contracts/core/Module.sol @@ -1,13 +1,11 @@ // SPDX-License-Identifier: LGPL-3.0-only - -/// @title Module Interface - A contract that can pass messages to a Module Manager contract if enabled by that contract. pragma solidity >=0.7.0 <0.9.0; -import "../interfaces/IAvatar.sol"; import "../factory/FactoryFriendly.sol"; -import "../guard/Guardable.sol"; +import "../interfaces/IAvatar.sol"; -abstract contract Module is FactoryFriendly, Guardable { +/// @title Module Interface - A contract that can pass messages to a Module Manager contract if enabled by that contract. +abstract contract Module is FactoryFriendly { /// @dev Address that will ultimately execute function calls. address public avatar; /// @dev Address that this module will pass transactions to. @@ -45,8 +43,14 @@ abstract contract Module is FactoryFriendly, Guardable { uint256 value, bytes memory data, Enum.Operation operation - ) internal returns (bool success) { - (success, ) = _exec(to, value, data, operation); + ) internal virtual returns (bool success) { + return + IAvatar(target).execTransactionFromModule( + to, + value, + data, + operation + ); } /// @dev Passes a transaction to be executed by the target and returns data. @@ -60,49 +64,13 @@ abstract contract Module is FactoryFriendly, Guardable { uint256 value, bytes memory data, Enum.Operation operation - ) internal returns (bool success, bytes memory returnData) { - (success, returnData) = _exec(to, value, data, operation); - } - - function _exec( - address to, - uint256 value, - bytes memory data, - Enum.Operation operation - ) private returns (bool success, bytes memory returnData) { - address currentGuard = guard; - if (currentGuard != address(0)) { - IGuard(currentGuard).checkTransaction( - /// Transaction info used by module transactions. + ) internal virtual returns (bool success, bytes memory returnData) { + return + IAvatar(target).execTransactionFromModuleReturnData( to, value, data, - operation, - /// Zero out the redundant transaction information only used for Safe multisig transctions. - 0, - 0, - 0, - address(0), - payable(0), - "", - msg.sender + operation ); - (success, returnData) = IAvatar(target) - .execTransactionFromModuleReturnData( - to, - value, - data, - operation - ); - IGuard(currentGuard).checkAfterExecution("", success); - } else { - (success, returnData) = IAvatar(target) - .execTransactionFromModuleReturnData( - to, - value, - data, - operation - ); - } } } diff --git a/contracts/factory/ModuleProxyFactory.sol b/contracts/factory/ModuleProxyFactory.sol index b2e2921b..ff2e26dc 100644 --- a/contracts/factory/ModuleProxyFactory.sol +++ b/contracts/factory/ModuleProxyFactory.sol @@ -19,10 +19,10 @@ contract ModuleProxyFactory { /// @notice Initialization failed. error FailedInitialization(); - function createProxy(address target, bytes32 salt) - internal - returns (address result) - { + function createProxy( + address target, + bytes32 salt + ) internal returns (address result) { if (address(target) == address(0)) revert ZeroAddress(target); if (address(target).code.length == 0) revert TargetHasNoCode(target); bytes memory deployment = abi.encodePacked( diff --git a/contracts/guard/BaseGuard.sol b/contracts/guard/BaseGuard.sol index 96d3d8c7..6e892bbc 100644 --- a/contracts/guard/BaseGuard.sol +++ b/contracts/guard/BaseGuard.sol @@ -6,12 +6,9 @@ import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "../interfaces/IGuard.sol"; abstract contract BaseGuard is IERC165 { - function supportsInterface(bytes4 interfaceId) - external - pure - override - returns (bool) - { + function supportsInterface( + bytes4 interfaceId + ) external pure override returns (bool) { return interfaceId == type(IGuard).interfaceId || // 0xe6d7a83a interfaceId == type(IERC165).interfaceId; // 0x01ffc9a7 diff --git a/contracts/interfaces/IAvatar.sol b/contracts/interfaces/IAvatar.sol index c757611d..7c4ebfdc 100644 --- a/contracts/interfaces/IAvatar.sol +++ b/contracts/interfaces/IAvatar.sol @@ -64,8 +64,8 @@ interface IAvatar { /// @param pageSize Maximum number of modules that should be returned. /// @return array Array of modules. /// @return next Start of the next page. - function getModulesPaginated(address start, uint256 pageSize) - external - view - returns (address[] memory array, address next); + function getModulesPaginated( + address start, + uint256 pageSize + ) external view returns (address[] memory array, address next); } diff --git a/contracts/signature/ExecutionTracker.sol b/contracts/signature/ExecutionTracker.sol new file mode 100644 index 00000000..ad88b1d4 --- /dev/null +++ b/contracts/signature/ExecutionTracker.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0 <0.9.0; + +/// @title ExecutionTracker - A contract that keeps track of executed and invalidated hashes +contract ExecutionTracker { + error HashAlreadyConsumed(bytes32); + + event HashExecuted(bytes32); + event HashInvalidated(bytes32); + + mapping(address => mapping(bytes32 => bool)) public consumed; + + function invalidate(bytes32 hash) external { + consumed[msg.sender][hash] = true; + emit HashInvalidated(hash); + } +} diff --git a/contracts/signature/IERC1271.sol b/contracts/signature/IERC1271.sol new file mode 100644 index 00000000..90e1927b --- /dev/null +++ b/contracts/signature/IERC1271.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: LGPL-3.0-only +/* solhint-disable one-contract-per-file */ +pragma solidity >=0.7.0 <0.9.0; + +interface IERC1271 { + /** + * @notice EIP1271 method to validate a signature. + * @param hash Hash of the data signed on the behalf of address(this). + * @param signature Signature byte array associated with _data. + * + * MUST return the bytes4 magic value 0x1626ba7e when function passes. + * MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5) + * MUST allow external calls + */ + function isValidSignature( + bytes32 hash, + bytes memory signature + ) external view returns (bytes4); +} diff --git a/contracts/signature/SignatureChecker.sol b/contracts/signature/SignatureChecker.sol new file mode 100644 index 00000000..21d1d145 --- /dev/null +++ b/contracts/signature/SignatureChecker.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0 <0.9.0; + +import "./IERC1271.sol"; + +/// @title SignatureChecker - A contract that retrieves and validates signatures appended to transaction calldata. +/// @dev currently supports eip-712 and eip-1271 signatures +abstract contract SignatureChecker { + /** + * @notice Searches for a signature, validates it, and returns the signer's address. + * @dev When signature not found or invalid, zero address is returned + * @return The address of the signer. + */ + function moduleTxSignedBy() internal view returns (bytes32, address) { + bytes calldata data = msg.data; + + /* + * The idea is to extend `onlyModule` and provide signature checking + * without code changes to inheriting contracts (Modifiers). + * + * Since it's a generic mechanism, there is no way to conclusively + * identify the trailing bytes as a signature. We simply slice those + * and recover signer. + * + * As a result, we impose a minimum calldata length equal to a function + * selector plus salt, plus a signature (i.e., 4 + 32 + 65 bytes), any + * shorter and calldata it guaranteed to not contain a signature. + */ + if (data.length < 4 + 32 + 65) { + return (bytes32(0), address(0)); + } + + (uint8 v, bytes32 r, bytes32 s) = _splitSignature(data); + + uint256 end = data.length - (32 + 65); + bytes32 salt = bytes32(data[end:]); + + /* + * When handling contract signatures: + * v - is zero + * r - contains the signer + * s - contains the offset within calldata where the signer specific + * signature is located + * + * We detect contract signatures by checking: + * 1- `v` is zero + * 2- `s` points within the buffer, is after selector, is before + * salt and delimits a non-zero length buffer + */ + if (v == 0) { + uint256 start = uint256(s); + if (start < 4 || start > end) { + return (bytes32(0), address(0)); + } + address signer = address(uint160(uint256(r))); + + bytes32 hash = moduleTxHash(data[:start], salt); + return + _isValidContractSignature(signer, hash, data[start:end]) + ? (hash, signer) + : (bytes32(0), address(0)); + } else { + bytes32 hash = moduleTxHash(data[:end], salt); + return (hash, ecrecover(hash, v, r, s)); + } + } + + /** + * @notice Hashes the transaction EIP-712 data structure. + * @dev The produced hash is intended to be signed. + * @param data The current transaction's calldata. + * @param salt The salt value. + * @return The 32-byte hash that is to be signed. + */ + function moduleTxHash( + bytes calldata data, + bytes32 salt + ) public view returns (bytes32) { + bytes32 domainSeparator = keccak256( + abi.encode(DOMAIN_SEPARATOR_TYPEHASH, block.chainid, this) + ); + bytes memory moduleTxData = abi.encodePacked( + bytes1(0x19), + bytes1(0x01), + domainSeparator, + keccak256(abi.encode(MODULE_TX_TYPEHASH, keccak256(data), salt)) + ); + return keccak256(moduleTxData); + } + + /** + * @dev Extracts signature from calldata, and divides it into `uint8 v, bytes32 r, bytes32 s`. + * @param data The current transaction's calldata. + * @return v The ECDSA v value + * @return r The ECDSA r value + * @return s The ECDSA s value + */ + function _splitSignature( + bytes calldata data + ) private pure returns (uint8 v, bytes32 r, bytes32 s) { + v = uint8(bytes1(data[data.length - 1:])); + r = bytes32(data[data.length - 65:]); + s = bytes32(data[data.length - 33:]); + } + + /** + * @dev Calls the signer contract, and validates the contract signature. + * @param signer The address of the signer contract. + * @param hash Hash of the data signed + * @param signature The contract signature. + * @return result Indicates whether the signature is valid. + */ + function _isValidContractSignature( + address signer, + bytes32 hash, + bytes calldata signature + ) internal view returns (bool result) { + uint256 size; + assembly { + size := extcodesize(signer) + } + if (size == 0) { + return false; + } + + (, bytes memory returnData) = signer.staticcall( + abi.encodeWithSelector( + IERC1271.isValidSignature.selector, + hash, + signature + ) + ); + + return bytes4(returnData) == EIP1271_MAGIC_VALUE; + } + + // keccak256( + // "EIP712Domain(uint256 chainId,address verifyingContract)" + // ); + bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = + 0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218; + + // keccak256( + // "ModuleTx(bytes data,bytes32 salt)" + // ); + bytes32 private constant MODULE_TX_TYPEHASH = + 0x2939aeeda3ca260200c9f7b436b19e13207547ccc65cfedc857751c5ea6d91d4; + + // bytes4(keccak256( + // "isValidSignature(bytes32,bytes)" + // )); + bytes4 private constant EIP1271_MAGIC_VALUE = 0x1626ba7e; +} diff --git a/contracts/test/TestAvatar.sol b/contracts/test/TestAvatar.sol index e0309848..a3399efd 100644 --- a/contracts/test/TestAvatar.sol +++ b/contracts/test/TestAvatar.sol @@ -51,11 +51,10 @@ contract TestAvatar { else (success, returnData) = to.call{value: value}(data); } - function getModulesPaginated(address, uint256 pageSize) - external - view - returns (address[] memory array, address next) - { + function getModulesPaginated( + address, + uint256 pageSize + ) external view returns (address[] memory array, address next) { // Init array with max page size array = new address[](pageSize); diff --git a/contracts/test/TestGuard.sol b/contracts/test/TestGuard.sol index de63f73b..72bb06b1 100644 --- a/contracts/test/TestGuard.sol +++ b/contracts/test/TestGuard.sol @@ -1,16 +1,10 @@ // SPDX-License-Identifier: LGPL-3.0-only - -/// @title Modifier Interface - A contract that sits between a Module and an Avatar and enforce some additional logic. pragma solidity >=0.7.0 <0.9.0; -import "../guard/BaseGuard.sol"; -import "../factory/FactoryFriendly.sol"; -import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import "../core/Module.sol"; +import "../core/GuardableModule.sol"; contract TestGuard is FactoryFriendly, BaseGuard { - event PreChecked(bool checked); + event PreChecked(address sender); event PostChecked(bool checked); address public module; @@ -35,18 +29,18 @@ contract TestGuard is FactoryFriendly, BaseGuard { address, address payable, bytes memory, - address + address sender ) public override { require(to != address(0), "Cannot send to zero address"); require(value != 1337, "Cannot send 1337"); require(bytes3(data) != bytes3(0xbaddad), "Cannot call 0xbaddad"); require(operation != Enum.Operation(1), "No delegate calls"); - emit PreChecked(true); + emit PreChecked(sender); } function checkAfterExecution(bytes32, bool) public override { require( - Module(module).guard() == address(this), + GuardableModule(module).guard() == address(this), "Module cannot remove its own guard." ); emit PostChecked(true); @@ -57,3 +51,25 @@ contract TestGuard is FactoryFriendly, BaseGuard { module = _module; } } + +contract TestNonCompliantGuard is IERC165 { + function supportsInterface(bytes4) external pure returns (bool) { + return false; + } + + function checkTransaction( + address, + uint256, + bytes memory, + Enum.Operation, + uint256, + uint256, + uint256, + address, + address, + bytes memory, + address + ) public {} + + function checkAfterExecution(bytes32, bool) public {} +} diff --git a/contracts/test/TestGuardableModifier.sol b/contracts/test/TestGuardableModifier.sol new file mode 100644 index 00000000..4b2e02c2 --- /dev/null +++ b/contracts/test/TestGuardableModifier.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.7.0 <0.9.0; + +import "../core/GuardableModifier.sol"; + +contract TestGuardableModifier is GuardableModifier { + event Executed( + address to, + uint256 value, + bytes data, + Enum.Operation operation, + bool success + ); + + event ExecutedAndReturnedData( + address to, + uint256 value, + bytes data, + Enum.Operation operation, + bytes returnData, + bool success + ); + + constructor(address _avatar, address _target) { + bytes memory initParams = abi.encode(_avatar, _target); + setUp(initParams); + } + + /// @dev Passes a transaction to the modifier. + /// @param to Destination address of module transaction + /// @param value Ether value of module transaction + /// @param data Data payload of module transaction + /// @param operation Operation type of module transaction + /// @notice Can only be called by enabled modules + function execTransactionFromModule( + address to, + uint256 value, + bytes calldata data, + Enum.Operation operation + ) public override moduleOnly returns (bool success) { + success = exec(to, value, data, operation); + emit Executed(to, value, data, operation, success); + } + + /// @dev Passes a transaction to the modifier, expects return data. + /// @param to Destination address of module transaction + /// @param value Ether value of module transaction + /// @param data Data payload of module transaction + /// @param operation Operation type of module transaction + /// @notice Can only be called by enabled modules + function execTransactionFromModuleReturnData( + address to, + uint256 value, + bytes calldata data, + Enum.Operation operation + ) + public + override + moduleOnly + returns (bool success, bytes memory returnData) + { + (success, returnData) = execAndReturnData(to, value, data, operation); + emit ExecutedAndReturnedData( + to, + value, + data, + operation, + returnData, + success + ); + } + + function setUp(bytes memory initializeParams) public override initializer { + setupModules(); + __Ownable_init(msg.sender); + (address _avatar, address _target) = abi.decode( + initializeParams, + (address, address) + ); + avatar = _avatar; + target = _target; + } + + function attemptToSetupModules() public { + setupModules(); + } +} diff --git a/contracts/test/TestModifier.sol b/contracts/test/TestModifier.sol index dac3c745..e2bcc468 100644 --- a/contracts/test/TestModifier.sol +++ b/contracts/test/TestModifier.sol @@ -6,7 +6,7 @@ pragma solidity >=0.7.0 <0.9.0; import "../core/Modifier.sol"; contract TestModifier is Modifier { - event executed( + event Executed( address to, uint256 value, bytes data, @@ -14,7 +14,7 @@ contract TestModifier is Modifier { bool success ); - event executedAndReturnedData( + event ExecutedAndReturnedData( address to, uint256 value, bytes data, @@ -41,7 +41,7 @@ contract TestModifier is Modifier { Enum.Operation operation ) public override moduleOnly returns (bool success) { success = exec(to, value, data, operation); - emit executed(to, value, data, operation, success); + emit Executed(to, value, data, operation, success); } /// @dev Passes a transaction to the modifier, expects return data. @@ -62,7 +62,7 @@ contract TestModifier is Modifier { returns (bool success, bytes memory returnData) { (success, returnData) = execAndReturnData(to, value, data, operation); - emit executedAndReturnedData( + emit ExecutedAndReturnedData( to, value, data, @@ -73,12 +73,12 @@ contract TestModifier is Modifier { } function setUp(bytes memory initializeParams) public override initializer { - setupModules(); - __Ownable_init(); (address _avatar, address _target) = abi.decode( initializeParams, (address, address) ); + setupModules(); + _transferOwnership(msg.sender); avatar = _avatar; target = _target; } diff --git a/contracts/test/TestModule.sol b/contracts/test/TestModule.sol index 0e8e7e2c..5e6d2ce4 100644 --- a/contracts/test/TestModule.sol +++ b/contracts/test/TestModule.sol @@ -3,9 +3,9 @@ /// @title Modifier Interface - A contract that sits between a Module and an Avatar and enforce some additional logic. pragma solidity >=0.7.0 <0.9.0; -import "../core/Module.sol"; +import "../core/GuardableModule.sol"; -contract TestModule is Module { +contract TestModule is GuardableModule { constructor(address _avatar, address _target) { bytes memory initParams = abi.encode(_avatar, _target); setUp(initParams); @@ -55,7 +55,7 @@ contract TestModule is Module { } function setUp(bytes memory initializeParams) public override initializer { - __Ownable_init(); + __Ownable_init(msg.sender); (address _avatar, address _target) = abi.decode( initializeParams, (address, address) diff --git a/contracts/test/TestSignature.sol b/contracts/test/TestSignature.sol new file mode 100644 index 00000000..1614cc26 --- /dev/null +++ b/contracts/test/TestSignature.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0; + +import "../signature/SignatureChecker.sol"; + +contract ContractSignerYes is IERC1271 { + function isValidSignature( + bytes32, + bytes memory + ) external pure override returns (bytes4) { + return 0x1626ba7e; + } +} + +contract ContractSignerNo is IERC1271 { + function isValidSignature( + bytes32, + bytes memory + ) external pure override returns (bytes4) { + return 0x33333333; + } +} + +contract ContractSignerMaybe is IERC1271 { + function isValidSignature( + bytes32, + bytes memory contractSpecificSignature + ) external pure override returns (bytes4) { + bool isValid = contractSpecificSignature.length == 6 && + bytes6(contractSpecificSignature) == 0x001122334455; + + return isValid ? bytes4(0x1626ba7e) : bytes4(0x33333333); + } +} + +contract ContractSignerOnlyEmpty is IERC1271 { + function isValidSignature( + bytes32, + bytes memory contractSpecificSignature + ) external pure override returns (bytes4) { + bool isValid = contractSpecificSignature.length == 0; + return isValid ? bytes4(0x1626ba7e) : bytes4(0x33333333); + } +} + +contract ContractSignerFaulty {} + +contract ContractSignerReturnSize { + function isValidSignature( + bytes32, + bytes memory + ) external pure returns (bytes2) { + return 0x1626; + } +} + +contract TestSignature is SignatureChecker { + event Hello(address signer); + + event Goodbye(address signer); + + function hello() public { + (, address signer) = moduleTxSignedBy(); + emit Hello(signer); + } + + function goodbye(uint256, bytes memory) public { + (, address signer) = moduleTxSignedBy(); + emit Goodbye(signer); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 02b6634d..53497ee1 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -53,7 +53,7 @@ export default { solidity: { compilers: [ { - version: "0.8.6", + version: "0.8.20", settings: { optimizer: { enabled: true, diff --git a/package.json b/package.json index facb29b8..85feb289 100644 --- a/package.json +++ b/package.json @@ -89,8 +89,8 @@ "dependencies": { "@gnosis.pm/mock-contract": "^4.0.0", "@gnosis.pm/safe-contracts": "1.3.0", - "@openzeppelin/contracts": "^4.8.1", - "@openzeppelin/contracts-upgradeable": "^4.8.1", + "@openzeppelin/contracts": "^5.0.0", + "@openzeppelin/contracts-upgradeable": "^5.0.0", "ethers": "^5.7.1" } } \ No newline at end of file diff --git a/test/01_IAvatar.spec.ts b/test/01_IAvatar.spec.ts index 60efd22c..8fae82e6 100644 --- a/test/01_IAvatar.spec.ts +++ b/test/01_IAvatar.spec.ts @@ -3,15 +3,16 @@ import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { expect } from "chai"; import hre from "hardhat"; +import { TestAvatar__factory } from "../typechain-types"; describe("IAvatar", async () => { async function setupTests() { + const [signer] = await hre.ethers.getSigners(); const Avatar = await hre.ethers.getContractFactory("TestAvatar"); - const avatar = await Avatar.deploy(); - const iAvatar = await hre.ethers.getContractAt("IAvatar", avatar.address); + const avatar = await Avatar.connect(signer).deploy(); + const iAvatar = TestAvatar__factory.connect(avatar.address, signer); const tx = { to: avatar.address, - value: 0, data: "0x", operation: 0, diff --git a/test/02_Module.spec.ts b/test/02_Module.spec.ts index 0e7fe537..f2d03b33 100644 --- a/test/02_Module.spec.ts +++ b/test/02_Module.spec.ts @@ -37,12 +37,12 @@ describe("Module", async () => { it("reverts if caller is not the owner", async () => { const { iAvatar, module } = await loadFixture(setupTests); - const [, wallet1] = await hre.ethers.getSigners(); + const [owner, wallet1] = await hre.ethers.getSigners(); await module.transferOwnership(wallet1.address); - await expect(module.setAvatar(iAvatar.address)).to.be.revertedWith( - "Ownable: caller is not the owner" - ); + await expect(module.setAvatar(iAvatar.address)) + .to.be.revertedWithCustomError(module, "OwnableUnauthorizedAccount") + .withArgs(owner.address); }); it("allows owner to set avatar", async () => { @@ -64,11 +64,11 @@ describe("Module", async () => { describe("setTarget", async () => { it("reverts if caller is not the owner", async () => { const { iAvatar, module } = await loadFixture(setupTests); - const [, wallet1] = await hre.ethers.getSigners(); + const [owner, wallet1] = await hre.ethers.getSigners(); await module.transferOwnership(wallet1.address); - await expect(module.setTarget(iAvatar.address)).to.be.revertedWith( - "Ownable: caller is not the owner" - ); + await expect(module.setTarget(iAvatar.address)) + .to.be.revertedWithCustomError(module, "OwnableUnauthorizedAccount") + .withArgs(owner.address); }); it("allows owner to set avatar", async () => { @@ -98,9 +98,7 @@ describe("Module", async () => { await module.setGuard(guard.address); await expect( module.executeTransaction(tx.to, tx.value, tx.data, tx.operation) - ) - .to.emit(guard, "PreChecked") - .withArgs(true); + ).to.emit(guard, "PreChecked"); }); it("executes a transaction", async () => { @@ -151,9 +149,7 @@ describe("Module", async () => { tx.data, tx.operation ) - ) - .to.emit(guard, "PreChecked") - .withArgs(true); + ).to.emit(guard, "PreChecked"); }); it("executes a transaction", async () => { diff --git a/test/03_Modifier.spec.ts b/test/03_Modifier.spec.ts index dfef65e5..30563533 100644 --- a/test/03_Modifier.spec.ts +++ b/test/03_Modifier.spec.ts @@ -1,18 +1,30 @@ +import hre from "hardhat"; +import { expect } from "chai"; +import { PopulatedTransaction } from "ethers"; + import { AddressZero } from "@ethersproject/constants"; import { AddressOne } from "@gnosis.pm/safe-contracts"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; -import { expect } from "chai"; -import hre from "hardhat"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +import typedDataForTransaction from "./typedDataForTransaction"; +import { TestAvatar__factory, TestModifier__factory } from "../typechain-types"; +import { keccak256, toUtf8Bytes } from "ethers/lib/utils"; describe("Modifier", async () => { const SENTINEL_MODULES = "0x0000000000000000000000000000000000000001"; async function setupTests() { + const [signer] = await hre.ethers.getSigners(); const Avatar = await hre.ethers.getContractFactory("TestAvatar"); - const avatar = await Avatar.deploy(); - const iAvatar = await hre.ethers.getContractAt("IAvatar", avatar.address); + const avatar = await Avatar.connect(signer).deploy(); + const iAvatar = TestAvatar__factory.connect(avatar.address, signer); const Modifier = await hre.ethers.getContractFactory("TestModifier"); - const modifier = await Modifier.deploy(iAvatar.address, iAvatar.address); + const modifier = await Modifier.connect(signer).deploy( + iAvatar.address, + iAvatar.address + ); + await iAvatar.enableModule(modifier.address); const tx = { to: avatar.address, @@ -28,7 +40,7 @@ describe("Modifier", async () => { }; return { iAvatar, - modifier, + modifier: TestModifier__factory.connect(modifier.address, signer), tx, }; } @@ -47,9 +59,9 @@ describe("Modifier", async () => { const [, user2] = await hre.ethers.getSigners(); - await expect( - modifier.connect(user2).enableModule(user2.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); + await expect(modifier.connect(user2).enableModule(user2.address)) + .to.be.revertedWithCustomError(modifier, "OwnableUnauthorizedAccount") + .withArgs(user2.address); }); it("reverts if module is zero address", async () => { @@ -94,7 +106,9 @@ describe("Modifier", async () => { const [, user2] = await hre.ethers.getSigners(); await expect( modifier.connect(user2).disableModule(SENTINEL_MODULES, user2.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); + ) + .to.be.revertedWithCustomError(modifier, "OwnableUnauthorizedAccount") + .withArgs(user2.address); }); it("reverts if module is zero address", async () => { @@ -240,9 +254,9 @@ describe("Modifier", async () => { const { modifier } = await loadFixture(setupTests); const [user1, user2, user3] = await hre.ethers.getSigners(); - await expect(modifier.enableModule(user1.address)); - await expect(modifier.enableModule(user2.address)); - await expect(modifier.enableModule(user3.address)); + await modifier.enableModule(user1.address); + await modifier.enableModule(user2.address); + await modifier.enableModule(user3.address); await expect(await modifier.isModuleEnabled(user1.address)).to.be.true; await expect(await modifier.isModuleEnabled(user2.address)).to.be.true; @@ -291,7 +305,6 @@ describe("Modifier", async () => { .to.be.revertedWithCustomError(modifier, "NotAuthorized") .withArgs("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); }); - it("execute a transaction.", async () => { const { modifier, tx } = await loadFixture(setupTests); const [user1] = await hre.ethers.getSigners(); @@ -306,7 +319,180 @@ describe("Modifier", async () => { tx.data, tx.operation ) - ).to.emit(modifier, "executed"); + ).to.emit(modifier, "Executed"); + }); + it("execute a transaction with signature.", async () => { + const { modifier, tx } = await loadFixture(setupTests); + const [user1, relayer] = await hre.ethers.getSigners(); + await expect(await modifier.enableModule(user1.address)) + .to.emit(modifier, "EnabledModule") + .withArgs(user1.address); + + const { from, ...transaction } = + await modifier.populateTransaction.execTransactionFromModule( + tx.to, + tx.value, + tx.data, + tx.operation + ); + + const signature = await sign( + modifier.address, + transaction, + keccak256(toUtf8Bytes("salt")), + user1 + ); + + const transactionWithSig = { + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }; + + await expect( + relayer.sendTransaction(transaction) + ).to.be.revertedWithCustomError(modifier, "NotAuthorized"); + + await expect(relayer.sendTransaction(transactionWithSig)).to.emit( + modifier, + "Executed" + ); + }); + it("reverts if signature not valid.", async () => { + const { modifier, tx } = await loadFixture(setupTests); + const [user1, user2, relayer] = await hre.ethers.getSigners(); + await expect(await modifier.enableModule(user1.address)) + .to.emit(modifier, "EnabledModule") + .withArgs(user1.address); + + const { from, ...transaction } = + await modifier.populateTransaction.execTransactionFromModule( + tx.to, + tx.value, + tx.data, + tx.operation + ); + + const signatureOk = await sign( + modifier.address, + transaction, + keccak256(toUtf8Bytes("salt")), + user1 + ); + const signatureBad = await sign( + modifier.address, + transaction, + keccak256(toUtf8Bytes("salt")), + user2 + ); + + const transactionWithBadSig = { + ...transaction, + data: `${transaction.data}${signatureBad.slice(2)}`, + }; + + const transactionWithOkSig = { + ...transaction, + data: `${transaction.data}${signatureOk.slice(2)}`, + }; + + await expect( + relayer.sendTransaction(transactionWithBadSig) + ).to.be.revertedWithCustomError(modifier, "NotAuthorized"); + + await expect(relayer.sendTransaction(transactionWithOkSig)).to.emit( + modifier, + "Executed" + ); + }); + it("reverts if signature previously used for execution.", async () => { + const { modifier, tx } = await loadFixture(setupTests); + const [user1, user2, relayer] = await hre.ethers.getSigners(); + await expect(await modifier.enableModule(user1.address)) + .to.emit(modifier, "EnabledModule") + .withArgs(user1.address); + + const { from, ...transaction } = + await modifier.populateTransaction.execTransactionFromModule( + tx.to, + tx.value, + tx.data, + tx.operation + ); + + const signatureOk = await sign( + modifier.address, + transaction, + keccak256(toUtf8Bytes("salt")), + user1 + ); + const signatureBad = await sign( + modifier.address, + transaction, + keccak256(toUtf8Bytes("salt")), + user2 + ); + + const transactionWithBadSig = { + ...transaction, + data: `${transaction.data}${signatureBad.slice(2)}`, + }; + + const transactionWithOkSig = { + ...transaction, + data: `${transaction.data}${signatureOk.slice(2)}`, + }; + + await expect( + relayer.sendTransaction(transactionWithBadSig) + ).to.be.revertedWithCustomError(modifier, "NotAuthorized"); + + await expect(relayer.sendTransaction(transactionWithOkSig)).to.emit( + modifier, + "Executed" + ); + + await expect( + relayer.sendTransaction(transactionWithOkSig) + ).to.be.revertedWithCustomError(modifier, "HashAlreadyConsumed"); + }); + it("reverts if signature invalidated.", async () => { + const { modifier, tx } = await loadFixture(setupTests); + const [user1, relayer] = await hre.ethers.getSigners(); + + await modifier.enableModule(user1.address); + + const { from, ...transaction } = + await modifier.populateTransaction.execTransactionFromModule( + tx.to, + tx.value, + tx.data, + tx.operation + ); + + const salt = keccak256(toUtf8Bytes("salt")); + + const signatureOk = await sign( + modifier.address, + transaction, + salt, + user1 + ); + + const transactionWithSig = { + ...transaction, + data: `${transaction.data}${signatureOk.slice(2)}`, + }; + + const hash = await modifier.moduleTxHash( + transaction.data as string, + salt + ); + + await modifier.invalidate(hash); + + await expect( + relayer.sendTransaction(transactionWithSig) + ).to.be.revertedWithCustomError(modifier, "HashAlreadyConsumed"); }); }); @@ -324,7 +510,6 @@ describe("Modifier", async () => { .to.be.revertedWithCustomError(modifier, "NotAuthorized") .withArgs("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); }); - it("execute a transaction.", async () => { const { modifier, tx } = await loadFixture(setupTests); const [user1] = await hre.ethers.getSigners(); @@ -339,7 +524,196 @@ describe("Modifier", async () => { tx.data, tx.operation ) - ).to.emit(modifier, "executedAndReturnedData"); + ).to.emit(modifier, "ExecutedAndReturnedData"); + }); + it("execute a transaction with signature.", async () => { + const { modifier, tx } = await loadFixture(setupTests); + const [user1, relayer] = await hre.ethers.getSigners(); + await expect(await modifier.enableModule(user1.address)) + .to.emit(modifier, "EnabledModule") + .withArgs(user1.address); + + const { from, ...transaction } = + await modifier.populateTransaction.execTransactionFromModuleReturnData( + tx.to, + tx.value, + tx.data, + tx.operation + ); + + const signature = await sign( + modifier.address, + transaction, + keccak256(toUtf8Bytes("salt")), + user1 + ); + + const transactionWithSig = { + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }; + + await expect( + relayer.sendTransaction(transaction) + ).to.be.revertedWithCustomError(modifier, "NotAuthorized"); + + await expect(relayer.sendTransaction(transactionWithSig)).to.emit( + modifier, + "ExecutedAndReturnedData" + ); + }); + it("reverts if signature not valid.", async () => { + const { modifier, tx } = await loadFixture(setupTests); + const [user1, user2, relayer] = await hre.ethers.getSigners(); + await expect(await modifier.enableModule(user1.address)) + .to.emit(modifier, "EnabledModule") + .withArgs(user1.address); + + const { from, ...transaction } = + await modifier.populateTransaction.execTransactionFromModuleReturnData( + tx.to, + tx.value, + tx.data, + tx.operation + ); + + const signatureBad = await sign( + modifier.address, + transaction, + keccak256(toUtf8Bytes("salt")), + user2 + ); + const signatureOk = await sign( + modifier.address, + transaction, + keccak256(toUtf8Bytes("salt")), + user1 + ); + + const transactionWithBadSig = { + ...transaction, + data: `${transaction.data}${signatureBad.slice(2)}`, + }; + + const transactionWithOkSig = { + ...transaction, + data: `${transaction.data}${signatureOk.slice(2)}`, + }; + + await expect( + relayer.sendTransaction(transactionWithBadSig) + ).to.be.revertedWithCustomError(modifier, "NotAuthorized"); + + await expect(relayer.sendTransaction(transactionWithOkSig)).to.emit( + modifier, + "ExecutedAndReturnedData" + ); + }); + it("reverts if signature previously used for execution.", async () => { + const { modifier, tx } = await loadFixture(setupTests); + const [user1, user2, relayer] = await hre.ethers.getSigners(); + await expect(await modifier.enableModule(user1.address)) + .to.emit(modifier, "EnabledModule") + .withArgs(user1.address); + + const { from, ...transaction } = + await modifier.populateTransaction.execTransactionFromModuleReturnData( + tx.to, + tx.value, + tx.data, + tx.operation + ); + + const signatureOk = await sign( + modifier.address, + transaction, + keccak256(toUtf8Bytes("salt")), + user1 + ); + const signatureBad = await sign( + modifier.address, + transaction, + keccak256(toUtf8Bytes("salt")), + user2 + ); + + const transactionWithBadSig = { + ...transaction, + data: `${transaction.data}${signatureBad.slice(2)}`, + }; + + const transactionWithOkSig = { + ...transaction, + data: `${transaction.data}${signatureOk.slice(2)}`, + }; + + await expect( + relayer.sendTransaction(transactionWithBadSig) + ).to.be.revertedWithCustomError(modifier, "NotAuthorized"); + + await expect(relayer.sendTransaction(transactionWithOkSig)).to.emit( + modifier, + "ExecutedAndReturnedData" + ); + + await expect( + relayer.sendTransaction(transactionWithOkSig) + ).to.be.revertedWithCustomError(modifier, "HashAlreadyConsumed"); + }); + it("reverts if signature invalidated.", async () => { + const { modifier, tx } = await loadFixture(setupTests); + const [user1, relayer] = await hre.ethers.getSigners(); + + await modifier.enableModule(user1.address); + + const { from, ...transaction } = + await modifier.populateTransaction.execTransactionFromModuleReturnData( + tx.to, + tx.value, + tx.data, + tx.operation + ); + + const salt = keccak256(toUtf8Bytes("salt")); + + const signatureOk = await sign( + modifier.address, + transaction, + salt, + user1 + ); + + const transactionWithSig = { + ...transaction, + data: `${transaction.data}${signatureOk.slice(2)}`, + }; + + const hash = await modifier.moduleTxHash( + transaction.data as string, + salt + ); + + await modifier.invalidate(hash); + + await expect( + relayer.sendTransaction(transactionWithSig) + ).to.be.revertedWithCustomError(modifier, "HashAlreadyConsumed"); }); }); }); + +async function sign( + contract: string, + transaction: PopulatedTransaction, + salt: string, + signer: SignerWithAddress +) { + const { domain, types, message } = typedDataForTransaction( + { contract, chainId: 31337, salt }, + transaction.data || "0x" + ); + + const signature = await signer._signTypedData(domain, types, message); + + return `${salt}${signature.slice(2)}`; +} diff --git a/test/04_Guard.spec.ts b/test/04_Guard.spec.ts index 0faced25..2914ddb2 100644 --- a/test/04_Guard.spec.ts +++ b/test/04_Guard.spec.ts @@ -1,32 +1,66 @@ -import { AddressZero } from "@ethersproject/constants"; -import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { expect } from "chai"; import hre from "hardhat"; -describe("Guardable", async () => { - async function setupTests() { - const Avatar = await hre.ethers.getContractFactory("TestAvatar"); - const avatar = await Avatar.deploy(); - const iAvatar = await hre.ethers.getContractAt("IAvatar", avatar.address); - const Module = await hre.ethers.getContractFactory("TestModule"); - const module = await Module.deploy(iAvatar.address, iAvatar.address); - await avatar.enableModule(module.address); - const Guard = await hre.ethers.getContractFactory("TestGuard"); - const guard = await Guard.deploy(module.address); - return { - iAvatar, - guard, - module, - }; - } +import { AddressZero } from "@ethersproject/constants"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { TestGuard__factory, TestModule__factory } from "../typechain-types"; + +async function setupTests() { + const [owner, other, relayer] = await hre.ethers.getSigners(); + const Avatar = await hre.ethers.getContractFactory("TestAvatar"); + const avatar = await Avatar.deploy(); + const Module = await hre.ethers.getContractFactory("TestModule"); + const module = TestModule__factory.connect( + (await Module.connect(owner).deploy(avatar.address, avatar.address)) + .address, + owner + ); + await avatar.enableModule(module.address); + + const Guard = await hre.ethers.getContractFactory("TestGuard"); + const guard = TestGuard__factory.connect( + (await Guard.deploy(module.address)).address, + relayer + ); + const GuardNonCompliant = await hre.ethers.getContractFactory( + "TestNonCompliantGuard" + ); + const guardNonCompliant = TestGuard__factory.connect( + (await GuardNonCompliant.deploy()).address, + hre.ethers.provider + ); + + const tx = { + to: avatar.address, + value: 0, + data: "0x", + operation: 0, + avatarTxGas: 0, + baseGas: 0, + gasPrice: 0, + gasToken: AddressZero, + refundReceiver: AddressZero, + signatures: "0x", + }; + + return { + owner, + other, + module, + guard, + guardNonCompliant, + tx, + }; +} + +describe("Guardable", async () => { describe("setGuard", async () => { it("reverts if reverts if caller is not the owner", async () => { - const { module } = await loadFixture(setupTests); - const [, user1] = await hre.ethers.getSigners(); - await expect( - module.connect(user1).setGuard(user1.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); + const { other, guard, module } = await loadFixture(setupTests); + await expect(module.connect(other).setGuard(guard.address)) + .to.be.revertedWithCustomError(module, "OwnableUnauthorizedAccount") + .withArgs(other.address); }); it("reverts if guard does not implement ERC165", async () => { @@ -34,12 +68,30 @@ describe("Guardable", async () => { await expect(module.setGuard(module.address)).to.be.reverted; }); + it("reverts if guard implements ERC165 and returns false", async () => { + const { module, guardNonCompliant } = await loadFixture(setupTests); + await expect(module.setGuard(guardNonCompliant.address)) + .to.be.revertedWithCustomError(module, "NotIERC165Compliant") + .withArgs(guardNonCompliant.address); + }); + it("sets module and emits event", async () => { const { module, guard } = await loadFixture(setupTests); await expect(module.setGuard(guard.address)) .to.emit(module, "ChangedGuard") .withArgs(guard.address); }); + + it("sets guard back to zero", async () => { + const { module, guard } = await loadFixture(setupTests); + await expect(module.setGuard(guard.address)) + .to.emit(module, "ChangedGuard") + .withArgs(guard.address); + + await expect(module.setGuard(AddressZero)) + .to.emit(module, "ChangedGuard") + .withArgs(AddressZero); + }); }); describe("getGuard", async () => { @@ -54,39 +106,15 @@ describe("BaseGuard", async () => { const txHash = "0x0000000000000000000000000000000000000000000000000000000000000001"; - async function setupTests() { - const Avatar = await hre.ethers.getContractFactory("TestAvatar"); - const avatar = await Avatar.deploy(); - const iAvatar = await hre.ethers.getContractAt("IAvatar", avatar.address); - const Module = await hre.ethers.getContractFactory("TestModule"); - const module = await Module.deploy(iAvatar.address, iAvatar.address); - await avatar.enableModule(module.address); - const Guard = await hre.ethers.getContractFactory("TestGuard"); - const guard = await Guard.deploy(module.address); - const tx = { - to: avatar.address, - value: 0, - data: "0x", - operation: 0, - avatarTxGas: 0, - baseGas: 0, - gasPrice: 0, - gasToken: AddressZero, - refundReceiver: AddressZero, - signatures: "0x", - }; - return { - iAvatar, - guard, - module, - tx, - }; - } + it("supports interface", async () => { + const { guard } = await loadFixture(setupTests); + expect(await guard.supportsInterface("0xe6d7a83a")).to.be.true; + expect(await guard.supportsInterface("0x01ffc9a7")).to.be.true; + }); describe("checkTransaction", async () => { it("reverts if test fails", async () => { const { guard, tx } = await loadFixture(setupTests); - const [user1] = await hre.ethers.getSigners(); await expect( guard.checkTransaction( tx.to, @@ -99,13 +127,12 @@ describe("BaseGuard", async () => { tx.gasToken, tx.refundReceiver, tx.signatures, - user1.address + AddressZero ) ).to.be.revertedWith("Cannot send 1337"); }); it("checks transaction", async () => { const { guard, tx } = await loadFixture(setupTests); - const [user1] = await hre.ethers.getSigners(); await expect( guard.checkTransaction( tx.to, @@ -118,11 +145,9 @@ describe("BaseGuard", async () => { tx.gasToken, tx.refundReceiver, tx.signatures, - user1.address + AddressZero ) - ) - .to.emit(guard, "PreChecked") - .withArgs(true); + ).to.emit(guard, "PreChecked"); }); }); diff --git a/test/06_SignatureChecker.spec.ts b/test/06_SignatureChecker.spec.ts new file mode 100644 index 00000000..6886b27a --- /dev/null +++ b/test/06_SignatureChecker.spec.ts @@ -0,0 +1,488 @@ +import hre from "hardhat"; +import { TestSignature__factory } from "../typechain-types"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { PopulatedTransaction } from "ethers"; +import { expect } from "chai"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +import typedDataForTransaction from "./typedDataForTransaction"; +import { + defaultAbiCoder, + keccak256, + solidityPack, + toUtf8Bytes, +} from "ethers/lib/utils"; + +describe("SignatureChecker", async () => { + async function setup() { + const [signer, relayer] = await hre.ethers.getSigners(); + const TestSignature = await hre.ethers.getContractFactory("TestSignature"); + const testSignature = await TestSignature.deploy(); + + return { + testSignature: TestSignature__factory.connect( + testSignature.address, + relayer + ), + signer, + relayer, + }; + } + + const AddressZero = "0x0000000000000000000000000000000000000000"; + + it("correctly detects an appended signature, for an entrypoint no arguments", async () => { + const { testSignature, signer, relayer } = await loadFixture(setup); + + const transaction = await testSignature.populateTransaction.hello(); + const signature = await sign( + testSignature.address, + transaction, + keccak256(toUtf8Bytes("Hello this is a salt")), + signer + ); + const transactionWithSig = { + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }; + + await expect(await relayer.sendTransaction(transaction)) + .to.emit(testSignature, "Hello") + .withArgs(AddressZero); + + await expect(await relayer.sendTransaction(transactionWithSig)) + .to.emit(testSignature, "Hello") + .withArgs(signer.address); + }); + + it("correctly detects an appended signature, entrypoint with arguments", async () => { + const { testSignature, signer, relayer } = await loadFixture(setup); + + const transaction = await testSignature.populateTransaction.goodbye( + 0, + "0xbadfed" + ); + const signature = await sign( + testSignature.address, + transaction, + keccak256(toUtf8Bytes("salt")), + signer + ); + const transactionWithSig = { + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }; + + await expect(await relayer.sendTransaction(transaction)) + .to.emit(testSignature, "Goodbye") + .withArgs(AddressZero); + + await expect(await relayer.sendTransaction(transactionWithSig)) + .to.emit(testSignature, "Goodbye") + .withArgs(signer.address); + }); + + describe("contract signature", () => { + it("s pointing out of bounds fails", async () => { + const { testSignature, relayer } = await loadFixture(setup); + + const ContractSigner = await hre.ethers.getContractFactory( + "ContractSignerYes" + ); + const signer = (await ContractSigner.deploy()).address; + + const transaction = await testSignature.populateTransaction.hello(); + + // 4 bytes of selector plus 3 bytes of custom signature + // an s of 4, 5 or 6 should be okay. 7 and higher should fail + let signature = makeContractSignature( + transaction, + "0xdddddd", + keccak256(toUtf8Bytes("salt")), + signer, + defaultAbiCoder.encode(["uint256"], [1000]) + ); + + await expect( + await relayer.sendTransaction({ + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }) + ) + .to.emit(testSignature, "Hello") + .withArgs(AddressZero); + + signature = makeContractSignature( + transaction, + "0xdddddd", + keccak256(toUtf8Bytes("salt")), + signer, + defaultAbiCoder.encode(["uint256"], [6]) + ); + + await expect( + await relayer.sendTransaction({ + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }) + ) + .to.emit(testSignature, "Hello") + .withArgs(signer); + }); + it("s pointing to selector fails", async () => { + const { testSignature, relayer } = await loadFixture(setup); + + const ContractSigner = await hre.ethers.getContractFactory( + "ContractSignerYes" + ); + const signer = (await ContractSigner.deploy()).address; + + const transaction = await testSignature.populateTransaction.hello(); + + let signature = makeContractSignature( + transaction, + "0xdddddd", + keccak256(toUtf8Bytes("salt")), + signer, + defaultAbiCoder.encode(["uint256"], [3]) + ); + + await expect( + await relayer.sendTransaction({ + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }) + ) + .to.emit(testSignature, "Hello") + .withArgs(AddressZero); + + signature = makeContractSignature( + transaction, + "0xdddddd", + keccak256(toUtf8Bytes("salt")), + signer, + defaultAbiCoder.encode(["uint256"], [4]) + ); + + await expect( + await relayer.sendTransaction({ + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }) + ) + .to.emit(testSignature, "Hello") + .withArgs(signer); + }); + it("s pointing to signature fails", async () => { + const { testSignature, relayer } = await loadFixture(setup); + + const ContractSigner = await hre.ethers.getContractFactory( + "ContractSignerYes" + ); + const signer = (await ContractSigner.deploy()).address; + + const transaction = await testSignature.populateTransaction.hello(); + + let signature = makeContractSignature( + transaction, + "0xdddddd", + keccak256(toUtf8Bytes("salt")), + signer, + defaultAbiCoder.encode(["uint256"], [60]) + ); + + await expect( + await relayer.sendTransaction({ + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }) + ) + .to.emit(testSignature, "Hello") + .withArgs(AddressZero); + + signature = makeContractSignature( + transaction, + "0xdddddd", + keccak256(toUtf8Bytes("salt")), + signer, + defaultAbiCoder.encode(["uint256"], [6]) + ); + + await expect( + await relayer.sendTransaction({ + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }) + ) + .to.emit(testSignature, "Hello") + .withArgs(signer); + }); + it("signer returns isValid maybe", async () => { + const { testSignature, relayer } = await loadFixture(setup); + + const ContractSigner = await hre.ethers.getContractFactory( + "ContractSignerMaybe" + ); + const contractSigner = await ContractSigner.deploy(); + + const transaction = await testSignature.populateTransaction.goodbye( + 0, + "0xbadfed" + ); + + const signatureGood = makeContractSignature( + transaction, + "0x001122334455", + keccak256(toUtf8Bytes("some irrelevant salt")), + contractSigner.address + ); + + const signatureBad = makeContractSignature( + transaction, + "0x00112233445566", + keccak256(toUtf8Bytes("some irrelevant salt")), + contractSigner.address + ); + + const transactionWithGoodSig = { + ...transaction, + data: `${transaction.data}${signatureGood.slice(2)}`, + }; + const transactionWithBadSig = { + ...transaction, + data: `${transaction.data}${signatureBad.slice(2)}`, + }; + + await expect(await relayer.sendTransaction(transaction)) + .to.emit(testSignature, "Goodbye") + .withArgs(AddressZero); + + await expect(await relayer.sendTransaction(transactionWithGoodSig)) + .to.emit(testSignature, "Goodbye") + .withArgs(contractSigner.address); + + await expect(await relayer.sendTransaction(transactionWithBadSig)) + .to.emit(testSignature, "Goodbye") + .withArgs(AddressZero); + }); + it("signer returns isValid yes", async () => { + const { testSignature, relayer } = await loadFixture(setup); + + const ContractSigner = await hre.ethers.getContractFactory( + "ContractSignerYes" + ); + const contractSigner = await ContractSigner.deploy(); + + const transaction = await testSignature.populateTransaction.goodbye( + 0, + "0xbadfed" + ); + + const signature = makeContractSignature( + transaction, + "0xaabbccddeeff", + keccak256(toUtf8Bytes("salt")), + contractSigner.address + ); + + const transactionWithSig = { + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }; + + await expect(await relayer.sendTransaction(transaction)) + .to.emit(testSignature, "Goodbye") + .withArgs(AddressZero); + + await expect(await relayer.sendTransaction(transactionWithSig)) + .to.emit(testSignature, "Goodbye") + .withArgs(contractSigner.address); + }); + it("signer returns isValid no", async () => { + const { testSignature, relayer } = await loadFixture(setup); + + const Signer = await hre.ethers.getContractFactory("ContractSignerNo"); + const signer = await Signer.deploy(); + + const transaction = await testSignature.populateTransaction.hello(); + + const signature = makeContractSignature( + transaction, + "0xaabbccddeeff", + keccak256(toUtf8Bytes("salt")), + signer.address + ); + + const transactionWithSig = { + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }; + + await expect(await relayer.sendTransaction(transactionWithSig)) + .to.emit(testSignature, "Hello") + .withArgs(AddressZero); + }); + + it("signer returns isValid for empty specific signature only", async () => { + const { testSignature, relayer } = await loadFixture(setup); + + const ContractSigner = await hre.ethers.getContractFactory( + "ContractSignerOnlyEmpty" + ); + const contractSigner = await ContractSigner.deploy(); + + const transaction = await testSignature.populateTransaction.goodbye( + 0, + "0xbadfed" + ); + + const signatureGood = makeContractSignature( + transaction, + "0x", + keccak256(toUtf8Bytes("some irrelevant salt")), + contractSigner.address + ); + + const signatureBad = makeContractSignature( + transaction, + "0xffff", + keccak256(toUtf8Bytes("some irrelevant salt")), + contractSigner.address + ); + + const transactionWithGoodSig = { + ...transaction, + data: `${transaction.data}${signatureGood.slice(2)}`, + }; + const transactionWithBadSig = { + ...transaction, + data: `${transaction.data}${signatureBad.slice(2)}`, + }; + + await expect(await relayer.sendTransaction(transaction)) + .to.emit(testSignature, "Goodbye") + .withArgs(AddressZero); + + await expect(await relayer.sendTransaction(transactionWithGoodSig)) + .to.emit(testSignature, "Goodbye") + .withArgs(contractSigner.address); + + await expect(await relayer.sendTransaction(transactionWithBadSig)) + .to.emit(testSignature, "Goodbye") + .withArgs(AddressZero); + }); + it("signer bad return size", async () => { + const { testSignature, relayer } = await loadFixture(setup); + + const Signer = await hre.ethers.getContractFactory( + "ContractSignerReturnSize" + ); + const signer = await Signer.deploy(); + + const transaction = await testSignature.populateTransaction.hello(); + + const signature = makeContractSignature( + transaction, + "0xaabbccddeeff", + keccak256(toUtf8Bytes("salt")), + signer.address + ); + + const transactionWithSig = { + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }; + + await expect(await relayer.sendTransaction(transactionWithSig)) + .to.emit(testSignature, "Hello") + .withArgs(AddressZero); + }); + it("signer with faulty entrypoint", async () => { + const { testSignature, relayer } = await loadFixture(setup); + + const Signer = await hre.ethers.getContractFactory( + "ContractSignerFaulty" + ); + const signer = await Signer.deploy(); + + const transaction = await testSignature.populateTransaction.hello(); + + const signature = makeContractSignature( + transaction, + "0xaabbccddeeff", + keccak256(toUtf8Bytes("salt")), + signer.address + ); + + const transactionWithSig = { + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }; + + await expect(await relayer.sendTransaction(transactionWithSig)) + .to.emit(testSignature, "Hello") + .withArgs(AddressZero); + }); + it("signer with no code deployed", async () => { + const { testSignature, relayer } = await loadFixture(setup); + + const signerAddress = "0x1234567890000000000000000000000123456789"; + + await expect(await hre.ethers.provider.getCode(signerAddress)).to.equal( + "0x" + ); + + const transaction = await testSignature.populateTransaction.hello(); + + const signature = makeContractSignature( + transaction, + "0xaabbccddeeff", + keccak256(toUtf8Bytes("salt")), + signerAddress + ); + + const transactionWithSig = { + ...transaction, + data: `${transaction.data}${signature.slice(2)}`, + }; + + await expect(await relayer.sendTransaction(transactionWithSig)) + .to.emit(testSignature, "Hello") + .withArgs(AddressZero); + }); + }); +}); + +async function sign( + contract: string, + transaction: PopulatedTransaction, + salt: string, + signer: SignerWithAddress +) { + const { domain, types, message } = typedDataForTransaction( + { contract, chainId: 31337, salt }, + transaction.data || "0x" + ); + + const signature = await signer._signTypedData(domain, types, message); + + return `${salt}${signature.slice(2)}`; +} + +function makeContractSignature( + transaction: PopulatedTransaction, + signerSpecificSignature: string, + salt: string, + r: string, + s?: string +) { + const dataBytesLength = ((transaction.data?.length as number) - 2) / 2; + + r = defaultAbiCoder.encode(["address"], [r]); + s = s || defaultAbiCoder.encode(["uint256"], [dataBytesLength]); + const v = solidityPack(["uint8"], [0]); + + return `${signerSpecificSignature}${salt.slice(2)}${r.slice(2)}${s.slice( + 2 + )}${v.slice(2)}`; +} diff --git a/test/07_GuardableModifier.spec.ts b/test/07_GuardableModifier.spec.ts new file mode 100644 index 00000000..40ff355a --- /dev/null +++ b/test/07_GuardableModifier.spec.ts @@ -0,0 +1,279 @@ +import hre from "hardhat"; +import { expect } from "chai"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; + +import { + TestAvatar__factory, + TestGuard__factory, + TestGuardableModifier__factory, +} from "../typechain-types"; +import typedDataForTransaction from "./typedDataForTransaction"; +import { PopulatedTransaction } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { keccak256, toUtf8Bytes } from "ethers/lib/utils"; + +describe("GuardableModifier", async () => { + async function setupTests() { + const [deployer, executor, signer, someone, relayer] = + await hre.ethers.getSigners(); + + const Avatar = await hre.ethers.getContractFactory("TestAvatar"); + const avatar = TestAvatar__factory.connect( + (await Avatar.deploy()).address, + deployer + ); + + const Modifier = await hre.ethers.getContractFactory( + "TestGuardableModifier" + ); + const modifier = TestGuardableModifier__factory.connect( + (await Modifier.connect(deployer).deploy(avatar.address, avatar.address)) + .address, + deployer + ); + const Guard = await hre.ethers.getContractFactory("TestGuard"); + const guard = TestGuard__factory.connect( + (await Guard.deploy(modifier.address)).address, + hre.ethers.provider + ); + + await avatar.enableModule(modifier.address); + await modifier.enableModule(executor.address); + + return { + executor, + signer, + someone, + relayer, + avatar, + guard, + modifier, + }; + } + + describe("exec", async () => { + it("skips guard pre-check if no guard is set", async () => { + const { avatar, modifier, executor } = await loadFixture(setupTests); + + await expect( + modifier + .connect(executor) + .execTransactionFromModule(avatar.address, 0, "0x", 0) + ).to.not.be.reverted; + }); + + it("pre-checks transaction if guard is set", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + await modifier.setGuard(guard.address); + + await expect( + modifier + .connect(executor) + .execTransactionFromModule(avatar.address, 0, "0x", 0) + ) + .to.emit(guard, "PreChecked") + .withArgs(executor.address); + }); + + it("pre-check gets called with signer when transaction is relayed", async () => { + const { signer, modifier, relayer, avatar, guard } = await loadFixture( + setupTests + ); + + await modifier.enableModule(signer.address); + await modifier.setGuard(guard.address); + + const inner = await avatar.populateTransaction.enableModule( + "0xff00000000000000000000000000000000ff3456" + ); + + const { from, ...transaction } = + await modifier.populateTransaction.execTransactionFromModule( + avatar.address, + 0, + inner.data as string, + 0 + ); + + const signature = await sign( + modifier.address, + transaction, + keccak256(toUtf8Bytes("salt")), + signer + ); + const transactionWithSig = { + ...transaction, + to: modifier.address, + data: `${transaction.data}${signature.slice(2)}`, + value: 0, + }; + + await expect(await relayer.sendTransaction(transactionWithSig)) + .to.emit(guard, "PreChecked") + .withArgs(signer.address); + }); + + it("pre-checks and reverts transaction if guard is set", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + await modifier.setGuard(guard.address); + + await expect( + modifier + .connect(executor) + .execTransactionFromModule(avatar.address, 1337, "0x", 0) + ).to.be.revertedWith("Cannot send 1337"); + }); + + it("skips post-check if no guard is enabled", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + + await expect( + modifier + .connect(executor) + .execTransactionFromModule(avatar.address, 0, "0x", 0) + ).not.to.emit(guard, "PostChecked"); + }); + + it("post-checks transaction if guard is set", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + await modifier.setGuard(guard.address); + + await expect( + modifier + .connect(executor) + .execTransactionFromModule(avatar.address, 0, "0x", 0) + ) + .to.emit(guard, "PostChecked") + .withArgs(true); + }); + }); + + describe("execAndReturnData", async () => { + it("skips guard pre-check if no guard is set", async () => { + const { avatar, modifier, executor } = await loadFixture(setupTests); + + await expect( + modifier + .connect(executor) + .execTransactionFromModuleReturnData(avatar.address, 0, "0x", 0) + ).to.not.be.reverted; + }); + + it("pre-checks transaction if guard is set", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + await modifier.setGuard(guard.address); + + await expect( + modifier + .connect(executor) + .execTransactionFromModuleReturnData(avatar.address, 0, "0x", 0) + ) + .to.emit(guard, "PreChecked") + .withArgs(executor.address); + }); + + it("pre-check gets called with signer when transaction is relayed", async () => { + const { signer, modifier, relayer, avatar, guard } = await loadFixture( + setupTests + ); + + await modifier.enableModule(signer.address); + await modifier.setGuard(guard.address); + + const inner = await avatar.populateTransaction.enableModule( + "0xff00000000000000000000000000000000ff3456" + ); + + const { from, ...transaction } = + await modifier.populateTransaction.execTransactionFromModuleReturnData( + avatar.address, + 0, + inner.data as string, + 0 + ); + + const signature = await sign( + modifier.address, + transaction, + keccak256(toUtf8Bytes("salt")), + signer + ); + const transactionWithSig = { + ...transaction, + to: modifier.address, + data: `${transaction.data}${signature.slice(2)}`, + value: 0, + }; + + await expect(await relayer.sendTransaction(transactionWithSig)) + .to.emit(guard, "PreChecked") + .withArgs(signer.address); + }); + + it("pre-checks and reverts transaction if guard is set", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + await modifier.setGuard(guard.address); + + await expect( + modifier + .connect(executor) + .execTransactionFromModuleReturnData(avatar.address, 1337, "0x", 0) + ).to.be.revertedWith("Cannot send 1337"); + }); + + it("skips post-check if no guard is enabled", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + + await expect( + modifier + .connect(executor) + .execTransactionFromModuleReturnData(avatar.address, 0, "0x", 0) + ).not.to.emit(guard, "PostChecked"); + }); + + it("post-checks transaction if guard is set", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + await modifier.setGuard(guard.address); + + await expect( + modifier + .connect(executor) + .execTransactionFromModuleReturnData(avatar.address, 0, "0x", 0) + ) + .to.emit(guard, "PostChecked") + .withArgs(true); + }); + }); +}); + +async function sign( + contract: string, + transaction: PopulatedTransaction, + salt: string, + signer: SignerWithAddress +) { + const { domain, types, message } = typedDataForTransaction( + { contract, chainId: 31337, salt }, + transaction.data || "0x" + ); + + const signature = await signer._signTypedData(domain, types, message); + + return `${salt}${signature.slice(2)}`; +} diff --git a/test/typedDataForTransaction.ts b/test/typedDataForTransaction.ts new file mode 100644 index 00000000..81f4d175 --- /dev/null +++ b/test/typedDataForTransaction.ts @@ -0,0 +1,28 @@ +import { BigNumberish } from "ethers"; + +export default function typedDataForTransaction( + { + contract, + chainId, + salt, + }: { + contract: string; + chainId: BigNumberish; + salt: string; + }, + data: string +) { + const domain = { verifyingContract: contract, chainId }; + const types = { + ModuleTx: [ + { type: "bytes", name: "data" }, + { type: "bytes32", name: "salt" }, + ], + }; + const message = { + data, + salt, + }; + + return { domain, types, message }; +} diff --git a/yarn.lock b/yarn.lock index f25a8c38..9dd61e90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -793,15 +793,15 @@ table "^6.8.0" undici "^5.14.0" -"@openzeppelin/contracts-upgradeable@^4.8.1": - version "4.8.3" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.8.3.tgz#6b076a7b751811b90fe3a172a7faeaa603e13a3f" - integrity sha512-SXDRl7HKpl2WDoJpn7CK/M9U4Z8gNXDHHChAKh0Iz+Wew3wu6CmFYBeie3je8V0GSXZAIYYwUktSrnW/kwVPtg== +"@openzeppelin/contracts-upgradeable@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.0.0.tgz#859c00c55f04b6dda85b3c88bce507d65019888f" + integrity sha512-D54RHzkOKHQ8xUssPgQe2d/U92mwaiBDY7qCCVGq6VqwQjsT3KekEQ3bonev+BLP30oZ0R1U6YC8/oLpizgC5Q== -"@openzeppelin/contracts@^4.8.1": - version "4.8.3" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.3.tgz#cbef3146bfc570849405f59cba18235da95a252a" - integrity sha512-bQHV8R9Me8IaJoJ2vPG4rXcL7seB7YVuskr4f+f5RyOStSZetwzkWtoqDMl5erkBJy0lDRUnIR2WIkPiC0GJlg== +"@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== "@pkgjs/parseargs@^0.11.0": version "0.11.0" @@ -5978,4 +5978,4 @@ yocto-queue@^0.1.0: zksync-web3@^0.14.3: version "0.14.3" resolved "https://registry.yarnpkg.com/zksync-web3/-/zksync-web3-0.14.3.tgz#64ac2a16d597464c3fc4ae07447a8007631c57c9" - integrity sha512-hT72th4AnqyLW1d5Jlv8N2B/qhEnl2NePK2A3org7tAa24niem/UAaHMkEvmWI3SF9waYUPtqAtjpf+yvQ9zvQ== \ No newline at end of file + integrity sha512-hT72th4AnqyLW1d5Jlv8N2B/qhEnl2NePK2A3org7tAa24niem/UAaHMkEvmWI3SF9waYUPtqAtjpf+yvQ9zvQ==