diff --git a/README.md b/README.md index c789aa5d6..ce7e0150a 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,10 @@ accounts ├─ Receiver — "Receiver mixin for ETH and safe-transferred ERC721 and ERC1155 tokens" ├─ Timelock — "Simple timelock" auth +├─ EnumerableRoles — "Enumerable multiroles authorization mixin" ├─ Ownable — "Simple single owner authorization mixin" ├─ OwnableRoles — "Simple single owner and multiroles authorization mixin" -├─ EnumerableRoles — "Enumerable multiroles authorization mixin" +├─ TimedRoles — "Timed multiroles authorization mixin" tokens ├─ ERC1155 — "Simple ERC1155 implementation" ├─ ERC20 — "Simple ERC20 + EIP-2612 implementation" diff --git a/src/Milady.sol b/src/Milady.sol index 21f76a074..b7f5de520 100644 --- a/src/Milady.sol +++ b/src/Milady.sol @@ -11,9 +11,10 @@ import "./accounts/LibERC6551.sol"; import "./accounts/LibERC7579.sol"; import "./accounts/Receiver.sol"; import "./accounts/Timelock.sol"; +import "./auth/EnumerableRoles.sol"; import "./auth/Ownable.sol"; import "./auth/OwnableRoles.sol"; -import "./auth/EnumerableRoles.sol"; +import "./auth/TimedRoles.sol"; import "./tokens/ERC1155.sol"; import "./tokens/ERC20.sol"; import "./tokens/ERC20Votes.sol"; diff --git a/src/auth/TimedRoles.sol b/src/auth/TimedRoles.sol new file mode 100644 index 000000000..cc0f68ba2 --- /dev/null +++ b/src/auth/TimedRoles.sol @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @notice Timed multiroles authorization mixin. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/auth/TimedRoles.sol) +/// +/// @dev Note: +/// This implementation is agnostic to the Ownable that the contract inherits from. +/// It performs a self-staticcall to the `owner()` function to determine the owner. +/// This is useful for situations where the contract inherits from +/// OpenZeppelin's Ownable, such as in LayerZero's OApp contracts. +/// +/// This implementation performs a self-staticcall to `MAX_TIMED_ROLE()` to determine +/// the maximum timed role that can be set/unset. If the inheriting contract does not +/// have `MAX_TIMED_ROLE()`, then any timed role can be set/unset. +/// +/// This implementation allows for any uint256 role, +/// it does NOT take in a bitmask of roles. +/// This is to accommodate teams that are allergic to bitwise flags. +/// +/// By default, the `owner()` is the only account that is authorized to set timed roles. +/// This behavior can be changed via overrides. +/// +/// This implementation is compatible with any Ownable. +/// This implementation is NOT compatible with OwnableRoles. +/// +/// As timed roles can turn active or inactive anytime, enumeration is omitted here. +/// Querying the number of active timed roles will cost `O(n)` instead of `O(1)`. +/// +/// Names are deliberately prefixed with "Timed", so that this contract +/// can be used in conjunction with EnumerableRoles without collisions. +abstract contract TimedRoles { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EVENTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The active time range of the timed role has been set. + event TimedRoleSet( + address indexed holder, uint256 indexed timedRole, uint40 start, uint40 expires + ); + + /// @dev `keccak256(bytes("TimedRoleSet(address,uint256,uint40,uint40)"))`. + uint256 private constant _TIMED_ROLE_SET_EVENT_SIGNATURE = + 0xf7b5bcd44281f9bd7dfe7227dbb5c96dafa8587339fe558592433e9d02ade7d7; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Cannot set the timed role of the zero address. + error TimedRoleHolderIsZeroAddress(); + + /// @dev The timed role has exceeded the maximum timed role. + error InvalidTimedRole(); + + /// @dev Unauthorized to perform the action. + error TimedRolesUnauthorized(); + + /// @dev The `expires` cannot be less than the `start`. + error InvalidTimedRoleRange(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The storage layout of the mapping is given by: + /// ``` + /// mstore(0x18, holder) + /// mstore(0x04, _TIMED_ROLES_SLOT_SEED) + /// mstore(0x00, timedRole) + /// let activeTimeRangeSlot := keccak256(0x00, 0x38) + /// ``` + /// Bits Layout: + /// - [0..39] `expires`. + /// - [216..255] `start`. + uint256 private constant _TIMED_ROLES_SLOT_SEED = 0x28900261; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* PUBLIC UPDATE FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Sets the active time range of `timedRole` of `holder` to [`start`, `expires`]. + /// The `timedRole` is active when `start <= block.timestamp && block.timestamp <= expires`. + function setTimedRole(address holder, uint256 timedRole, uint40 start, uint40 expires) + public + payable + virtual + { + _authorizeSetTimedRole(holder, timedRole, start, expires); + _setTimedRole(holder, timedRole, start, expires); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* PUBLIC READ FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns whether the `timedRole` is active for `holder` and the active time range. + function timedRoleActive(address holder, uint256 timedRole) + public + view + virtual + returns (bool isActive, uint40 start, uint40 expires) + { + /// @solidity memory-safe-assembly + assembly { + mstore(0x18, holder) + mstore(0x04, _TIMED_ROLES_SLOT_SEED) + mstore(0x00, timedRole) + let p := sload(keccak256(0x00, 0x38)) + start := shr(216, p) + expires := and(0xffffffffff, p) + isActive := iszero(or(lt(timestamp(), start), gt(timestamp(), expires))) + } + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Set the timed role for holder directly without authorization guard. + function _setTimedRole(address holder, uint256 timedRole, uint40 start, uint40 expires) + internal + virtual + { + _validateTimedRole(timedRole); + /// @solidity memory-safe-assembly + assembly { + let holder_ := shl(96, holder) + if iszero(holder_) { + mstore(0x00, 0x093a136f) // `TimedRoleHolderIsZeroAddress()`. + revert(0x1c, 0x04) + } + // Clean the upper bits. + start := and(0xffffffffff, start) + expires := and(0xffffffffff, expires) + // Validate the range. + if lt(expires, start) { + mstore(0x00, 0x3304dd8c) // `InvalidTimedRoleRange()`. + revert(0x1c, 0x04) + } + // Store the range. + mstore(0x18, holder) + mstore(0x04, _TIMED_ROLES_SLOT_SEED) + mstore(0x00, timedRole) + sstore(keccak256(0x00, 0x38), or(shl(216, start), expires)) + // Emit the {TimedRoleSet} event. + mstore(0x00, start) + mstore(0x20, expires) + log3(0x00, 0x40, _TIMED_ROLE_SET_EVENT_SIGNATURE, shr(96, holder_), timedRole) + } + } + + /// @dev Requires the timedRole is not greater than `MAX_TIMED_ROLE()`. + /// If `MAX_TIMED_ROLE()` is not implemented, this is an no-op. + function _validateTimedRole(uint256 timedRole) internal view virtual { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, 0x32bc6439) // `MAX_TIMED_ROLE()`. + if and( + and(gt(timedRole, mload(0x00)), gt(returndatasize(), 0x1f)), + staticcall(gas(), address(), 0x1c, 0x04, 0x00, 0x20) + ) { + mstore(0x00, 0x802ee27f) // `InvalidTimedRole()`. + revert(0x1c, 0x04) + } + } + } + + /// @dev Checks that the caller is authorized to set the timed role. + function _authorizeSetTimedRole(address holder, uint256 timedRole, uint40 start, uint40 expires) + internal + virtual + { + if (!_timedRolesSenderIsContractOwner()) _revertTimedRolesUnauthorized(); + // Silence compiler warning on unused variables. + (holder, timedRole, start, expires) = (holder, timedRole, start, expires); + } + + /// @dev Returns if `holder` has any roles in `encodedTimeRoles`. + /// `encodedTimeRoles` is `abi.encode(SAMPLE_TIMED_ROLE_0, SAMPLE_TIMED_ROLE_1, ...)`. + function _hasAnyTimedRoles(address holder, bytes memory encodedTimeRoles) + internal + view + virtual + returns (bool result) + { + /// @solidity memory-safe-assembly + assembly { + mstore(0x18, holder) + mstore(0x04, _TIMED_ROLES_SLOT_SEED) + let end := add(encodedTimeRoles, shl(5, shr(5, mload(encodedTimeRoles)))) + for {} lt(result, lt(encodedTimeRoles, end)) {} { + encodedTimeRoles := add(0x20, encodedTimeRoles) + mstore(0x00, mload(encodedTimeRoles)) + let p := sload(keccak256(0x00, 0x38)) + result := + iszero(or(lt(timestamp(), shr(216, p)), gt(timestamp(), and(0xffffffffff, p)))) + } + } + } + + /// @dev Reverts if `msg.sender` does not have `timedRole`. + function _checkTimedRole(uint256 timedRole) internal view virtual { + (bool isActive,,) = timedRoleActive(msg.sender, timedRole); + if (!isActive) _revertTimedRolesUnauthorized(); + } + + /// @dev Reverts if `msg.sender` does not have any timed role in `encodedTimedRoles`. + function _checkTimedRoles(bytes memory encodedTimedRoles) internal view virtual { + if (!_hasAnyTimedRoles(msg.sender, encodedTimedRoles)) _revertTimedRolesUnauthorized(); + } + + /// @dev Reverts if `msg.sender` is not the contract owner and does not have `timedRole`. + function _checkOwnerOrTimedRole(uint256 timedRole) internal view virtual { + if (!_timedRolesSenderIsContractOwner()) _checkTimedRole(timedRole); + } + + /// @dev Reverts if `msg.sender` is not the contract owner and + /// does not have any timed role in `encodedTimedRoles`. + function _checkOwnerOrTimedRoles(bytes memory encodedTimedRoles) internal view virtual { + if (!_timedRolesSenderIsContractOwner()) _checkTimedRoles(encodedTimedRoles); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* MODIFIERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Marks a function as only callable by an account with `timedRole`. + modifier onlyTimedRole(uint256 timedRole) virtual { + _checkTimedRole(timedRole); + _; + } + + /// @dev Marks a function as only callable by an account with any role in `encodedTimedRoles`. + /// `encodedTimedRoles` is `abi.encode(SAMPLE_TIMED_ROLE_0, SAMPLE_TIMED_ROLE_1, ...)`. + modifier onlyTimedRoles(bytes memory encodedTimedRoles) virtual { + _checkTimedRoles(encodedTimedRoles); + _; + } + + /// @dev Marks a function as only callable by the owner or by an account with `timedRole`. + modifier onlyOwnerOrTimedRole(uint256 timedRole) virtual { + _checkOwnerOrTimedRole(timedRole); + _; + } + + /// @dev Marks a function as only callable by the owner or + /// by an account with any role in `encodedTimedRoles`. + /// Checks for ownership first, then checks for roles. + /// `encodedTimedRoles` is `abi.encode(SAMPLE_TIMED_ROLE_0, SAMPLE_TIMED_ROLE_1, ...)`. + modifier onlyOwnerOrTimedRoles(bytes memory encodedTimedRoles) virtual { + _checkOwnerOrTimedRoles(encodedTimedRoles); + _; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* PRIVATE HELPERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns if the `msg.sender` is equal to `owner()` on this contract. + /// If the contract does not have `owner()` implemented, returns false. + function _timedRolesSenderIsContractOwner() private view returns (bool result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, 0x8da5cb5b) // `owner()`. + result := + and( + and(eq(caller(), mload(0x00)), gt(returndatasize(), 0x1f)), + staticcall(gas(), address(), 0x1c, 0x04, 0x00, 0x20) + ) + } + } + + /// @dev Reverts with `TimedRolesUnauthorized()`. + function _revertTimedRolesUnauthorized() private pure { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, 0xb0c7b036) // `TimedRolesUnauthorized()`. + revert(0x1c, 0x04) + } + } +} diff --git a/test/TimedRoles.t.sol b/test/TimedRoles.t.sol new file mode 100644 index 000000000..c108e31b7 --- /dev/null +++ b/test/TimedRoles.t.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {LibSort} from "../src/utils/LibSort.sol"; +import {DynamicArrayLib} from "../src/utils/DynamicArrayLib.sol"; +import "./utils/SoladyTest.sol"; +import "./utils/mocks/MockTimedRoles.sol"; + +contract TimedRolesTest is SoladyTest { + using DynamicArrayLib for *; + + event TimedRoleSet( + address indexed holder, uint256 indexed timedRole, uint40 start, uint40 expires + ); + + MockTimedRoles mockTimedRoles; + + function setUp() public { + mockTimedRoles = new MockTimedRoles(); + mockTimedRoles.setMaxTimedRole(type(uint256).max); + mockTimedRoles.setOwner(address(this)); + } + + struct TimedRoleConfig { + address holder; + uint256 role; + uint40 start; + uint40 expires; + } + + function _sampleTimedRoleConfig() internal returns (TimedRoleConfig memory c) { + uint256 m = 0xf00000000000000000000000000000000000000000000000000000000000000f; + c.holder = _randomNonZeroAddress(); + c.role = _randomUniform() & m; + (c.start, c.expires) = _sampleValidActiveTimeRange(); + } + + function _hasDuplicateKeys(TimedRoleConfig[] memory a) internal pure returns (bool) { + bytes32[] memory hashes = new bytes32[](a.length); + for (uint256 i; i != a.length; ++i) { + hashes[i] = keccak256(abi.encode(a[i].holder, a[i].role)); + } + LibSort.insertionSort(hashes); + LibSort.uniquifySorted(hashes); + return hashes.length != a.length; + } + + function _sampleTimedRoleConfigs() internal returns (TimedRoleConfig[] memory a) { + a = new TimedRoleConfig[](_randomUniform() & 3); + for (uint256 i; i != a.length; ++i) { + a[i] = _sampleTimedRoleConfig(); + } + } + + function _sampleActiveTimeRange() internal returns (uint40 start, uint40 expires) { + if (_randomChance(2)) { + start = uint40(_random()); + expires = uint40(_random()); + } else { + start = uint8(_random()); + expires = uint8(_random()); + } + } + + function _sampleValidActiveTimeRange() internal returns (uint40 start, uint40 expires) { + do { + (start, expires) = _sampleActiveTimeRange(); + } while (expires < start); + } + + function _sampleInvalidActiveTimeRange() internal returns (uint40 start, uint40 expires) { + do { + (start, expires) = _sampleActiveTimeRange(); + } while (!(expires < start)); + } + + function testSetAndGetTimedRoles(bytes32) public { + TimedRoleConfig[] memory a = _sampleTimedRoleConfigs(); + + uint256 targetTimestamp = _bound(_random(), 0, _randomChance(2) ? 0xff : 2 ** 41 - 1); + vm.warp(targetTimestamp); + + for (uint256 i; i != a.length; ++i) { + TimedRoleConfig memory c = a[i]; + vm.expectEmit(true, true, true, true); + emit TimedRoleSet(c.holder, c.role, c.start, c.expires); + mockTimedRoles.setTimedRole(c.holder, c.role, c.start, c.expires); + (bool isActive, uint40 start, uint40 expires) = + mockTimedRoles.timedRoleActive(c.holder, c.role); + assertEq(start, c.start); + assertEq(expires, c.expires); + assertEq(isActive, start <= targetTimestamp && targetTimestamp <= expires); + } + if (!_hasDuplicateKeys(a)) { + for (uint256 i; i != a.length; ++i) { + TimedRoleConfig memory c = a[i]; + (bool isActive, uint40 start, uint40 expires) = + mockTimedRoles.timedRoleActive(c.holder, c.role); + assertEq(start, c.start); + assertEq(expires, c.expires); + assertEq(isActive, start <= targetTimestamp && targetTimestamp <= expires); + } + } + if (_randomChance(16)) { + TimedRoleConfig memory c = _sampleTimedRoleConfig(); + (c.start, c.expires) = _sampleInvalidActiveTimeRange(); + vm.expectRevert(TimedRoles.InvalidTimedRoleRange.selector); + mockTimedRoles.setTimedRole(c.holder, c.role, c.start, c.expires); + } + if (_randomChance(16)) { + TimedRoleConfig memory c = _sampleTimedRoleConfig(); + mockTimedRoles.setOwner(_randomUniqueHashedAddress()); + vm.expectRevert(TimedRoles.TimedRolesUnauthorized.selector); + mockTimedRoles.setTimedRole(c.holder, c.role, c.start, c.expires); + mockTimedRoles.setOwner(address(this)); + if (_randomChance(16)) { + c.holder = address(0); + vm.expectRevert(TimedRoles.TimedRoleHolderIsZeroAddress.selector); + } + mockTimedRoles.setTimedRole(c.holder, c.role, c.start, c.expires); + } + if (_randomChance(16)) { + uint256 maxTimedRole = _random(); + mockTimedRoles.setMaxTimedRole(maxTimedRole); + TimedRoleConfig memory c = _sampleTimedRoleConfig(); + if (c.role > maxTimedRole) { + vm.expectRevert(TimedRoles.InvalidTimedRole.selector); + } + mockTimedRoles.setTimedRole(c.holder, c.role, c.start, c.expires); + } + } + + function testTimedRolesModifiers(bytes32) public { + TimedRoleConfig memory c = _sampleTimedRoleConfig(); + c.start = 0; + c.expires = 0xffffffffff; + mockTimedRoles.setTimedRole(c.holder, c.role, c.start, c.expires); + uint256[] memory allowedTimeRoles = _sampleRoles(3); + mockTimedRoles.setAllowedTimedRole(allowedTimeRoles[0]); + vm.warp(_bound(_randomUniform(), c.start, c.expires)); + + if (allowedTimeRoles[0] == c.role) { + vm.prank(c.holder); + mockTimedRoles.guardedByOnlyOwnerOrTimedRole(); + } else { + vm.prank(c.holder); + vm.expectRevert(TimedRoles.TimedRolesUnauthorized.selector); + mockTimedRoles.guardedByOnlyOwnerOrTimedRole(); + } + + mockTimedRoles.setAllowedTimedRolesEncoded(abi.encodePacked(allowedTimeRoles)); + + if (allowedTimeRoles.contains(c.role)) { + vm.prank(c.holder); + mockTimedRoles.guardedByOnlyOwnerOrTimedRoles(); + } else { + vm.prank(c.holder); + vm.expectRevert(TimedRoles.TimedRolesUnauthorized.selector); + mockTimedRoles.guardedByOnlyOwnerOrTimedRoles(); + } + + if (_randomChance(128)) { + mockTimedRoles.guardedByOnlyOwnerOrTimedRole(); + mockTimedRoles.guardedByOnlyOwnerOrTimedRoles(); + } + } + + function _sampleRoles(uint256 n) internal returns (uint256[] memory roles) { + unchecked { + uint256 m = 0xf00000000000000000000000000000000000000000000000000000000000000f; + roles = DynamicArrayLib.malloc(n); + for (uint256 i; i != n; ++i) { + roles.set(i, _randomUniform() & m); + } + } + } +} diff --git a/test/utils/mocks/MockTimedRoles.sol b/test/utils/mocks/MockTimedRoles.sol new file mode 100644 index 000000000..b178bd2fd --- /dev/null +++ b/test/utils/mocks/MockTimedRoles.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {TimedRoles} from "../../../src/auth/TimedRoles.sol"; +import {EnumerableRoles} from "../../../src/auth/EnumerableRoles.sol"; +import {Brutalizer} from "../Brutalizer.sol"; + +/// @dev WARNING! This mock is strictly intended for testing purposes only. +/// Do NOT copy anything here into production code unless you really know what you are doing. +contract MockTimedRoles is TimedRoles, EnumerableRoles, Brutalizer { + struct MockTimedRolesStorage { + uint256 maxTimedRole; + bool maxTimedRoleReverts; + address owner; + bool ownerReverts; + bytes allowedTimedRolesEncoded; + uint256 allowedTimedRole; + } + + event Yo(); + + MockTimedRolesStorage internal $; + + function setOwner(address value) public { + $.owner = value; + } + + function setOwnerReverts(bool value) public { + $.ownerReverts = value; + } + + function setMaxTimedRole(uint256 value) public { + $.maxTimedRole = value; + } + + function setMaxTimedRoleReverts(bool value) public { + $.maxTimedRoleReverts = value; + } + + function MAX_TIMED_ROLE() public view returns (uint256) { + if ($.maxTimedRoleReverts) revert(); + return $.maxTimedRole; + } + + function owner() public view returns (address) { + if ($.ownerReverts) revert(); + return $.owner; + } + + function setTimedRoleDirect(address holder, uint256 timedRole, uint40 start, uint40 end) + public + { + _setTimedRole(_brutalized(holder), timedRole, start, end); + } + + function hasAnyTimedRoles(address holder, bytes memory encodedTimedRoles) + public + view + returns (bool) + { + return _hasAnyTimedRoles(_brutalized(holder), encodedTimedRoles); + } + + function setAllowedTimedRolesEncoded(bytes memory value) public { + $.allowedTimedRolesEncoded = value; + } + + function setAllowedTimedRole(uint256 timedRole) public { + $.allowedTimedRole = timedRole; + } + + function guardedByOnlyOwnerOrTimedRoles() + public + onlyOwnerOrTimedRoles($.allowedTimedRolesEncoded) + { + emit Yo(); + } + + function guardedByOnlyOwnerOrTimedRole() public onlyOwnerOrTimedRole($.allowedTimedRole) { + emit Yo(); + } + + function guardedByOnlyTimedRoles() public onlyTimedRoles($.allowedTimedRolesEncoded) { + emit Yo(); + } +}