Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ TimedRoles #1246

Merged
merged 14 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion src/Milady.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
282 changes: 282 additions & 0 deletions src/auth/TimedRoles.sol
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Loading
Loading