diff --git a/CHANGELOG.md b/CHANGELOG.md index cbac3b59e..1dbaf1836 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- AccessControlDefaultAdminRules interface and component (#1432) + ## 2.0.0 (2025-06-18) ### Added diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index 9fd2d037c..e887b58b4 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -357,6 +357,8 @@ security practice. Note that each account may still have more than one role, if === Granting and revoking roles +:access-control-default-admin-rules: xref:api/access.adoc#AccessControlDefaultAdminRulesComponent[AccessControlDefaultAdminRules] + The ERC20 token example above uses xref:api/access.adoc#AccessControlComponent-_grant_role[`_grant_role`], an `internal` function that is useful when programmatically assigning roles (such as during construction). But what if we later want to grant the 'minter' role to additional accounts? @@ -378,6 +380,8 @@ of `0`, called `DEFAULT_ADMIN_ROLE`, which acts as the *default admin role for a An account with this role will be able to manage any other role, unless xref:api/access.adoc#AccessControlComponent-set_role_admin[`set_role_admin`] is used to select a new admin role. +Since it is the admin for all roles by default, and in fact it is also its own admin, this role carries significant risk. To mitigate this risk we provide {access-control-default-admin-rules}, a recommended extension of AccessControl that adds a number of enforced security measures for this role: the admin is restricted to a single account, with a 2-step transfer procedure with a delay in between steps. + Let's take a look at the ERC20 token example, this time taking advantage of the default admin role: [,cairo] diff --git a/docs/modules/ROOT/pages/api/access.adoc b/docs/modules/ROOT/pages/api/access.adoc index 393e27cde..d38e0916e 100644 --- a/docs/modules/ROOT/pages/api/access.adoc +++ b/docs/modules/ROOT/pages/api/access.adoc @@ -18,7 +18,7 @@ This mechanism can be useful in simple scenarios, but fine grained access needs - {AccessControl} provides a general role based access control mechanism. Multiple hierarchical roles can be created and assigned each to multiple accounts. -== Authorization +== Core [.contract] [[OwnableComponent]] @@ -503,9 +503,11 @@ that only accounts with this role will be able to grant or revoke other roles. More complex role relationships can be created by using {set_role_admin}. +:AccessControlDefaultAdminRulesComponent: xref:#AccessControlDefaultAdminRulesComponent[AccessControlDefaultAdminRulesComponent] + WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to grant and revoke this role. Extra precautions should be taken to secure -accounts that have been granted it. +accounts that have been granted it. See {AccessControlDefaultAdminRulesComponent}. [.contract-index#AccessControl-Mixin-Impl] .{mixin-impls} @@ -835,3 +837,907 @@ See xref:IAccessControlWithDelay-RoleGrantedWithDelay[IAccessControlWithDelay::R ==== `[.contract-item-name]#++RoleRevoked++#++(role: felt252, account: ContractAddress, sender: ContractAddress)++` [.item-kind]#event# See xref:IAccessControl-RoleRevoked[IAccessControl::RoleRevoked]. + +== Extensions + +[.contract] +[[IAccessControlDefaultAdminRules]] +=== `++IAccessControlDefaultAdminRules++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v2.0.0/packages/access/src/accesscontrol/extensions/interface.cairo[{github-icon},role=heading-link] + +:grant_role: xref:#IAccessControl-grant_role[grant_role] + +```cairo +use openzeppelin_access::accesscontrol::extensions::interface::IAccessControlDefaultAdminRules; +``` + +External interface of AccessControlDefaultAdminRules declared to support {src5} detection. + +[.contract-index] +.{inner-src5} +-- +0x3509b3083c9586afe5dae781146b0608c3846870510f8d4d21ae38676cc33eb +-- + +[.contract-index] +.Functions +-- +* xref:IAccessControlDefaultAdminRules-default_admin[`++default_admin()++`] +* xref:IAccessControlDefaultAdminRules-pending_default_admin[`++pending_default_admin()++`] +* xref:IAccessControlDefaultAdminRules-default_admin_delay[`++default_admin_delay()++`] +* xref:IAccessControlDefaultAdminRules-pending_default_admin_delay[`++pending_default_admin_delay()++`] +* xref:IAccessControlDefaultAdminRules-begin_default_admin_transfer[`++begin_default_admin_transfer(new_admin)++`] +* xref:IAccessControlDefaultAdminRules-cancel_default_admin_transfer[`++cancel_default_admin_transfer()++`] +* xref:IAccessControlDefaultAdminRules-accept_default_admin_transfer[`++accept_default_admin_transfer()++`] +* xref:IAccessControlDefaultAdminRules-change_default_admin_delay[`++change_default_admin_delay(new_delay)++`] +* xref:IAccessControlDefaultAdminRules-rollback_default_admin_delay[`++rollback_default_admin_delay()++`] +* xref:IAccessControlDefaultAdminRules-default_admin_delay_increase_wait[`++default_admin_delay_increase_wait()++`] +-- + +[.contract-index] +.Events +-- +* xref:IAccessControlDefaultAdminRules-DefaultAdminTransferScheduled[`++DefaultAdminTransferScheduled(new_admin, accept_schedule)++`] +* xref:IAccessControlDefaultAdminRules-DefaultAdminTransferCanceled[`++DefaultAdminTransferCanceled()++`] +* xref:IAccessControlDefaultAdminRules-DefaultAdminDelayChangeScheduled[`++DefaultAdminDelayChangeScheduled(new_delay, effect_schedule)++`] +* xref:IAccessControlDefaultAdminRules-DefaultAdminDelayChangeCanceled[`++DefaultAdminDelayChangeCanceled()++`] +-- + +:default_admin_transfer_scheduled: xref:IAccessControlDefaultAdminRules-DefaultAdminTransferScheduled[DefaultAdminTransferScheduled] +:default_admin_transfer_canceled: xref:IAccessControlDefaultAdminRules-DefaultAdminTransferCanceled[DefaultAdminTransferCanceled] +:default_admin_delay_change_scheduled: xref:IAccessControlDefaultAdminRules-DefaultAdminDelayChangeScheduled[DefaultAdminDelayChangeScheduled] +:default_admin_delay_change_canceled: xref:IAccessControlDefaultAdminRules-DefaultAdminDelayChangeCanceled[DefaultAdminDelayChangeCanceled] + +:default_admin: xref:IAccessControlDefaultAdminRules-default_admin[default_admin] +:pending_default_admin: xref:IAccessControlDefaultAdminRules-pending_default_admin[pending_default_admin] +:default_admin_delay: xref:IAccessControlDefaultAdminRules-default_admin_delay[default_admin_delay] +:pending_default_admin_delay: xref:IAccessControlDefaultAdminRules-pending_default_admin_delay[pending_default_admin_delay] +:begin_default_admin_transfer: xref:IAccessControlDefaultAdminRules-begin_default_admin_transfer[begin_default_admin_transfer] +:cancel_default_admin_transfer: xref:IAccessControlDefaultAdminRules-cancel_default_admin_transfer[cancel_default_admin_transfer] +:accept_default_admin_transfer: xref:IAccessControlDefaultAdminRules-accept_default_admin_transfer[accept_default_admin_transfer] +:change_default_admin_delay: xref:IAccessControlDefaultAdminRules-change_default_admin_delay[change_default_admin_delay] +:rollback_default_admin_delay: xref:IAccessControlDefaultAdminRules-rollback_default_admin_delay[rollback_default_admin_delay] +:default_admin_delay_increase_wait: xref:IAccessControlDefaultAdminRules-default_admin_delay_increase_wait[default_admin_delay_increase_wait] + +[#IAccessControlDefaultAdminRules-Functions] +==== Functions + +[.contract-item] +[[IAccessControlDefaultAdminRules-default_admin]] +==== `[.contract-item-name]#++default_admin++#++() → ContractAddress++` [.item-kind]#external# + +Returns the address of the current `DEFAULT_ADMIN_ROLE` holder. + +[.contract-item] +[[IAccessControlDefaultAdminRules-pending_default_admin]] +==== `[.contract-item-name]#++pending_default_admin++#++() → (ContractAddress, u64)++` [.item-kind]#external# + +Returns a tuple of a `new_admin` and an `accept_schedule`. + +After the `accept_schedule` passes, the `new_admin` will be able to accept the +`default_admin` role by calling {accept_default_admin_transfer}, completing the role +transfer. + +A zero value only in `accept_schedule` indicates no pending admin transfer. + +NOTE: A zero address `new_admin` means that `default_admin` is being renounced. + +[.contract-item] +[[IAccessControlDefaultAdminRules-default_admin_delay]] +==== `[.contract-item-name]#++default_admin_delay++#++() → u64++` [.item-kind]#external# + +Returns the delay required to schedule the acceptance of a `default_admin` transfer started. + +This delay will be added to the current timestamp when calling +{begin_default_admin_transfer} to set the acceptance schedule. + +NOTE: If a delay change has been scheduled, it will take effect as soon as the schedule +passes, making this function return the new delay. + +See {change_default_admin_delay}. + +[.contract-item] +[[IAccessControlDefaultAdminRules-pending_default_admin_delay]] +==== `[.contract-item-name]#++pending_default_admin_delay++#++() → (u64, u64)++` [.item-kind]#external# + +Returns a tuple of `new_delay` and an `effect_schedule`. + +After the `effect_schedule` passes, the `new_delay` will get into effect immediately for +every new `default_admin` transfer started with {begin_default_admin_transfer}. + +A zero value only in `effect_schedule` indicates no pending delay change. + +NOTE: A zero value only for `new_delay` means that the next {default_admin_delay} +will be zero after the effect schedule. + +[.contract-item] +[[IAccessControlDefaultAdminRules-begin_default_admin_transfer]] +==== `[.contract-item-name]#++begin_default_admin_transfer++#++(new_admin)++` [.item-kind]#external# + +Starts a `default_admin` transfer by setting a {pending_default_admin} scheduled for +acceptance after the current timestamp plus a {default_admin_delay}. + +Requirements: + +- Only can be called by the current `default_admin`. + +Emits a {default_admin_transfer_scheduled} event. + +[.contract-item] +[[IAccessControlDefaultAdminRules-cancel_default_admin_transfer]] +==== `[.contract-item-name]#++cancel_default_admin_transfer++#++()++` [.item-kind]#external# + +Cancels a `default_admin` transfer previously started with {begin_default_admin_transfer}. + +A {pending_default_admin} not yet accepted can also be cancelled with this function. + +Requirements: + +- Only can be called by the current `default_admin`. + +May emit a {default_admin_transfer_canceled} event. + +[.contract-item] +[[IAccessControlDefaultAdminRules-accept_default_admin_transfer]] +==== `[.contract-item-name]#++accept_default_admin_transfer++#++()++` [.item-kind]#external# + +Completes a `default_admin` transfer previously started with {begin_default_admin_transfer}. + +After calling the function: + +- `DEFAULT_ADMIN_ROLE` must be granted to the caller. +- `DEFAULT_ADMIN_ROLE` must be revoked from the previous holder. +- {pending_default_admin} must be reset to zero value. + +Requirements: + +- Only can be called by the {pending_default_admin}'s `new_admin`. +- The {pending_default_admin}'s `accept_schedule` should've passed. + +[.contract-item] +[[IAccessControlDefaultAdminRules-change_default_admin_delay]] +==== `[.contract-item-name]#++change_default_admin_delay++#++(new_delay)++` [.item-kind]#external# + +Initiates a {default_admin_delay} update by setting a {pending_default_admin_delay} +scheduled to take effect after the current timestamp plus a {default_admin_delay}. + +This function guarantees that any call to {begin_default_admin_transfer} done between the +timestamp this method is called and the {pending_default_admin_delay} effect schedule will +use the current {default_admin_delay} set before calling. + +The {pending_default_admin_delay}'s effect schedule is defined in a way that waiting until +the schedule and then calling {begin_default_admin_transfer} with the new delay will take at +least the same as another `default_admin` complete transfer (including acceptance). + +The schedule is designed for two scenarios: + +- When the delay is changed for a larger one the schedule is `block.timestamp + new delay` +capped by {default_admin_delay_increase_wait}. +- When the delay is changed for a shorter one, the schedule is `block.timestamp + (current +delay - new delay)`. + +A {pending_default_admin_delay} that never got into effect will be canceled in favor of a +new scheduled change. + +Requirements: + +- Only can be called by the current `default_admin`. + +Emits a {default_admin_delay_change_scheduled} event and may emit a +{default_admin_delay_change_canceled} event. + +[.contract-item] +[[IAccessControlDefaultAdminRules-rollback_default_admin_delay]] +==== `[.contract-item-name]#++rollback_default_admin_delay++#++()++` [.item-kind]#external# + +Cancels a scheduled {default_admin_delay} change. + +Requirements: + +- Only can be called by the current `default_admin`. + +May emit a {default_admin_delay_change_canceled} event. + +[.contract-item] +[[IAccessControlDefaultAdminRules-default_admin_delay_increase_wait]] +==== `[.contract-item-name]#++default_admin_delay_increase_wait++#++() → u64++` [.item-kind]#external# + +Maximum time in seconds for an increase to {default_admin_delay} (that is scheduled using +{change_default_admin_delay}) to take effect. Defaults to 5 days. + +When the {default_admin_delay} is scheduled to be increased, it goes into effect after the +new delay has passed with the purpose of giving enough time for reverting any accidental +change (i.e. using milliseconds instead of seconds) +that may lock the contract. However, to avoid excessive schedules, the wait is capped by +this function and it can be overridden for a custom {default_admin_delay} increase +scheduling. + +IMPORTANT: Make sure to add a reasonable amount of time while overriding this value, +otherwise, there's a risk of setting a high new delay that goes into effect almost +immediately without the possibility of human intervention in the case of an input error +(e.g. +set milliseconds instead of seconds). + +[#IAccessControlDefaultAdminRules-Events] +==== Events + +[.contract-item] +[[IAccessControlDefaultAdminRules-DefaultAdminTransferScheduled]] +==== `[.contract-item-name]#++DefaultAdminTransferScheduled++#++(new_admin: ContractAddress, accept_schedule: u64)++` [.item-kind]#event# + +Emitted when a `default_admin` transfer is started. + +Sets `new_admin` as the next address to become the `default_admin` by calling +{accept_default_admin_transfer} only after `accept_schedule` passes. + +[.contract-item] +[[IAccessControlDefaultAdminRules-DefaultAdminTransferCanceled]] +==== `[.contract-item-name]#++DefaultAdminTransferCanceled++#++()++` [.item-kind]#event# + +Emitted when a {pending_default_admin} is reset if it was never +accepted, regardless of its schedule. + +[.contract-item] +[[IAccessControlDefaultAdminRules-DefaultAdminDelayChangeScheduled]] +==== `[.contract-item-name]#++DefaultAdminDelayChangeScheduled++#++(new_delay: u64, effect_schedule: u64)++` [.item-kind]#event# + +Emitted when a {default_admin_delay} change is started. + +Sets `new_delay` as the next delay to be applied between default admins transfers +after `effect_schedule` has passed. + +[.contract-item] +[[IAccessControlDefaultAdminRules-DefaultAdminDelayChangeCanceled]] + +Emitted when a {pending_default_admin_delay} is reset if its schedule didn't pass. + +[.contract] +[[AccessControlDefaultAdminRulesComponent]] +=== `++AccessControlDefaultAdminRulesComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v2.0.0/packages/access/src/accesscontrol/extensions/accesscontrol_default_admin_rules.cairo[{github-icon},role=heading-link] + +```cairo +use openzeppelin_access::accesscontrol::extensions::AccessControlDefaultAdminRulesComponent; +``` + +Extension of {AccessControl} that allows specifying special rules to manage the `DEFAULT_ADMIN_ROLE` holder, +which is a sensitive role with special permissions over other roles that may potentially have +privileged rights in the system. + +If a specific role doesn’t have an admin role assigned, the holder of the `DEFAULT_ADMIN_ROLE` will +have the ability to grant it and revoke it. + +This contract implements the following risk mitigations on top of {AccessControl}: + +- Only one account holds the `DEFAULT_ADMIN_ROLE` since deployment until it’s potentially renounced. + +- Enforces a 2-step process to transfer the `DEFAULT_ADMIN_ROLE` to another account. + +- Enforces a configurable delay between the two steps, with the ability to cancel before the transfer is accepted. + +- The delay can be changed by scheduling, see {change_default_admin_delay}. + +- It is not possible to use another role to manage the `DEFAULT_ADMIN_ROLE`. + +[.contract-index#AccessControlDefaultAdminRules-Mixin-Impl] +.{mixin-impls} + +-- +.AccessControlMixinImpl + +* xref:#AccessControlDefaultAdminRulesComponent-Embeddable-Impls-AccessControlDefaultAdminRulesImpl[`++AccessControlDefaultAdminRulesImpl++`] +* xref:#AccessControlDefaultAdminRulesComponent-Embeddable-Impls-AccessControlImpl[`++AccessControlImpl++`] +* xref:#AccessControlDefaultAdminRulesComponent-Embeddable-Impls-AccessControlCamelImpl[`++AccessControlCamelImpl++`] +* xref:#AccessControlDefaultAdminRulesComponent-Embeddable-Impls-AccessControlWithDelayImpl[`++AccessControlWithDelayImpl++`] +* xref:api/introspection.adoc#SRC5Component-Embeddable-Impls[`++SRC5Impl++`] +-- + +[.contract-index#AccessControlDefaultAdminRulesComponent-Embeddable-Impls] +.Embeddable Implementations +-- +[.sub-index#AccessControlDefaultAdminRulesComponent-Embeddable-Impls-AccessControlDefaultAdminRulesImpl] +.AccessControlDefaultAdminRulesImpl + +* xref:#IAccessControlDefaultAdminRules-default_admin[`++default_admin(self)++`] +* xref:#IAccessControlDefaultAdminRules-pending_default_admin[`++pending_default_admin(self)++`] +* xref:#IAccessControlDefaultAdminRules-default_admin_delay[`++default_admin_delay(self)++`] +* xref:#IAccessControlDefaultAdminRules-pending_default_admin_delay[`++pending_default_admin_delay(self)++`] +* xref:#IAccessControlDefaultAdminRules-begin_default_admin_transfer[`++begin_default_admin_transfer(self, new_admin)++`] +* xref:#IAccessControlDefaultAdminRules-cancel_default_admin_transfer[`++cancel_default_admin_transfer(self)++`] +* xref:#IAccessControlDefaultAdminRules-accept_default_admin_transfer[`++accept_default_admin_transfer(self)++`] +* xref:#IAccessControlDefaultAdminRules-change_default_admin_delay[`++change_default_admin_delay(self, new_delay)++`] +* xref:#IAccessControlDefaultAdminRules-rollback_default_admin_delay[`++rollback_default_admin_delay(self)++`] +* xref:#IAccessControlDefaultAdminRules-default_admin_delay_increase_wait[`++default_admin_delay_increase_wait(self)++`] + +[.sub-index#AccessControlDefaultAdminRulesComponent-Embeddable-Impls-AccessControlImpl] +.AccessControlImpl + +* xref:#AccessControlDefaultAdminRulesComponent-has_role[`++has_role(self, role, account)++`] +* xref:#AccessControlDefaultAdminRulesComponent-get_role_admin[`++get_role_admin(self, role)++`] +* xref:#AccessControlDefaultAdminRulesComponent-grant_role[`++grant_role(self, role, account)++`] +* xref:#AccessControlDefaultAdminRulesComponent-revoke_role[`++revoke_role(self, role, account)++`] +* xref:#AccessControlDefaultAdminRulesComponent-renounce_role[`++renounce_role(self, role, account)++`] + +[.sub-index#AccessControlDefaultAdminRulesComponent-Embeddable-Impls-AccessControlCamelImpl] +.AccessControlCamelImpl + +* xref:#AccessControlDefaultAdminRulesComponent-hasRole[`++hasRole(self, role, account)++`] +* xref:#AccessControlDefaultAdminRulesComponent-getRoleAdmin[`++getRoleAdmin(self, role)++`] +* xref:#AccessControlDefaultAdminRulesComponent-grantRole[`++grantRole(self, role, account)++`] +* xref:#AccessControlDefaultAdminRulesComponent-revokeRole[`++revokeRole(self, role, account)++`] +* xref:#AccessControlDefaultAdminRulesComponent-renounceRole[`++renounceRole(self, role, account)++`] + +[.sub-index#AccessControlDefaultAdminRulesComponent-Embeddable-Impls-AccessControlWithDelayImpl] +.AccessControlWithDelayImpl + +* xref:#AccessControlDefaultAdminRulesComponent-get_role_status[`++get_role_status(self, role, account)++`] +* xref:#AccessControlDefaultAdminRulesComponent-grant_role_with_delay[`++grant_role_with_delay(self, role, account, delay)++`] + +.SRC5Impl +* xref:api/introspection.adoc#ISRC5-supports_interface[`supports_interface(self, interface_id: felt252)`] +-- + +[.contract-index] +.Internal Implementations +-- +.InternalImpl + +* xref:#AccessControlDefaultAdminRulesComponent-initializer[`++initializer(self, initial_delay, initial_default_admin)++`] +* xref:#AccessControlDefaultAdminRulesComponent-assert_only_role[`++assert_only_role(self, role)++`] +* xref:#AccessControlDefaultAdminRulesComponent-is_role_effective[`++is_role_effective(self, role, account)++`] +* xref:#AccessControlDefaultAdminRulesComponent-resolve_role_status[`++resolve_role_status(self, role, account)++`] +* xref:#AccessControlDefaultAdminRulesComponent-is_role_granted[`++is_role_granted(self, role, account)++`] +* xref:#AccessControlDefaultAdminRulesComponent-set_role_admin[`++set_role_admin(self, role, admin_role)++`] +* xref:#AccessControlDefaultAdminRulesComponent-_grant_role[`++_grant_role(self, role, account)++`] +* xref:#AccessControlDefaultAdminRulesComponent-_grant_role_with_delay[`++_grant_role_with_delay(self, role, account, delay)++`] +* xref:#AccessControlDefaultAdminRulesComponent-_revoke_role[`++_revoke_role(self, role, account)++`] +* xref:#AccessControlDefaultAdminRulesComponent-set_pending_default_admin[`++set_pending_default_admin(self, new_admin, new_schedule)++`] +* xref:#AccessControlDefaultAdminRulesComponent-set_pending_delay[`++set_pending_delay(self, new_delay, new_schedule)++`] +* xref:#AccessControlDefaultAdminRulesComponent-delay_change_wait[`++delay_change_wait(self, new_delay)++`] +-- + +[.contract-index] +.Events +-- +.IAccessControl +* xref:#AccessControlDefaultAdminRulesComponent-RoleAdminChanged[`++RoleAdminChanged(role, previous_admin_role, new_admin_role)++`] +* xref:#AccessControlDefaultAdminRulesComponent-RoleGranted[`++RoleGranted(role, account, sender)++`] +* xref:#AccessControlDefaultAdminRulesComponent-RoleRevoked[`++RoleRevoked(role, account, sender)++`] + +.IAccessControlWithDelay +* xref:#AccessControlDefaultAdminRulesComponent-RoleGrantedWithDelay[`++RoleGrantedWithDelay(role, account, sender, delay)++`] + +.IAccessControlDefaultAdminRules +* xref:#AccessControlDefaultAdminRulesComponent-DefaultAdminTransferScheduled[`++DefaultAdminTransferScheduled(new_admin, accept_schedule)++`] +* xref:#AccessControlDefaultAdminRulesComponent-DefaultAdminTransferCanceled[`++DefaultAdminTransferCanceled()++`] +* xref:#AccessControlDefaultAdminRulesComponent-DefaultAdminDelayChangeScheduled[`++DefaultAdminDelayChangeScheduled(new_delay, effect_schedule)++`] +* xref:#AccessControlDefaultAdminRulesComponent-DefaultAdminDelayChangeCanceled[`++DefaultAdminDelayChangeCanceled()++`] +-- + +:default_admin_transfer_scheduled: xref:AccessControlDefaultAdminRulesComponent-DefaultAdminTransferScheduled[DefaultAdminTransferScheduled] +:default_admin_transfer_canceled: xref:AccessControlDefaultAdminRulesComponent-DefaultAdminTransferCanceled[DefaultAdminTransferCanceled] +:default_admin_delay_change_scheduled: xref:AccessControlDefaultAdminRulesComponent-DefaultAdminDelayChangeScheduled[DefaultAdminDelayChangeScheduled] +:default_admin_delay_change_canceled: xref:AccessControlDefaultAdminRulesComponent-DefaultAdminDelayChangeCanceled[DefaultAdminDelayChangeCanceled] + +:RoleGranted: xref:AccessControlDefaultAdminRulesComponent-RoleGranted[RoleGranted] +:RoleGrantedWithDelay: xref:AccessControlDefaultAdminRulesComponent-RoleGrantedWithDelay[RoleGrantedWithDelay] +:RoleRevoked: xref:AccessControlDefaultAdminRulesComponent-RoleRevoked[RoleRevoked] +:RoleAdminChanged: xref:AccessControlDefaultAdminRulesComponent-RoleAdminChanged[RoleAdminChanged] + +:default_admin: xref:AccessControlDefaultAdminRulesComponent-default_admin[default_admin] +:pending_default_admin: xref:AccessControlDefaultAdminRulesComponent-pending_default_admin[pending_default_admin] +:default_admin_delay: xref:AccessControlDefaultAdminRulesComponent-default_admin_delay[default_admin_delay] +:pending_default_admin_delay: xref:AccessControlDefaultAdminRulesComponent-pending_default_admin_delay[pending_default_admin_delay] +:begin_default_admin_transfer: xref:AccessControlDefaultAdminRulesComponent-begin_default_admin_transfer[begin_default_admin_transfer] +:cancel_default_admin_transfer: xref:AccessControlDefaultAdminRulesComponent-cancel_default_admin_transfer[cancel_default_admin_transfer] +:accept_default_admin_transfer: xref:AccessControlDefaultAdminRulesComponent-accept_default_admin_transfer[accept_default_admin_transfer] +:change_default_admin_delay: xref:AccessControlDefaultAdminRulesComponent-change_default_admin_delay[change_default_admin_delay] +:rollback_default_admin_delay: xref:AccessControlDefaultAdminRulesComponent-rollback_default_admin_delay[rollback_default_admin_delay] +:default_admin_delay_increase_wait: xref:AccessControlDefaultAdminRulesComponent-default_admin_delay_increase_wait[default_admin_delay_increase_wait] + +[#AccessControlDefaultAdminRulesComponent-Embeddable-Functions] +==== Embeddable functions + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-default_admin]] +==== `[.contract-item-name]#++default_admin++#++(self: @ContractState) → ContractAddress++` [.item-kind]#external# + +Returns the address of the current `DEFAULT_ADMIN_ROLE` holder. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-pending_default_admin]] +==== `[.contract-item-name]#++pending_default_admin++#++(self: @ContractState) → (ContractAddress, u64)++` [.item-kind]#external# + +Returns a tuple of a `new_admin` and an `accept_schedule`. + +After the `accept_schedule` passes, the `new_admin` will be able to accept the +`default_admin` role by calling {accept_default_admin_transfer}, completing the role +transfer. + +A zero value only in `accept_schedule` indicates no pending admin transfer. + +NOTE: A zero address `new_admin` means that `default_admin` is being renounced. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-default_admin_delay]] +==== `[.contract-item-name]#++default_admin_delay++#++(self: @ContractState) → u64++` [.item-kind]#external# + +Returns the delay required to schedule the acceptance of a `default_admin` transfer +started. + +This delay will be added to the current timestamp when calling +{begin_default_admin_transfer} to set the acceptance schedule. + +NOTE: If a delay change has been scheduled, it will take effect as soon as the schedule +passes, making this function returns the new delay. + +See {change_default_admin_delay}. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-pending_default_admin_delay]] +==== `[.contract-item-name]#++pending_default_admin_delay++#++(self: @ContractState) → (u64, u64)++` [.item-kind]#external# + +Returns a tuple of `new_delay` and an `effect_schedule`. + +After the `effect_schedule` passes, the `new_delay` will get into effect immediately for +every new `default_admin` transfer started with {begin_default_admin_transfer}. + +A zero value only in `effect_schedule` indicates no pending delay change. + +NOTE: A zero value only for `new_delay` means that the next {default_admin_delay} +will be zero after the effect schedule. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-begin_default_admin_transfer]] +==== `[.contract-item-name]#++begin_default_admin_transfer++#++(ref self: ContractState, new_admin: ContractAddress)++` [.item-kind]#external# + +Starts a `default_admin` transfer by setting a {pending_default_admin} scheduled for +acceptance after the current timestamp plus a {default_admin_delay}. + +Requirements: + +- Only can be called by the current `default_admin`. + +Emits a {default_admin_transfer_scheduled} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-cancel_default_admin_transfer]] +==== `[.contract-item-name]#++cancel_default_admin_transfer++#++(ref self: ContractState)++` [.item-kind]#external# + +Cancels a `default_admin` transfer previously started with {begin_default_admin_transfer}. + +A {pending_default_admin} not yet accepted can also be cancelled with this function. + +Requirements: + +- Only can be called by the current `default_admin`. + +May emit a {default_admin_transfer_canceled} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-accept_default_admin_transfer]] +==== `[.contract-item-name]#++accept_default_admin_transfer++#++(ref self: ContractState)++` [.item-kind]#external# + +Completes a `default_admin` transfer previously started with {begin_default_admin_transfer}. + +After calling the function: + +- `DEFAULT_ADMIN_ROLE` must be granted to the caller. +- `DEFAULT_ADMIN_ROLE` must be revoked from the previous holder. +- {pending_default_admin} must be reset to zero values. + +Requirements: + +- Only can be called by the {pending_default_admin}'s `new_admin`. +- The {pending_default_admin}'s `accept_schedule` should've passed. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-change_default_admin_delay]] +==== `[.contract-item-name]#++change_default_admin_delay++#++(ref self: ContractState, new_delay: u64)++` [.item-kind]#external# + +Initiates a {default_admin_delay} update by setting a {pending_default_admin_delay} +scheduled for getting into effect after the current timestamp plus a +{default_admin_delay}. + +This function guarantees that any call to {begin_default_admin_transfer} done between +the timestamp this method is called and the {pending_default_admin_delay} effect +schedule will use the current {default_admin_delay} +set before calling. + +The {pending_default_admin_delay}'s effect schedule is defined in a way that waiting +until the schedule and then calling {begin_default_admin_transfer} with the new delay +will take at least the same as another `default_admin` +complete transfer (including acceptance). + +The schedule is designed for two scenarios: + +- When the delay is changed for a larger one the schedule is `block.timestamp + +new delay` capped by {default_admin_delay_increase_wait}. +- When the delay is changed for a shorter one, the schedule is `block.timestamp + +(current delay - new delay)`. + +A {pending_default_admin_delay} that never got into effect will be canceled in favor of +a new scheduled change. + +Requirements: + +- Only can be called by the current `default_admin`. + +Emits a {default_admin_delay_change_scheduled} event and may emit a +{default_admin_delay_change_canceled} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-rollback_default_admin_delay]] +==== `[.contract-item-name]#++rollback_default_admin_delay++#++(ref self: ContractState)++` [.item-kind]#external# + +Cancels a scheduled {default_admin_delay} change. + +Requirements: + +- Only can be called by the current `default_admin`. + +May emit a {default_admin_delay_change_canceled} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-default_admin_delay_increase_wait]] +==== `[.contract-item-name]#++default_admin_delay_increase_wait++#++(self: @ContractState) → u64++` [.item-kind]#external# + +Maximum time in seconds for an increase to {default_admin_delay} (that is scheduled +using {change_default_admin_delay}) to take effect. Defaults to 5 days. + +When the {default_admin_delay} is scheduled to be increased, it goes into effect after +the new delay has passed with the purpose of giving enough time for reverting any +accidental change (i.e. using milliseconds instead of seconds) +that may lock the contract. However, to avoid excessive schedules, the wait is capped by +this function and it can be overridden for a custom {default_admin_delay} increase +scheduling. + +IMPORTANT: Make sure to add a reasonable amount of time while overriding this value, +otherwise, there's a risk of setting a high new delay that goes into effect almost +immediately without the possibility of human intervention in the case of an input error +(eg. set milliseconds instead of seconds). + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-has_role]] +==== `[.contract-item-name]#++has_role++#++(self: @ContractState, role: felt252, account: ContractAddress) → bool++` [.item-kind]#external# + +Returns whether `account` can act as `role`. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-get_role_admin]] +==== `[.contract-item-name]#++get_role_admin++#++(self: @ContractState, role: felt252) → felt252++` [.item-kind]#external# + +Returns the admin role that controls `role`. See {grant_role} and +{revoke_role}. + +To change a role's admin, use {set_role_admin}. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-get_role_status]] +==== `[.contract-item-name]#++get_role_status++#++(self: @ContractState, role: felt252, account: ContractAddress) → RoleStatus++` [.item-kind]#external# + +Returns the account's status for the given role. + +The possible statuses are: + +- `NotGranted`: the role has not been granted to the account. +- `Delayed`: The role has been granted to the account but is not yet active due to a +time delay. +- `Effective`: the role has been granted to the account and is currently active. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-grant_role]] +==== `[.contract-item-name]#++grant_role++#++(ref self: ContractState, role: felt252, account: ContractAddress)++` [.item-kind]#external# + +Grants `role` to `account`. + +If `account` had not been already granted `role`, emits a {RoleGranted} +event. + +Requirements: + +- the caller must have ``role``'s admin role. + +May emit a {RoleGranted} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-grant_role_with_delay]] +==== `[.contract-item-name]#++grant_role_with_delay++#++(ref self: ContractState, role: felt252, account: ContractAddress, delay: u64)++` [.item-kind]#external# + +Attempts to grant `role` to `account` with the specified activation delay. + +Requirements: + +- The caller must have `role`'s admin role. +- delay must be greater than 0. +- the `role` must not be already effective for `account`. + +May emit a {RoleGrantedWithDelay} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-revoke_role]] +==== `[.contract-item-name]#++revoke_role++#++(ref self: ContractState, role: felt252, account: ContractAddress)++` [.item-kind]#external# + +Revokes `role` from `account`. + +If `account` had been granted `role`, emits a {RoleRevoked} event. + +Requirements: + +- the caller must have ``role``'s admin role. + +May emit a {RoleRevoked} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-renounce_role]] +==== `[.contract-item-name]#++renounce_role++#++(ref self: ContractState, role: felt252, account: ContractAddress)++` [.item-kind]#external# + +Revokes `role` from the calling account. + +Roles are often managed via {grant_role} and {revoke_role}. This function's +purpose is to provide a mechanism for accounts to lose their privileges +if they are compromised (such as when a trusted device is misplaced). + +If the calling account had been revoked `role`, emits a {RoleRevoked} +event. + +Requirements: + +- the caller must be `account`. + +May emit a {RoleRevoked} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-supports_interface]] +==== `[.contract-item-name]#++supports_interface++#++(self: @ContractState, interface_id: felt252) → bool++` [.item-kind]#external# + +See xref:api/introspection.adoc#ISRC5-supports_interface[ISRC5::supports_interface]. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-hasRole]] +==== `[.contract-item-name]#++hasRole++#++(self: @ContractState, role: felt252, account: ContractAddress) → bool++` [.item-kind]#external# + +See xref:AccessControlDefaultAdminRulesComponent-has_role[has_role]. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-getRoleAdmin]] +==== `[.contract-item-name]#++getRoleAdmin++#++(self: @ContractState, role: felt252) → felt252++` [.item-kind]#external# + +See xref:AccessControlDefaultAdminRulesComponent-get_role_admin[get_role_admin]. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-grantRole]] +==== `[.contract-item-name]#++grantRole++#++(ref self: ContractState, role: felt252, account: ContractAddress)++` [.item-kind]#external# + +See xref:AccessControlDefaultAdminRulesComponent-grant_role[grant_role]. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-revokeRole]] +==== `[.contract-item-name]#++revokeRole++#++(ref self: ContractState, role: felt252, account: ContractAddress)++` [.item-kind]#external# + +See xref:AccessControlDefaultAdminRulesComponent-revoke_role[revoke_role]. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-renounceRole]] +==== `[.contract-item-name]#++renounceRole++#++(ref self: ContractState, role: felt252, account: ContractAddress)++` [.item-kind]#external# + +See xref:AccessControlDefaultAdminRulesComponent-renounce_role[renounce_role]. + +[#AccessControlDefaultAdminRulesComponent-Internal-Functions] +==== Internal functions + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-initializer]] +==== `[.contract-item-name]#++initializer++#++(ref self: ContractState, initial_delay: u64, initial_default_admin: ContractAddress)++` [.item-kind]#external# + +Initializes the contract by registering the IAccessControl interface ID and +setting the initial delay and default admin. + +Requirements: + +- `initial_default_admin` must not be the zero address. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-assert_only_role]] +==== `[.contract-item-name]#++assert_only_role++#++(self: @ContractState, role: felt252)++` [.item-kind]#external# + +Validates that the caller can act as the given role. Otherwise it panics. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-is_role_effective]] +==== `[.contract-item-name]#++is_role_effective++#++(self: @ContractState, role: felt252, account: ContractAddress) → bool++` [.item-kind]#external# + +Returns whether the account can act as the given role. + +The account can act as the role if it is active and the `effective_from` time is before +or equal to the current time. + +NOTE: If the `effective_from` timepoint is 0, the role is effective immediately. +This is backwards compatible with implementations that didn't use delays but +a single boolean flag. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-resolve_role_status]] +==== `[.contract-item-name]#++resolve_role_status++#++(self: @ContractState, role: felt252, account: ContractAddress) → RoleStatus++` [.item-kind]#external# + +Returns the account's status for the given role. + +The possible statuses are: + +- `NotGranted`: the role has not been granted to the account. +- `Delayed`: The role has been granted to the account but is not yet active due to a +time delay. +- `Effective`: the role has been granted to the account and is currently active. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-is_role_granted]] +==== `[.contract-item-name]#++is_role_granted++#++(self: @ContractState, role: felt252, account: ContractAddress) → bool++` [.item-kind]#external# + +Returns whether the account has the given role granted. + +NOTE: The account may not be able to act as the role yet, if a delay was set and has not +passed yet. Use {is_role_effective} to check if the account can act as the role. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-set_role_admin]] +==== `[.contract-item-name]#++set_role_admin++#++(ref self: ContractState, role: felt252, admin_role: felt252)++` [.item-kind]#external# + +Sets `admin_role` as `role`'s admin role. + +Internal function without access restriction. + +Requirements: + +- `role` must not be `DEFAULT_ADMIN_ROLE`. + +Emits a {RoleAdminChanged} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-_grant_role]] +==== `[.contract-item-name]#++_grant_role++#++(ref self: ContractState, role: felt252, account: ContractAddress)++` [.item-kind]#external# + +Attempts to grant `role` to `account`. The function does nothing if `role` is already +effective for `account`. If `role` has been granted to `account`, but is not yet active +due to a time delay, the delay is removed and `role` becomes effective immediately. + +Internal function without access restriction. + +For `DEFAULT_ADMIN_ROLE`, it only allows granting if there isn't already a +`default_admin` +or if the role has been previously renounced. + +NOTE: Exposing this function through another mechanism may make the `DEFAULT_ADMIN_ROLE` +assignable again. Make sure to guarantee this is the expected behavior in your +implementation. + +May emit a {RoleGranted} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-_grant_role_with_delay]] +==== `[.contract-item-name]#++_grant_role_with_delay++#++(ref self: ContractState, role: felt252, account: ContractAddress, delay: u64)++` [.item-kind]#external# + +Attempts to grant `role` to `account` with the specified activation delay. + +The role will become effective after the given delay has passed. If the role is already +active (`Effective`) for the account, the function will panic. If the role has been +granted but is not yet active (being in the `Delayed` state), the existing delay will be +overwritten with the new `delay`. + +Internal function without access restriction. + +Requirements: + +- `delay` must be greater than 0. +- the `role` must not be already effective for `account`. +- `role` must not be `DEFAULT_ADMIN_ROLE`. + +May emit a {RoleGrantedWithDelay} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-_revoke_role]] +==== `[.contract-item-name]#++_revoke_role++#++(ref self: ContractState, role: felt252, account: ContractAddress)++` [.item-kind]#external# + +Attempts to revoke `role` from `account`. The function does nothing if `role` is not +effective for `account`. If `role` has been revoked from `account`, but is still active +due to a time delay, the delay is removed and `role` becomes inactive immediately. + +Internal function without access restriction. + +May emit a {RoleRevoked} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-set_pending_default_admin]] +==== `[.contract-item-name]#++set_pending_default_admin++#++(ref self: ContractState, new_admin: ContractAddress, new_schedule: u64)++` [.item-kind]#external# + +Setter of the tuple for pending admin and its schedule. + +May emit a {DefaultAdminTransferCanceled} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-set_pending_delay]] +==== `[.contract-item-name]#++set_pending_delay++#++(ref self: ContractState, new_delay: u64, new_schedule: u64)++` [.item-kind]#external# + +Setter of the tuple for pending delay and its schedule. + +May emit a {DefaultAdminDelayChangeCanceled} event. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-delay_change_wait]] +==== `[.contract-item-name]#++delay_change_wait++#++(self: @ContractState, new_delay: u64) → u64++` [.item-kind]#external# + +Returns the amount of seconds to wait after the `new_delay` will +become the new `default_admin_delay`. + +The value returned guarantees that if the delay is reduced, it will go into effect +after a wait that honors the previously set delay. + +See {default_admin_delay_increase_wait}. + +[#AccessControlDefaultAdminRulesComponent-Events] +==== Events + +[.contract-item] +[#AccessControlDefaultAdminRulesComponent-RoleAdminChanged] +==== `[.contract-item-name]#++RoleAdminChanged++#++(role: felt252, previous_admin_role: felt252, new_admin_role: felt252)++` [.item-kind]#event# + +Emitted when `new_admin_role` is set as `role`'s admin role, replacing `previous_admin_role` + +`DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite +`RoleAdminChanged` not being emitted signaling this. + +[.contract-item] +[#AccessControlDefaultAdminRulesComponent-RoleGranted] +==== `[.contract-item-name]#++RoleGranted++#++(role: felt252, account: ContractAddress, sender: ContractAddress)++` [.item-kind]#event# + +Emitted when `account` is granted `role`. + +`sender` is the account that originated the contract call, an account with the admin role +or the deployer address if `_grant_role` is called from the constructor. + +[.contract-item] +[#AccessControlDefaultAdminRulesComponent-RoleRevoked] +==== `[.contract-item-name]#++RoleRevoked++#++(role: felt252, account: ContractAddress, sender: ContractAddress)++` [.item-kind]#event# + +Emitted when `role` is revoked for `account`. + +`sender` is the account that originated the contract call: + +- If using `revoke_role`, it is the admin role bearer. +- If using `renounce_role`, it is the role bearer (i.e. `account`). + +[.contract-item] +[#AccessControlDefaultAdminRulesComponent-RoleGrantedWithDelay] +==== `[.contract-item-name]#++RoleGrantedWithDelay++#++(role: felt252, account: ContractAddress, sender: ContractAddress, delay: u64)++` [.item-kind]#event# + +Emitted when `account` is granted `role` with a delay. + +`sender` is the account that originated the contract call, an account with the admin role +or the deployer address if `_grant_role_with_delay` is called from the constructor. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-DefaultAdminTransferScheduled]] +==== `[.contract-item-name]#++DefaultAdminTransferScheduled++#++(new_admin: ContractAddress, accept_schedule: u64)++` [.item-kind]#event# + +Emitted when a `default_admin` transfer is started. + +Sets `new_admin` as the next address to become the `default_admin` by calling +{accept_default_admin_transfer} only after `accept_schedule` passes. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-DefaultAdminTransferCanceled]] +==== `[.contract-item-name]#++DefaultAdminTransferCanceled++#++()++` [.item-kind]#event# + +Emitted when a {pending_default_admin} is reset if it was never +accepted, regardless of its schedule. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-DefaultAdminDelayChangeScheduled]] +==== `[.contract-item-name]#++DefaultAdminDelayChangeScheduled++#++(new_delay: u64, effect_schedule: u64)++` [.item-kind]#event# + +Emitted when a {default_admin_delay} change is started. + +Sets `new_delay` as the next delay to be applied between default admins transfers +after `effect_schedule` has passed. + +[.contract-item] +[[AccessControlDefaultAdminRulesComponent-DefaultAdminDelayChangeCanceled]] + +Emitted when a {pending_default_admin_delay} is reset if its schedule didn't pass. diff --git a/packages/access/src/accesscontrol.cairo b/packages/access/src/accesscontrol.cairo index 595adfd45..8dd966427 100644 --- a/packages/access/src/accesscontrol.cairo +++ b/packages/access/src/accesscontrol.cairo @@ -1,5 +1,6 @@ pub mod accesscontrol; pub mod account_role_info; +pub mod extensions; pub mod interface; pub use accesscontrol::AccessControlComponent; diff --git a/packages/access/src/accesscontrol/accesscontrol.cairo b/packages/access/src/accesscontrol/accesscontrol.cairo index eae6c5301..b15d28625 100644 --- a/packages/access/src/accesscontrol/accesscontrol.cairo +++ b/packages/access/src/accesscontrol/accesscontrol.cairo @@ -281,7 +281,7 @@ pub mod AccessControlComponent { ) -> bool { match self.resolve_role_status(role, account) { RoleStatus::Effective => true, - RoleStatus::Delayed => false, + RoleStatus::Delayed(_) => false, RoleStatus::NotGranted => false, } } @@ -352,7 +352,7 @@ pub mod AccessControlComponent { ) { match self.resolve_role_status(role, account) { RoleStatus::Effective => (), - RoleStatus::Delayed | + RoleStatus::Delayed(_) | RoleStatus::NotGranted => { let caller = starknet::get_caller_address(); let role_info = AccountRoleInfo { is_granted: true, effective_from: 0 }; @@ -386,7 +386,7 @@ pub mod AccessControlComponent { assert(delay > 0, Errors::INVALID_DELAY); match self.resolve_role_status(role, account) { RoleStatus::Effective => panic_with_const_felt252::(), - RoleStatus::Delayed | + RoleStatus::Delayed(_) | RoleStatus::NotGranted => { let caller = starknet::get_caller_address(); let effective_from = starknet::get_block_timestamp() + delay; @@ -408,7 +408,7 @@ pub mod AccessControlComponent { match self.resolve_role_status(role, account) { RoleStatus::NotGranted => (), RoleStatus::Effective | - RoleStatus::Delayed => { + RoleStatus::Delayed(_) => { let caller = starknet::get_caller_address(); let role_info = AccountRoleInfo { is_granted: false, effective_from: 0 }; self.AccessControl_role_member.write((role, account), role_info); diff --git a/packages/access/src/accesscontrol/extensions.cairo b/packages/access/src/accesscontrol/extensions.cairo new file mode 100644 index 000000000..5007c790d --- /dev/null +++ b/packages/access/src/accesscontrol/extensions.cairo @@ -0,0 +1,6 @@ +pub mod accesscontrol_default_admin_rules; +pub mod interface; +pub mod pending_delay; + +pub use accesscontrol_default_admin_rules::AccessControlDefaultAdminRulesComponent::DEFAULT_ADMIN_ROLE; +pub use accesscontrol_default_admin_rules::{AccessControlDefaultAdminRulesComponent, DefaultConfig}; diff --git a/packages/access/src/accesscontrol/extensions/accesscontrol_default_admin_rules.cairo b/packages/access/src/accesscontrol/extensions/accesscontrol_default_admin_rules.cairo new file mode 100644 index 000000000..652c26fb1 --- /dev/null +++ b/packages/access/src/accesscontrol/extensions/accesscontrol_default_admin_rules.cairo @@ -0,0 +1,970 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v2.0.0 +// (access/src/accesscontrol/extensions/accesscontrol_default_admin_rules.cairo) + +/// # AccessControlDefaultAdminRules Component +/// +/// Extension of AccessControl that allows specifying special rules to manage +/// the `DEFAULT_ADMIN_ROLE` holder, which is a sensitive role with special permissions +/// over other roles that may potentially have privileged rights in the system. +/// +/// If a specific role doesn't have an admin role assigned, the holder of the +/// `DEFAULT_ADMIN_ROLE` will have the ability to grant it and revoke it. +/// +/// This contract implements the following risk mitigations on top of {AccessControl}: +/// +/// - Only one account holds the `DEFAULT_ADMIN_ROLE` since deployment until it's potentially +/// renounced. +/// - Enforces a 2-step process to transfer the `DEFAULT_ADMIN_ROLE` to another account. +/// - Enforces a configurable delay between the two steps, with the ability to cancel before the +/// transfer is accepted. +/// - The delay can be changed by scheduling, see `change_default_admin_delay`. +/// - It is not possible to use another role to manage the `DEFAULT_ADMIN_ROLE`. +#[starknet::component] +pub mod AccessControlDefaultAdminRulesComponent { + use core::num::traits::Zero; + use core::panic_with_const_felt252; + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_introspection::src5::SRC5Component::{ + InternalImpl as SRC5InternalImpl, SRC5Impl, + }; + use starknet::ContractAddress; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; + use crate::accesscontrol::AccessControlComponent::{ + RoleAdminChanged, RoleGranted, RoleGrantedWithDelay, RoleRevoked, + }; + use crate::accesscontrol::account_role_info::AccountRoleInfo; + use crate::accesscontrol::extensions::interface as default_admin_rules_interface; + use crate::accesscontrol::extensions::pending_delay::PendingDelay; + use crate::accesscontrol::interface; + use crate::accesscontrol::interface::RoleStatus; + + pub const DEFAULT_ADMIN_ROLE: felt252 = 0; + + #[storage] + pub struct Storage { + pub AccessControl_role_admin: Map, + pub AccessControl_role_member: Map<(felt252, ContractAddress), AccountRoleInfo>, + pub AccessControl_pending_default_admin: ContractAddress, + pub AccessControl_pending_default_admin_schedule: u64, // 0 if not scheduled + pub AccessControl_current_delay: u64, + pub AccessControl_current_default_admin: ContractAddress, + pub AccessControl_pending_delay: PendingDelay, + } + + #[event] + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub enum Event { + RoleGranted: RoleGranted, + RoleGrantedWithDelay: RoleGrantedWithDelay, + RoleRevoked: RoleRevoked, + RoleAdminChanged: RoleAdminChanged, + DefaultAdminTransferScheduled: DefaultAdminTransferScheduled, + DefaultAdminTransferCanceled: DefaultAdminTransferCanceled, + DefaultAdminDelayChangeScheduled: DefaultAdminDelayChangeScheduled, + DefaultAdminDelayChangeCanceled: DefaultAdminDelayChangeCanceled, + } + + /// Emitted when a `default_admin` transfer is started. + /// + /// Sets `new_admin` as the next address to become the `default_admin` by calling + /// `accept_default_admin_transfer` only after accept_schedule` passes. + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub struct DefaultAdminTransferScheduled { + #[key] + pub new_admin: ContractAddress, + pub accept_schedule: u64, + } + + /// Emitted when a `pending_default_admin` is reset if it was never + /// accepted, regardless of its schedule. + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub struct DefaultAdminTransferCanceled {} + + /// Emitted when a `default_admin_delay` change is started. + /// + /// Sets `new_delay` as the next delay to be applied between default admins transfers + /// after `effect_schedule` has passed. + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub struct DefaultAdminDelayChangeScheduled { + pub new_delay: u64, + pub effect_schedule: u64, + } + + /// Emitted when a `pending_default_admin_delay` is reset if its schedule didn't pass. + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub struct DefaultAdminDelayChangeCanceled {} + + pub mod Errors { + /// AccessControl errors + pub const INVALID_CALLER: felt252 = 'Can only renounce role for self'; + pub const MISSING_ROLE: felt252 = 'Caller is missing role'; + pub const INVALID_DELAY: felt252 = 'Delay must be greater than 0'; + pub const ALREADY_EFFECTIVE: felt252 = 'Role is already effective'; + + /// DefaultAdminRules extension errors + pub const INVALID_DEFAULT_ADMIN: felt252 = 'Invalid default admin'; + pub const ONLY_NEW_DEFAULT_ADMIN: felt252 = 'Only new default admin allowed'; + pub const ENFORCED_DEFAULT_ADMIN_RULES: felt252 = 'Default admin rules enforced'; + pub const ENFORCED_DEFAULT_ADMIN_DELAY: felt252 = 'Default admin delay enforced'; + } + + /// Constants expected to be defined at the contract level used to configure the component + /// behaviour. + /// + /// - `DEFAULT_ADMIN_DELAY_INCREASE_WAIT`: Returns the maximum number of seconds to wait for a + /// delay increase. + pub trait ImmutableConfig { + const DEFAULT_ADMIN_DELAY_INCREASE_WAIT: u64; + } + + #[embeddable_as(AccessControlDefaultAdminRulesImpl)] + impl AccessControlDefaultAdminRules< + TContractState, + +HasComponent, + impl Immutable: ImmutableConfig, + +SRC5Component::HasComponent, + +Drop, + > of default_admin_rules_interface::IAccessControlDefaultAdminRules< + ComponentState, + > { + /// Returns the address of the current `DEFAULT_ADMIN_ROLE` holder. + fn default_admin(self: @ComponentState) -> ContractAddress { + self.AccessControl_current_default_admin.read() + } + + /// Returns a tuple of a `new_admin` and an `accept_schedule`. + /// + /// After the `accept_schedule` passes, the `new_admin` will be able to accept the + /// `default_admin` role by calling `accept_default_admin_transfer`, completing the role + /// transfer. + /// + /// A zero value only in `accept_schedule` indicates no pending admin transfer. + /// + /// NOTE: A zero address `new_admin` means that `default_admin` is being renounced. + fn pending_default_admin(self: @ComponentState) -> (ContractAddress, u64) { + let pending_default_admin = self.AccessControl_pending_default_admin.read(); + let pending_default_admin_schedule = self + .AccessControl_pending_default_admin_schedule + .read(); + (pending_default_admin, pending_default_admin_schedule) + } + + /// Returns the delay required to schedule the acceptance of a `default_admin` transfer + /// started. + /// + /// This delay will be added to the current timestamp when calling + /// `begin_default_admin_transfer` to set the acceptance schedule. + /// + /// NOTE: If a delay change has been scheduled, it will take effect as soon as the schedule + /// passes, making this function return the new delay. + /// + /// See `change_default_admin_delay`. + fn default_admin_delay(self: @ComponentState) -> u64 { + let pending_delay = self.AccessControl_pending_delay.read(); + let schedule = pending_delay.schedule; + + if is_schedule_set(schedule) && has_schedule_passed(schedule) { + pending_delay.delay + } else { + self.AccessControl_current_delay.read() + } + } + + /// Returns a tuple of `new_delay` and an `effect_schedule`. + /// + /// After the `effect_schedule` passes, the `new_delay` will get into effect immediately for + /// every new `default_admin` transfer started with `begin_default_admin_transfer`. + /// + /// A zero value only in `effect_schedule` indicates no pending delay change. + /// + /// NOTE: A zero value only for `new_delay` means that the next `default_admin_delay` + /// will be zero after the effect schedule. + fn pending_default_admin_delay(self: @ComponentState) -> (u64, u64) { + let pending_delay = self.AccessControl_pending_delay.read(); + let schedule = pending_delay.schedule; + + if is_schedule_set(schedule) && !has_schedule_passed(schedule) { + let delay = pending_delay.delay; + (delay, schedule) + } else { + (0, 0) + } + } + + /// Starts a `default_admin` transfer by setting a `pending_default_admin` scheduled for + /// acceptance after the current timestamp plus a `default_admin_delay`. + /// + /// Requirements: + /// + /// - Only can be called by the current `default_admin`. + /// + /// Emits a `DefaultAdminRoleChangeStarted` event. + fn begin_default_admin_transfer( + ref self: ComponentState, new_admin: ContractAddress, + ) { + self.assert_only_role(DEFAULT_ADMIN_ROLE); + + let new_schedule = starknet::get_block_timestamp() + Self::default_admin_delay(@self); + self.set_pending_default_admin(new_admin, new_schedule); + self.emit(DefaultAdminTransferScheduled { new_admin, accept_schedule: new_schedule }); + } + + /// Cancels a `default_admin` transfer previously started with + /// `begin_default_admin_transfer`. + /// + /// A `pending_default_admin` not yet accepted can also be canceled with this function. + /// + /// Requirements: + /// + /// - Only can be called by the current `default_admin`. + /// + /// May emit a `DefaultAdminTransferCanceled` event. + fn cancel_default_admin_transfer(ref self: ComponentState) { + self.assert_only_role(DEFAULT_ADMIN_ROLE); + self.set_pending_default_admin(Zero::zero(), 0); + } + + /// Completes a `default_admin` transfer previously started with + /// `begin_default_admin_transfer`. + /// + /// After calling the function: + /// + /// - `DEFAULT_ADMIN_ROLE` must be granted to the caller. + /// - `DEFAULT_ADMIN_ROLE` must be revoked from the previous holder. + /// - `pending_default_admin` must be reset to zero values. + /// + /// Requirements: + /// + /// - Only can be called by the `pending_default_admin`'s `new_admin`. + /// - The `pending_default_admin`'s `accept_schedule` should've passed. + fn accept_default_admin_transfer(ref self: ComponentState) { + let (new_default_admin, schedule) = Self::pending_default_admin(@self); + // Enforce that the caller is the `new_default_admin` + assert( + new_default_admin == starknet::get_caller_address(), Errors::ONLY_NEW_DEFAULT_ADMIN, + ); + + if !is_schedule_set(schedule) || !has_schedule_passed(schedule) { + panic_with_const_felt252::(); + } + + self._revoke_role(DEFAULT_ADMIN_ROLE, Self::default_admin(@self)); + self._grant_role(DEFAULT_ADMIN_ROLE, new_default_admin); + + self.AccessControl_pending_default_admin.write(Zero::zero()); + self.AccessControl_pending_default_admin_schedule.write(0); + } + + /// Initiates a `default_admin_delay` update by setting a `pending_default_admin_delay` + /// scheduled for getting into effect after the current timestamp plus a + /// `default_admin_delay`. + /// + /// This function guarantees that any call to `begin_default_admin_transfer` done between + /// the timestamp this method is called at and the `pending_default_admin_delay` effect + /// schedule will use the current `default_admin_delay` + /// set before calling. + /// + /// The `pending_default_admin_delay`'s effect schedule is defined in a way that waiting + /// until the schedule and then calling `begin_default_admin_transfer` with the new delay + /// will take at least the same as another `default_admin` + /// complete transfer (including acceptance). + /// + /// The schedule is designed for two scenarios: + /// + /// - When the delay is changed for a larger one the schedule is `block.timestamp + + /// new delay` capped by `default_admin_delay_increase_wait`. + /// - When the delay is changed for a shorter one, the schedule is `block.timestamp + + /// (current delay - new delay)`. + /// + /// A `pending_default_admin_delay` that never got into effect will be canceled in favor of + /// a new scheduled change. + /// + /// Requirements: + /// + /// - Only can be called by the current `default_admin`. + /// + /// Emits a `DefaultAdminDelayChangeScheduled` event and may emit a + /// `DefaultAdminDelayChangeCanceled` event. + fn change_default_admin_delay(ref self: ComponentState, new_delay: u64) { + self.assert_only_role(DEFAULT_ADMIN_ROLE); + + let new_schedule = starknet::get_block_timestamp() + self.delay_change_wait(new_delay); + self.set_pending_delay(new_delay, new_schedule); + self + .emit( + DefaultAdminDelayChangeScheduled { new_delay, effect_schedule: new_schedule }, + ); + } + + /// Cancels a scheduled `default_admin_delay` change. + /// + /// Requirements: + /// + /// - Only can be called by the current `default_admin`. + /// + /// May emit a `DefaultAdminDelayChangeCanceled` event. + fn rollback_default_admin_delay(ref self: ComponentState) { + self.assert_only_role(DEFAULT_ADMIN_ROLE); + self.set_pending_delay(0, 0); + } + + /// Maximum time in seconds for an increase to `default_admin_delay` (that is scheduled + /// using `change_default_admin_delay`) + /// to take effect. Defaults to 5 days. + /// + /// When the `default_admin_delay` is scheduled to be increased, it goes into effect after + /// the new delay has passed with the purpose of giving enough time for reverting any + /// accidental change (i.e. using milliseconds instead of seconds) + /// that may lock the contract. However, to avoid excessive schedules, the wait is capped by + /// this function and it can be overridden for a custom `default_admin_delay` increase + /// scheduling. + /// + /// IMPORTANT: Make sure to add a reasonable amount of time while overriding this value, + /// otherwise, there's a risk of setting a high new delay that goes into effect almost + /// immediately without the possibility of human intervention in the case of an input error + /// (eg. set milliseconds instead of seconds). + fn default_admin_delay_increase_wait(self: @ComponentState) -> u64 { + Immutable::DEFAULT_ADMIN_DELAY_INCREASE_WAIT + } + } + + #[embeddable_as(AccessControlImpl)] + impl AccessControl< + TContractState, + +HasComponent, + +ImmutableConfig, + +SRC5Component::HasComponent, + +Drop, + > of interface::IAccessControl> { + /// Returns whether `account` can act as `role`. + fn has_role( + self: @ComponentState, role: felt252, account: ContractAddress, + ) -> bool { + self.is_role_effective(role, account) + } + + /// Returns the admin role that controls `role`. + fn get_role_admin(self: @ComponentState, role: felt252) -> felt252 { + self.AccessControl_role_admin.read(role) + } + + /// Grants `role` to `account`. + /// + /// If `account` has not been already granted `role`, emits a `RoleGranted` event. + /// + /// Requirements: + /// + /// - The caller must have `role`'s admin role. + /// - `role` must not be the `DEFAULT_ADMIN_ROLE`. + fn grant_role( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + assert(role != DEFAULT_ADMIN_ROLE, Errors::ENFORCED_DEFAULT_ADMIN_RULES); + + let admin = Self::get_role_admin(@self, role); + self.assert_only_role(admin); + self._grant_role(role, account); + } + + /// Revokes `role` from `account`. + /// + /// If `account` has been granted `role`, emits a `RoleRevoked` event. + /// + /// Requirements: + /// + /// - The caller must have `role`'s admin role. + /// - `role` must not be the `DEFAULT_ADMIN_ROLE`. + fn revoke_role( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + assert(role != DEFAULT_ADMIN_ROLE, Errors::ENFORCED_DEFAULT_ADMIN_RULES); + + let admin = Self::get_role_admin(@self, role); + self.assert_only_role(admin); + self._revoke_role(role, account); + } + + /// Revokes `role` from the calling account. + /// + /// Roles are often managed via `grant_role` and `revoke_role`: this function's + /// purpose is to provide a mechanism for accounts to lose their privileges + /// if they are compromised (such as when a trusted device is misplaced). + /// + /// If the calling account had been revoked `role`, emits a `RoleRevoked` + /// event. + /// + /// For the `DEFAULT_ADMIN_ROLE`, it only allows renouncing in two steps by first calling + /// `begin_default_admin_transfer` to the zero address, so it's required that the + /// `pending_default_admin_schedule` has also passed when calling this function. + /// + /// After its execution, it will not be possible to call + /// `assert_only_role(DEFAULT_ADMIN_ROLE)`-protected functions. + /// + /// NOTE: Renouncing `DEFAULT_ADMIN_ROLE` will leave the contract without a `default_admin`, + /// thereby disabling any functionality that is only available for it, and the possibility + /// of reassigning a non-administrated role. + /// + /// Requirements: + /// + /// - The caller must be `account`. + fn renounce_role( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + if role == DEFAULT_ADMIN_ROLE + && account == AccessControlDefaultAdminRules::default_admin(@self) { + let (new_default_admin, schedule) = + AccessControlDefaultAdminRules::pending_default_admin( + @self, + ); + if new_default_admin.is_non_zero() + || !is_schedule_set(schedule) + || !has_schedule_passed(schedule) { + panic_with_const_felt252::(); + } + self.AccessControl_pending_default_admin_schedule.write(0); + } + + let caller = starknet::get_caller_address(); + assert(caller == account, Errors::INVALID_CALLER); + self._revoke_role(role, account); + } + } + + /// Adds camelCase support for `IAccessControl`. + #[embeddable_as(AccessControlCamelImpl)] + impl AccessControlCamel< + TContractState, + +HasComponent, + +ImmutableConfig, + +SRC5Component::HasComponent, + +Drop, + > of interface::IAccessControlCamel> { + fn hasRole( + self: @ComponentState, role: felt252, account: ContractAddress, + ) -> bool { + AccessControl::has_role(self, role, account) + } + + fn getRoleAdmin(self: @ComponentState, role: felt252) -> felt252 { + AccessControl::get_role_admin(self, role) + } + + fn grantRole( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + AccessControl::grant_role(ref self, role, account); + } + + fn revokeRole( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + AccessControl::revoke_role(ref self, role, account); + } + + fn renounceRole( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + AccessControl::renounce_role(ref self, role, account); + } + } + + #[embeddable_as(AccessControlWithDelayImpl)] + impl AccessControlWithDelay< + TContractState, + +HasComponent, + +ImmutableConfig, + +SRC5Component::HasComponent, + +Drop, + > of interface::IAccessControlWithDelay> { + /// Returns the account's status for the given role. + /// + /// The possible statuses are: + /// + /// - `NotGranted`: the role has not been granted to the account. + /// - `Delayed`: The role has been granted to the account but is not yet active due to a + /// time delay. + /// - `Effective`: the role has been granted to the account and is currently active. + fn get_role_status( + self: @ComponentState, role: felt252, account: ContractAddress, + ) -> RoleStatus { + self.resolve_role_status(role, account) + } + + /// Attempts to grant `role` to `account` with the specified activation delay. + /// + /// Requirements: + /// + /// - The caller must have `role`'s admin role. + /// - delay must be greater than 0. + /// - the `role` must not be already effective for `account`. + /// + /// May emit a `RoleGrantedWithDelay` event. + fn grant_role_with_delay( + ref self: ComponentState, + role: felt252, + account: ContractAddress, + delay: u64, + ) { + let admin = AccessControl::get_role_admin(@self, role); + self.assert_only_role(admin); + self._grant_role_with_delay(role, account, delay); + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + +ImmutableConfig, + impl SRC5: SRC5Component::HasComponent, + +Drop, + > of InternalTrait { + /// Initializes the contract by registering the IAccessControl interface ID and + /// setting the initial delay and default admin. + /// + /// Requirements: + /// + /// - initial_default_admin must not be the zero address. + fn initializer( + ref self: ComponentState, + initial_delay: u64, + initial_default_admin: ContractAddress, + ) { + assert(initial_default_admin.is_non_zero(), Errors::INVALID_DEFAULT_ADMIN); + + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + + let default_admin_rules_interface_id = + default_admin_rules_interface::IACCESSCONTROL_DEFAULT_ADMIN_RULES_ID; + src5_component.register_interface(interface::IACCESSCONTROL_ID); + src5_component.register_interface(default_admin_rules_interface_id); + + self.AccessControl_current_delay.write(initial_delay); + self._grant_role(DEFAULT_ADMIN_ROLE, initial_default_admin); + } + + /// Validates that the caller can act as the given role. Otherwise it panics. + fn assert_only_role(self: @ComponentState, role: felt252) { + let caller = starknet::get_caller_address(); + let authorized = self.is_role_effective(role, caller); + assert(authorized, Errors::MISSING_ROLE); + } + + /// Returns whether the account can act as the given role. + /// + /// The account can act as the role if it is active and the `effective_from` time is before + /// or equal to the current time. + /// + /// NOTE: If the `effective_from` timepoint is 0, the role is effective immediately. + /// This is backwards compatible with implementations that didn't use delays but + /// a single boolean flag. + fn is_role_effective( + self: @ComponentState, role: felt252, account: ContractAddress, + ) -> bool { + match self.resolve_role_status(role, account) { + RoleStatus::Effective => true, + RoleStatus::Delayed(_) => false, + RoleStatus::NotGranted => false, + } + } + + /// Returns the account's status for the given role. + /// + /// The possible statuses are: + /// + /// - `NotGranted`: the role has not been granted to the account. + /// - `Delayed`: The role has been granted to the account but is not yet active due to a + /// time delay. + /// - `Effective`: the role has been granted to the account and is currently active. + fn resolve_role_status( + self: @ComponentState, role: felt252, account: ContractAddress, + ) -> RoleStatus { + let AccountRoleInfo { + is_granted, effective_from, + } = self.AccessControl_role_member.read((role, account)); + if is_granted { + if effective_from == 0 { + RoleStatus::Effective + } else { + let now = starknet::get_block_timestamp(); + if effective_from <= now { + RoleStatus::Effective + } else { + RoleStatus::Delayed(effective_from) + } + } + } else { + RoleStatus::NotGranted + } + } + + /// Returns whether the account has the given role granted. + /// + /// NOTE: The account may not be able to act as the role yet, if a delay was set and has not + /// passed yet. Use `is_role_effective` to check if the account can act as the role. + fn is_role_granted( + self: @ComponentState, role: felt252, account: ContractAddress, + ) -> bool { + let account_role_info = self.AccessControl_role_member.read((role, account)); + account_role_info.is_granted + } + + /// Sets `admin_role` as `role`'s admin role. + /// + /// Internal function without access restriction. + /// + /// Requirements: + /// + /// - `role` must not be `DEFAULT_ADMIN_ROLE`. + /// + /// Emits a `RoleAdminChanged` event. + fn set_role_admin( + ref self: ComponentState, role: felt252, admin_role: felt252, + ) { + assert(role != DEFAULT_ADMIN_ROLE, Errors::ENFORCED_DEFAULT_ADMIN_RULES); + + let previous_admin_role = AccessControl::get_role_admin(@self, role); + self.AccessControl_role_admin.write(role, admin_role); + self.emit(RoleAdminChanged { role, previous_admin_role, new_admin_role: admin_role }); + } + + /// Attempts to grant `role` to `account`. The function does nothing if `role` is already + /// effective for `account`. If `role` has been granted to `account`, but is not yet active + /// due to a time delay, the delay is removed and `role` becomes effective immediately. + /// + /// Internal function without access restriction. + /// + /// For `DEFAULT_ADMIN_ROLE`, it only allows granting if there isn't already a + /// `default_admin` + /// or if the role has been previously renounced. + /// + /// NOTE: Exposing this function through another mechanism may make the `DEFAULT_ADMIN_ROLE` + /// assignable again. Make sure to guarantee this is the expected behavior in your + /// implementation. + /// + /// May emit a `RoleGranted` event. + fn _grant_role( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + if role == DEFAULT_ADMIN_ROLE { + assert( + AccessControlDefaultAdminRules::default_admin(@self) == Zero::zero(), + Errors::ENFORCED_DEFAULT_ADMIN_RULES, + ); + self.AccessControl_current_default_admin.write(account); + } + + match self.resolve_role_status(role, account) { + RoleStatus::Effective => (), + RoleStatus::Delayed(_) | + RoleStatus::NotGranted => { + let caller = starknet::get_caller_address(); + let role_info = AccountRoleInfo { is_granted: true, effective_from: 0 }; + self.AccessControl_role_member.write((role, account), role_info); + self.emit(RoleGranted { role, account, sender: caller }); + }, + }; + } + + /// Attempts to grant `role` to `account` with the specified activation delay. + /// + /// The role will become effective after the given delay has passed. If the role is already + /// active (`Effective`) for the account, the function will panic. If the role has been + /// granted but is not yet active (being in the `Delayed` state), the existing delay will be + /// overwritten with the new `delay`. + /// + /// Internal function without access restriction. + /// + /// Requirements: + /// + /// - delay must be greater than 0. + /// - the `role` must not be already effective for `account`. + /// - `role` must not be `DEFAULT_ADMIN_ROLE`. + /// + /// May emit a `RoleGrantedWithDelay` event. + fn _grant_role_with_delay( + ref self: ComponentState, + role: felt252, + account: ContractAddress, + delay: u64, + ) { + assert(role != DEFAULT_ADMIN_ROLE, Errors::ENFORCED_DEFAULT_ADMIN_RULES); + assert(delay > 0, Errors::INVALID_DELAY); + + match self.resolve_role_status(role, account) { + RoleStatus::Effective => panic_with_const_felt252::(), + RoleStatus::Delayed(_) | + RoleStatus::NotGranted => { + let caller = starknet::get_caller_address(); + let effective_from = starknet::get_block_timestamp() + delay; + let role_info = AccountRoleInfo { is_granted: true, effective_from }; + self.AccessControl_role_member.write((role, account), role_info); + self.emit(RoleGrantedWithDelay { role, account, sender: caller, delay }); + }, + }; + } + + /// Attempts to revoke `role` from `account`. + /// + /// Internal function without access restriction. + /// + /// May emit a `RoleRevoked` event. + fn _revoke_role( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + if role == DEFAULT_ADMIN_ROLE + && account == AccessControlDefaultAdminRules::default_admin(@self) { + self.AccessControl_current_default_admin.write(Zero::zero()); + } + + match self.resolve_role_status(role, account) { + RoleStatus::NotGranted => (), + RoleStatus::Effective | + RoleStatus::Delayed(_) => { + let caller = starknet::get_caller_address(); + let role_info = AccountRoleInfo { is_granted: false, effective_from: 0 }; + self.AccessControl_role_member.write((role, account), role_info); + self.emit(RoleRevoked { role, account, sender: caller }); + }, + }; + } + + /// Setter of the tuple for pending admin and its schedule. + /// + /// May emit a `DefaultAdminTransferCanceled` event. + fn set_pending_default_admin( + ref self: ComponentState, new_admin: ContractAddress, new_schedule: u64, + ) { + let (_, old_schedule) = AccessControlDefaultAdminRules::pending_default_admin(@self); + + self.AccessControl_pending_default_admin.write(new_admin); + self.AccessControl_pending_default_admin_schedule.write(new_schedule); + + // An `old_schedule` from `pending_default_admin()` is only set if it hasn't been + // accepted + if is_schedule_set(old_schedule) { + self.emit(DefaultAdminTransferCanceled {}); + } + } + + /// Setter of the tuple for pending delay and its schedule. + /// + /// May emit a `DefaultAdminDelayChangeCanceled` event. + fn set_pending_delay( + ref self: ComponentState, new_delay: u64, new_schedule: u64, + ) { + let pending_delay = self.AccessControl_pending_delay.read(); + let old_schedule = pending_delay.schedule; + + if is_schedule_set(old_schedule) { + if has_schedule_passed(old_schedule) { + // Materialize a virtual delay + self.AccessControl_current_delay.write(pending_delay.delay); + } else { + // Emit for implicit cancellations when another delay was scheduled + self.emit(DefaultAdminDelayChangeCanceled {}); + } + } + + let new_pending_delay = PendingDelay { delay: new_delay, schedule: new_schedule }; + self.AccessControl_pending_delay.write(new_pending_delay); + } + + /// Returns the amount of seconds to wait after the `new_delay` will + /// become the new `default_admin_delay`. + /// + /// The value returned guarantees that if the delay is reduced, it will go into effect + /// after a wait that honors the previously set delay. + /// + /// See `default_admin_delay_increase_wait`. + fn delay_change_wait(self: @ComponentState, new_delay: u64) -> u64 { + let current_delay = self.AccessControl_current_delay.read(); + + // When increasing the delay, we schedule the delay change to occur after a period of + // "new delay" has passed, up to a maximum given by defaultAdminDelayIncreaseWait, by + // default 5 days. For example, if increasing from 1 day to 3 days, the new delay will + // come into effect after 3 days. If increasing from 1 day to 10 days, the new delay + // will come into effect after 5 days. The 5 day wait period is intended to be able to + // fix an error like using milliseconds instead of seconds. + // + // When decreasing the delay, we wait the difference between "current delay" and "new + // delay". This guarantees that an admin transfer cannot be made faster than "current + // delay" at the time the delay change is scheduled. + // For example, if decreasing from 10 days to 3 days, the new delay will come into + // effect after 7 days. + if new_delay > current_delay { + core::cmp::min( + new_delay, + AccessControlDefaultAdminRules::default_admin_delay_increase_wait(self), + ) + } else { + current_delay - new_delay + } + } + } + + #[embeddable_as(AccessControlMixinImpl)] + impl AccessControlMixin< + TContractState, + +HasComponent, + +ImmutableConfig, + impl SRC5: SRC5Component::HasComponent, + +Drop, + > of default_admin_rules_interface::AccessControlDefaultAdminRulesABI< + ComponentState, + > { + // IAccessControlDefaultAdminRules + fn default_admin(self: @ComponentState) -> ContractAddress { + AccessControlDefaultAdminRules::default_admin(self) + } + + fn pending_default_admin(self: @ComponentState) -> (ContractAddress, u64) { + AccessControlDefaultAdminRules::pending_default_admin(self) + } + + fn default_admin_delay(self: @ComponentState) -> u64 { + AccessControlDefaultAdminRules::default_admin_delay(self) + } + + fn pending_default_admin_delay(self: @ComponentState) -> (u64, u64) { + AccessControlDefaultAdminRules::pending_default_admin_delay(self) + } + + fn begin_default_admin_transfer( + ref self: ComponentState, new_admin: ContractAddress, + ) { + AccessControlDefaultAdminRules::begin_default_admin_transfer(ref self, new_admin); + } + + fn cancel_default_admin_transfer(ref self: ComponentState) { + AccessControlDefaultAdminRules::cancel_default_admin_transfer(ref self); + } + + fn accept_default_admin_transfer(ref self: ComponentState) { + AccessControlDefaultAdminRules::accept_default_admin_transfer(ref self); + } + + fn change_default_admin_delay(ref self: ComponentState, new_delay: u64) { + AccessControlDefaultAdminRules::change_default_admin_delay(ref self, new_delay); + } + + fn rollback_default_admin_delay(ref self: ComponentState) { + AccessControlDefaultAdminRules::rollback_default_admin_delay(ref self); + } + + fn default_admin_delay_increase_wait(self: @ComponentState) -> u64 { + AccessControlDefaultAdminRules::default_admin_delay_increase_wait(self) + } + + // IAccessControl + fn has_role( + self: @ComponentState, role: felt252, account: ContractAddress, + ) -> bool { + AccessControl::has_role(self, role, account) + } + + fn get_role_admin(self: @ComponentState, role: felt252) -> felt252 { + AccessControl::get_role_admin(self, role) + } + + fn grant_role( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + AccessControl::grant_role(ref self, role, account); + } + + fn revoke_role( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + AccessControl::revoke_role(ref self, role, account); + } + + fn renounce_role( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + AccessControl::renounce_role(ref self, role, account); + } + + // IAccessControlCamel + fn hasRole( + self: @ComponentState, role: felt252, account: ContractAddress, + ) -> bool { + AccessControlCamel::hasRole(self, role, account) + } + + fn getRoleAdmin(self: @ComponentState, role: felt252) -> felt252 { + AccessControlCamel::getRoleAdmin(self, role) + } + + fn grantRole( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + AccessControlCamel::grantRole(ref self, role, account); + } + + fn revokeRole( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + AccessControlCamel::revokeRole(ref self, role, account); + } + + fn renounceRole( + ref self: ComponentState, role: felt252, account: ContractAddress, + ) { + AccessControlCamel::renounceRole(ref self, role, account); + } + + // IAccessControlWithDelay + fn get_role_status( + self: @ComponentState, role: felt252, account: ContractAddress, + ) -> RoleStatus { + AccessControlWithDelay::get_role_status(self, role, account) + } + + fn grant_role_with_delay( + ref self: ComponentState, + role: felt252, + account: ContractAddress, + delay: u64, + ) { + AccessControlWithDelay::grant_role_with_delay(ref self, role, account, delay); + } + + // ISRC5 + fn supports_interface( + self: @ComponentState, interface_id: felt252, + ) -> bool { + let src5 = get_dep_component!(self, SRC5); + src5.supports_interface(interface_id) + } + } + + // + // Private helpers + // + + /// Defines if an `schedule` is considered set. For consistency purposes. + fn is_schedule_set(schedule: u64) -> bool { + schedule != 0 + } + + /// Defines if an `schedule` is considered passed. For consistency purposes. + fn has_schedule_passed(schedule: u64) -> bool { + let now = starknet::get_block_timestamp(); + now >= schedule + } +} + +/// Implementation of the default ERC20Component ImmutableConfig. +/// +/// See +/// https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-107.md#defaultconfig-implementation +/// +/// The default delay increase wait is set to `DEFAULT_ADMIN_DELAY_INCREASE_WAIT`. +pub impl DefaultConfig of AccessControlDefaultAdminRulesComponent::ImmutableConfig { + const DEFAULT_ADMIN_DELAY_INCREASE_WAIT: u64 = 5 * 24 * 60 * 60; // 5 days +} diff --git a/packages/access/src/accesscontrol/extensions/interface.cairo b/packages/access/src/accesscontrol/extensions/interface.cairo new file mode 100644 index 000000000..900426d3a --- /dev/null +++ b/packages/access/src/accesscontrol/extensions/interface.cairo @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v2.0.0 +// (access/src/accesscontrol/extensions/interface.cairo) + +use starknet::ContractAddress; +use crate::accesscontrol::interface::RoleStatus; + +pub const IACCESSCONTROL_DEFAULT_ADMIN_RULES_ID: felt252 = + 0x3509b3083c9586afe5dae781146b0608c3846870510f8d4d21ae38676cc33eb; + +#[starknet::interface] +pub trait IAccessControlDefaultAdminRules { + /// Returns the address of the current `DEFAULT_ADMIN_ROLE` holder. + fn default_admin(self: @TState) -> ContractAddress; + + /// Returns a tuple of a `new_admin` and an `accept_schedule`. + /// + /// After the `accept_schedule` passes, the `new_admin` will be able to accept the + /// `default_admin` role by calling `accept_default_admin_transfer`, completing the role + /// transfer. + /// + /// A zero value only in `accept_schedule` indicates no pending admin transfer. + /// + /// NOTE: A zero address `new_admin` means that `default_admin` is being renounced. + fn pending_default_admin(self: @TState) -> (ContractAddress, u64); + + /// Returns the delay required to schedule the acceptance of a `default_admin` transfer started. + /// + /// This delay will be added to the current timestamp when calling + /// `begin_default_admin_transfer` to set the acceptance schedule. + /// + /// NOTE: If a delay change has been scheduled, it will take effect as soon as the schedule + /// passes, making this function return the new delay. + /// + /// See `change_default_admin_delay`. + fn default_admin_delay(self: @TState) -> u64; + + /// Returns a tuple of `new_delay` and an `effect_schedule`. + /// + /// After the `effect_schedule` passes, the `new_delay` will get into effect immediately for + /// every new `default_admin` transfer started with `begin_default_admin_transfer`. + /// + /// A zero value only in `effect_schedule` indicates no pending delay change. + /// + /// NOTE: A zero value only for `new_delay` means that the next `default_admin_delay` + /// will be zero after the effect schedule. + fn pending_default_admin_delay(self: @TState) -> (u64, u64); + + /// Starts a `default_admin` transfer by setting a `pending_default_admin` scheduled for + /// acceptance after the current timestamp plus a `default_admin_delay`. + /// + /// Requirements: + /// + /// - Only can be called by the current `default_admin`. + /// + /// Emits a `DefaultAdminRoleChangeStarted` event. + fn begin_default_admin_transfer(ref self: TState, new_admin: ContractAddress); + + /// Cancels a `default_admin` transfer previously started with `begin_default_admin_transfer`. + /// + /// A `pending_default_admin` not yet accepted can also be canceled with this function. + /// + /// Requirements: + /// + /// - Only can be called by the current `default_admin`. + /// + /// May emit a `DefaultAdminTransferCanceled` event. + fn cancel_default_admin_transfer(ref self: TState); + + /// Completes a `default_admin` transfer previously started with `begin_default_admin_transfer`. + /// + /// After calling the function: + /// + /// - `DEFAULT_ADMIN_ROLE` must be granted to the caller. + /// - `DEFAULT_ADMIN_ROLE` must be revoked from the previous holder. + /// - `pending_default_admin` must be reset to zero value. + /// + /// Requirements: + /// + /// - Only can be called by the `pending_default_admin`'s `new_admin`. + /// - The `pending_default_admin`'s `accept_schedule` should've passed. + fn accept_default_admin_transfer(ref self: TState); + + /// Initiates a `default_admin_delay` update by setting a `pending_default_admin_delay` + /// scheduled to take effect after the current timestamp plus a `default_admin_delay`. + /// + /// This function guarantees that any call to `begin_default_admin_transfer` done between the + /// timestamp this method is called at and the `pending_default_admin_delay` effect schedule + /// will use the current `default_admin_delay` set before calling. + /// + /// The `pending_default_admin_delay`'s effect schedule is defined in a way that waiting until + /// the schedule and then calling `begin_default_admin_transfer` with the new delay will take at + /// least the same as another `default_admin` complete transfer (including acceptance). + /// + /// The schedule is designed for two scenarios: + /// + /// - When the delay is changed for a larger one the schedule is `block.timestamp + new delay` + /// capped by `default_admin_delay_increase_wait`. + /// - When the delay is changed for a shorter one, the schedule is `block.timestamp + (current + /// delay - new delay)`. + /// + /// A `pending_default_admin_delay` that never got into effect will be canceled in favor of a + /// new scheduled change. + /// + /// Requirements: + /// + /// - Only can be called by the current `default_admin`. + /// + /// Emits a `DefaultAdminDelayChangeScheduled` event and may emit a + /// `DefaultAdminDelayChangeCanceled` event. + fn change_default_admin_delay(ref self: TState, new_delay: u64); + + /// Cancels a scheduled `default_admin_delay` change. + /// + /// Requirements: + /// + /// - Only can be called by the current `default_admin`. + /// + /// May emit a `DefaultAdminDelayChangeCanceled` event. + fn rollback_default_admin_delay(ref self: TState); + + /// Maximum time in seconds for an increase to `default_admin_delay` (that is scheduled using + /// `change_default_admin_delay`) to take effect. Defaults to 5 days. + /// + /// When the `default_admin_delay` is scheduled to be increased, it goes into effect after the + /// new delay has passed with the purpose of giving enough time for reverting any accidental + /// change (i.e. using milliseconds instead of seconds) + /// that may lock the contract. However, to avoid excessive schedules, the wait is capped by + /// this function and it can be overridden for a custom `default_admin_delay` increase + /// scheduling. + /// + /// IMPORTANT: Make sure to add a reasonable amount of time while overriding this value, + /// otherwise, there's a risk of setting a high new delay that goes into effect almost + /// immediately without the possibility of human intervention in the case of an input error + /// (e.g. + /// set milliseconds instead of seconds). + fn default_admin_delay_increase_wait(self: @TState) -> u64; +} + +#[starknet::interface] +pub trait AccessControlDefaultAdminRulesABI { + // IAccessControlDefaultAdminRules + fn default_admin(self: @TState) -> ContractAddress; + fn pending_default_admin(self: @TState) -> (ContractAddress, u64); + fn default_admin_delay(self: @TState) -> u64; + fn pending_default_admin_delay(self: @TState) -> (u64, u64); + fn begin_default_admin_transfer(ref self: TState, new_admin: ContractAddress); + fn cancel_default_admin_transfer(ref self: TState); + fn accept_default_admin_transfer(ref self: TState); + fn change_default_admin_delay(ref self: TState, new_delay: u64); + fn rollback_default_admin_delay(ref self: TState); + fn default_admin_delay_increase_wait(self: @TState) -> u64; + + // IAccessControl + fn has_role(self: @TState, role: felt252, account: ContractAddress) -> bool; + fn get_role_admin(self: @TState, role: felt252) -> felt252; + fn grant_role(ref self: TState, role: felt252, account: ContractAddress); + fn revoke_role(ref self: TState, role: felt252, account: ContractAddress); + fn renounce_role(ref self: TState, role: felt252, account: ContractAddress); + + // IAccessControlCamel + fn hasRole(self: @TState, role: felt252, account: ContractAddress) -> bool; + fn getRoleAdmin(self: @TState, role: felt252) -> felt252; + fn grantRole(ref self: TState, role: felt252, account: ContractAddress); + fn revokeRole(ref self: TState, role: felt252, account: ContractAddress); + fn renounceRole(ref self: TState, role: felt252, account: ContractAddress); + + // IAccessControlWithDelay + fn get_role_status(self: @TState, role: felt252, account: ContractAddress) -> RoleStatus; + fn grant_role_with_delay(ref self: TState, role: felt252, account: ContractAddress, delay: u64); + + // ISRC5 + fn supports_interface(self: @TState, interface_id: felt252) -> bool; +} diff --git a/packages/access/src/accesscontrol/extensions/pending_delay.cairo b/packages/access/src/accesscontrol/extensions/pending_delay.cairo new file mode 100644 index 000000000..2e60b2391 --- /dev/null +++ b/packages/access/src/accesscontrol/extensions/pending_delay.cairo @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v2.0.0 +// (access/src/accesscontrol/extensions/pending_delay.cairo) + +use core::integer::u128_safe_divmod; +use starknet::storage_access::StorePacking; + +/// Information about a scheduledpending delay. +#[derive(Copy, Drop, Serde, PartialEq, Debug)] +pub struct PendingDelay { + pub delay: u64, + pub schedule: u64 // 0 if not scheduled +} + +const _2_POW_64: NonZero = 0x10000000000000000; + +/// Packs an PendingDelay into a single u128. +/// +/// The packing is done as follows: +/// +/// 1. `delay` is stored at range [0,63] (0-indexed starting from the most significant bits). +/// 2. `schedule` is stored at range [64, 127], following `delay`. +impl PendingDelayStorePacking of StorePacking { + fn pack(value: PendingDelay) -> u128 { + let PendingDelay { delay, schedule } = value; + let delay_with_offset = delay.into() * _2_POW_64.into(); + + delay_with_offset + schedule.into() + } + + fn unpack(value: u128) -> PendingDelay { + let (delay, schedule) = u128_safe_divmod(value, _2_POW_64); + + // It is safe to unwrap because the two values were packed from u64 integers + PendingDelay { delay: delay.try_into().unwrap(), schedule: schedule.try_into().unwrap() } + } +} + +#[cfg(test)] +mod tests { + use core::num::traits::Bounded; + use super::{PendingDelay, PendingDelayStorePacking}; + + #[test] + fn test_pack_and_unpack() { + let pending_delay = PendingDelay { delay: 100, schedule: 200 }; + let packed = PendingDelayStorePacking::pack(pending_delay); + let unpacked = PendingDelayStorePacking::unpack(packed); + assert_eq!(pending_delay, unpacked); + } + + #[test] + fn test_pack_and_unpack_big_values() { + let pending_delay = PendingDelay { delay: Bounded::MAX, schedule: Bounded::MAX }; + let packed = PendingDelayStorePacking::pack(pending_delay); + let unpacked = PendingDelayStorePacking::unpack(packed); + assert_eq!(pending_delay, unpacked); + } + + #[test] + #[fuzzer] + fn test_pack_and_unpack_fuzz(delay: u64, schedule: u64) { + let pending_delay = PendingDelay { delay, schedule }; + let packed_value = PendingDelayStorePacking::pack(pending_delay); + let unpacked_info = PendingDelayStorePacking::unpack(packed_value); + + assert_eq!(unpacked_info.delay, delay); + assert_eq!(unpacked_info.schedule, schedule); + } +} diff --git a/packages/access/src/tests.cairo b/packages/access/src/tests.cairo index 173ef02da..1ede88df4 100644 --- a/packages/access/src/tests.cairo +++ b/packages/access/src/tests.cairo @@ -1,3 +1,4 @@ mod test_accesscontrol; +mod test_accesscontrol_default_admin_rules; mod test_ownable; mod test_ownable_twostep; diff --git a/packages/access/src/tests/test_accesscontrol.cairo b/packages/access/src/tests/test_accesscontrol.cairo index d040b953d..8397c9b58 100644 --- a/packages/access/src/tests/test_accesscontrol.cairo +++ b/packages/access/src/tests/test_accesscontrol.cairo @@ -692,7 +692,20 @@ fn test_default_admin_role_is_its_own_admin() { // #[generate_trait] -impl AccessControlSpyHelpersImpl of AccessControlSpyHelpers { +pub impl AccessControlSpyHelpersImpl of AccessControlSpyHelpers { + fn assert_event_role_revoked( + ref self: EventSpy, + contract: ContractAddress, + role: felt252, + account: ContractAddress, + sender: ContractAddress, + ) { + let expected = AccessControlComponent::Event::RoleRevoked( + RoleRevoked { role, account, sender }, + ); + self.assert_emitted_single(contract, expected); + } + fn assert_only_event_role_revoked( ref self: EventSpy, contract: ContractAddress, diff --git a/packages/access/src/tests/test_accesscontrol_default_admin_rules.cairo b/packages/access/src/tests/test_accesscontrol_default_admin_rules.cairo new file mode 100644 index 000000000..5c0cd6d97 --- /dev/null +++ b/packages/access/src/tests/test_accesscontrol_default_admin_rules.cairo @@ -0,0 +1,1207 @@ +use openzeppelin_introspection::interface::ISRC5; +use openzeppelin_test_common::mocks::access::DualCaseAccessControlDefaultAdminRulesMock; +use openzeppelin_test_common::mocks::access::DualCaseAccessControlDefaultAdminRulesMock::INITIAL_DELAY; +use openzeppelin_testing::constants::{ + ADMIN, AUTHORIZED, OTHER, OTHER_ADMIN, OTHER_ROLE, ROLE, TIMESTAMP, ZERO, +}; +use openzeppelin_testing::{EventSpyExt, EventSpyQueue as EventSpy, spy_events}; +use snforge_std::{start_cheat_block_timestamp_global, start_cheat_caller_address, test_address}; +use starknet::ContractAddress; +use crate::accesscontrol::extensions::AccessControlDefaultAdminRulesComponent::{ + DefaultAdminDelayChangeCanceled, DefaultAdminDelayChangeScheduled, DefaultAdminTransferCanceled, + DefaultAdminTransferScheduled, InternalTrait, +}; +use crate::accesscontrol::extensions::interface::{ + IACCESSCONTROL_DEFAULT_ADMIN_RULES_ID, IAccessControlDefaultAdminRules, +}; +use crate::accesscontrol::extensions::{ + AccessControlDefaultAdminRulesComponent, DEFAULT_ADMIN_ROLE, DefaultConfig, +}; +use crate::accesscontrol::interface::{ + IACCESSCONTROL_ID, IAccessControl, IAccessControlCamel, IAccessControlWithDelay, RoleStatus, +}; +use crate::tests::test_accesscontrol::AccessControlSpyHelpers; + +// +// Setup +// + +type ComponentState = + AccessControlDefaultAdminRulesComponent::ComponentState< + DualCaseAccessControlDefaultAdminRulesMock::ContractState, + >; + +fn CONTRACT_STATE() -> DualCaseAccessControlDefaultAdminRulesMock::ContractState { + DualCaseAccessControlDefaultAdminRulesMock::contract_state_for_testing() +} + +fn COMPONENT_STATE() -> ComponentState { + AccessControlDefaultAdminRulesComponent::component_state_for_testing() +} + +fn setup() -> ComponentState { + let mut state = COMPONENT_STATE(); + state.initializer(INITIAL_DELAY, ADMIN); + state +} + +const ONE_HOUR: u64 = 3600; + +// +// initializer +// + +#[test] +fn test_initializer() { + let mut state = COMPONENT_STATE(); + state.initializer(INITIAL_DELAY, ADMIN); + + // Check that the default admin role is granted + let has_role = state.has_role(DEFAULT_ADMIN_ROLE, ADMIN); + assert!(has_role); + + // Check that the IAccessControl interface is registered + let supports_iaccesscontrol = CONTRACT_STATE().src5.supports_interface(IACCESSCONTROL_ID); + assert!(supports_iaccesscontrol); + + // Check that the IAccessControlDefaultAdminRules interface is registered + let supports_iaccesscontrol_default_admin_rules = CONTRACT_STATE() + .src5 + .supports_interface(IACCESSCONTROL_DEFAULT_ADMIN_RULES_ID); + assert!(supports_iaccesscontrol_default_admin_rules); + + // Check that the delay is set + let delay = state.default_admin_delay(); + assert_eq!(delay, INITIAL_DELAY); +} + +#[test] +#[should_panic(expected: 'Invalid default admin')] +fn test_initializer_with_zero_address() { + let mut state = COMPONENT_STATE(); + state.initializer(INITIAL_DELAY, ZERO); +} + +// +// default_admin +// + +#[test] +fn test_default_admin() { + let mut state = setup(); + let default_admin = state.default_admin(); + assert_eq!(default_admin, ADMIN); +} + +// +// pending_default_admin +// + +#[test] +fn test_pending_default_admin_default_values() { + let mut state = setup(); + let (pending_default_admin, pending_default_admin_schedule) = state.pending_default_admin(); + assert_eq!(pending_default_admin, ZERO); + assert_eq!(pending_default_admin_schedule, 0); +} + +#[test] +fn test_pending_default_admin_set() { + let mut state = setup(); + let contract_address = test_address(); + + start_cheat_block_timestamp_global(TIMESTAMP); + start_cheat_caller_address(contract_address, ADMIN); + state.begin_default_admin_transfer(OTHER_ADMIN); + let (pending_default_admin, pending_default_admin_schedule) = state.pending_default_admin(); + + assert_eq!(pending_default_admin, OTHER_ADMIN); + assert_eq!(pending_default_admin_schedule, TIMESTAMP + INITIAL_DELAY); +} + +// +// default_admin_delay +// + +#[test] +fn test_default_admin_delay_default_values() { + let mut state = setup(); + let delay = state.default_admin_delay(); + assert_eq!(delay, INITIAL_DELAY); +} + +#[test] +fn test_default_admin_delay_pending_delay_schedule_not_passed() { + let mut state = setup(); + let new_delay = INITIAL_DELAY + ONE_HOUR; + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, ADMIN); + state.change_default_admin_delay(new_delay); + + // Check that the delay is not changed since the schedule + // for a delay change has not passed + let delay = state.default_admin_delay(); + assert_eq!(delay, INITIAL_DELAY); +} + +#[test] +fn test_default_admin_delay_pending_delay_schedule_passed() { + let mut state = setup(); + let new_delay = INITIAL_DELAY + ONE_HOUR; + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + state.change_default_admin_delay(new_delay); + + // Check that the delay is changed since the schedule + // for a delay change has passed + start_cheat_block_timestamp_global(TIMESTAMP + new_delay); + let delay = state.default_admin_delay(); + assert_eq!(delay, new_delay); +} + +// +// pending_default_admin_delay && change_default_admin_delay +// + +#[test] +fn test_pending_default_admin_delay_is_not_pending() { + let mut state = setup(); + let (pending_delay, pending_delay_schedule) = state.pending_default_admin_delay(); + assert_eq!(pending_delay, 0); + assert_eq!(pending_delay_schedule, 0); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_change_default_admin_delay_unauthorized() { + let mut state = setup(); + let new_delay = INITIAL_DELAY + ONE_HOUR; + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, OTHER); + state.change_default_admin_delay(new_delay); +} + +#[test] +fn test_pending_default_admin_delay_is_pending_increasing_delay() { + let mut state = setup(); + let new_delay = INITIAL_DELAY + ONE_HOUR; + let contract_address = test_address(); + + let mut spy = spy_events(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + state.change_default_admin_delay(new_delay); + + // The schedule must be the new delay since the it is increasing + let expected_schedule = TIMESTAMP + new_delay; + let (pending_delay, pending_delay_schedule) = state.pending_default_admin_delay(); + assert_eq!(pending_delay, new_delay); + assert_eq!(pending_delay_schedule, expected_schedule); + + spy + .assert_only_event_default_admin_delay_change_scheduled( + contract_address, new_delay, expected_schedule, + ); +} + +#[test] +fn test_pending_default_admin_delay_is_pending_decreasing_delay() { + let mut state = setup(); + let new_delay = INITIAL_DELAY - ONE_HOUR / 2; + let contract_address = test_address(); + + let mut spy = spy_events(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + state.change_default_admin_delay(new_delay); + + // The schedule must be the difference between the current delay and the new delay + let expected_schedule = TIMESTAMP + INITIAL_DELAY - new_delay; + let (pending_delay, pending_delay_schedule) = state.pending_default_admin_delay(); + assert_eq!(pending_delay, new_delay); + assert_eq!(pending_delay_schedule, expected_schedule); + + spy + .assert_only_event_default_admin_delay_change_scheduled( + contract_address, new_delay, expected_schedule, + ); +} + +#[test] +fn test_pending_default_admin_delay_increasing_after_schedule_limit() { + let mut state = setup(); + let new_delay = ONE_HOUR * 24 * 10; // 10 days + let contract_address = test_address(); + + let mut spy = spy_events(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + state.change_default_admin_delay(new_delay); + + // The schedule must be the limit of the increase wait + let expected_schedule = TIMESTAMP + DefaultConfig::DEFAULT_ADMIN_DELAY_INCREASE_WAIT; + let (pending_delay, pending_delay_schedule) = state.pending_default_admin_delay(); + assert_eq!(pending_delay, new_delay); + assert_eq!(pending_delay_schedule, expected_schedule); + + spy + .assert_only_event_default_admin_delay_change_scheduled( + contract_address, new_delay, expected_schedule, + ); +} + +// +// begin_default_admin_transfer +// + +#[test] +fn test_begin_default_admin_transfer() { + let mut state = setup(); + let contract_address = test_address(); + + let mut spy = spy_events(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + state.begin_default_admin_transfer(OTHER_ADMIN); + + let expected_schedule = TIMESTAMP + state.default_admin_delay(); + spy + .assert_only_event_default_admin_transfer_scheduled( + contract_address, OTHER_ADMIN, expected_schedule, + ); + + let (pending_default_admin, pending_default_admin_schedule) = state.pending_default_admin(); + assert_eq!(pending_default_admin, OTHER_ADMIN); + assert_eq!(pending_default_admin_schedule, expected_schedule); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_begin_default_admin_transfer_unauthorized() { + let mut state = setup(); + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, OTHER); + state.begin_default_admin_transfer(OTHER_ADMIN); +} + +// +// cancel_default_admin_transfer +// + +#[test] +fn test_cancel_default_admin_transfer() { + let mut state = setup(); + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, ADMIN); + state.begin_default_admin_transfer(OTHER_ADMIN); + + let mut spy = spy_events(); + state.cancel_default_admin_transfer(); + + spy.assert_only_event_default_admin_transfer_canceled(contract_address); + + let (pending_default_admin, pending_default_admin_schedule) = state.pending_default_admin(); + assert_eq!(pending_default_admin, ZERO); + assert_eq!(pending_default_admin_schedule, 0); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_cancel_default_admin_transfer_unauthorized() { + let mut state = setup(); + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, OTHER); + state.cancel_default_admin_transfer(); +} + +// +// accept_default_admin_transfer +// + +#[test] +fn test_accept_default_admin_transfer() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_block_timestamp_global(TIMESTAMP); + + start_cheat_caller_address(contract_address, ADMIN); + state.begin_default_admin_transfer(OTHER_ADMIN); + + let mut spy = spy_events(); + start_cheat_caller_address(contract_address, OTHER_ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP + state.default_admin_delay()); + state.accept_default_admin_transfer(); + + spy.assert_event_role_revoked(contract_address, DEFAULT_ADMIN_ROLE, ADMIN, OTHER_ADMIN); + spy + .assert_only_event_role_granted( + contract_address, DEFAULT_ADMIN_ROLE, OTHER_ADMIN, OTHER_ADMIN, + ); + + let has_role = state.has_role(DEFAULT_ADMIN_ROLE, OTHER_ADMIN); + assert!(has_role); + + let has_not_role = !state.has_role(DEFAULT_ADMIN_ROLE, ADMIN); + assert!(has_not_role); + + let (pending_default_admin, pending_default_admin_schedule) = state.pending_default_admin(); + assert_eq!(pending_default_admin, ZERO); + assert_eq!(pending_default_admin_schedule, 0); +} + +#[test] +#[should_panic(expected: 'Only new default admin allowed')] +fn test_accept_default_admin_transfer_unauthorized() { + let mut state = setup(); + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, ADMIN); + state.begin_default_admin_transfer(OTHER_ADMIN); + + start_cheat_caller_address(contract_address, OTHER); + state.accept_default_admin_transfer(); +} + +#[test] +#[should_panic(expected: 'Default admin delay enforced')] +fn test_accept_default_admin_transfer_unauthorized_when_schedule_not_passed() { + let mut state = setup(); + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, ADMIN); + state.begin_default_admin_transfer(OTHER_ADMIN); + + start_cheat_caller_address(contract_address, OTHER_ADMIN); + state.accept_default_admin_transfer(); +} + +// +// rollback_default_admin_delay +// + +#[test] +fn test_rollback_default_admin_delay() { + let mut state = setup(); + let new_delay = ONE_HOUR * 24 * 1; // 1 day + let contract_address = test_address(); + + start_cheat_block_timestamp_global(TIMESTAMP); + start_cheat_caller_address(contract_address, ADMIN); + state.change_default_admin_delay(new_delay); + + let (pending_delay, pending_delay_schedule) = state.pending_default_admin_delay(); + assert_eq!(pending_delay, new_delay); + assert_eq!(pending_delay_schedule, TIMESTAMP + new_delay); + + start_cheat_caller_address(contract_address, ADMIN); + let mut spy = spy_events(); + state.rollback_default_admin_delay(); + + spy.assert_only_event_default_admin_delay_change_canceled(contract_address); + + let (pending_delay, pending_delay_schedule) = state.pending_default_admin_delay(); + assert_eq!(pending_delay, 0); + assert_eq!(pending_delay_schedule, 0); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_rollback_default_admin_delay_unauthorized() { + let mut state = setup(); + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, ADMIN); + state.change_default_admin_delay(ONE_HOUR); + + start_cheat_caller_address(contract_address, OTHER); + state.rollback_default_admin_delay(); +} + +// +// default_admin_delay_increase_wait +// + +#[test] +fn test_default_admin_delay_increase_wait() { + let state = setup(); + let wait = state.default_admin_delay_increase_wait(); + assert_eq!(wait, DefaultConfig::DEFAULT_ADMIN_DELAY_INCREASE_WAIT); +} + +// +// has_role & hasRole +// + +#[test] +fn test_has_role() { + let mut state = setup(); + assert!(!state.has_role(ROLE, AUTHORIZED)); + state._grant_role(ROLE, AUTHORIZED); + assert!(state.has_role(ROLE, AUTHORIZED)); +} + +#[test] +fn test_hasRole() { + let mut state = setup(); + assert!(!state.hasRole(ROLE, AUTHORIZED)); + state._grant_role(ROLE, AUTHORIZED); + assert!(state.hasRole(ROLE, AUTHORIZED)); +} + +// +// assert_only_role +// + +#[test] +fn test_assert_only_role() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + state.grant_role(ROLE, AUTHORIZED); + + start_cheat_caller_address(contract_address, AUTHORIZED); + state.assert_only_role(ROLE); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_assert_only_role_unauthorized() { + let state = setup(); + start_cheat_caller_address(test_address(), OTHER); + state.assert_only_role(ROLE); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_assert_only_role_unauthorized_when_authorized_for_another_role() { + let mut state = setup(); + state.grant_role(ROLE, AUTHORIZED); + + start_cheat_caller_address(test_address(), AUTHORIZED); + state.assert_only_role(OTHER_ROLE); +} + +// +// grant_role & grantRole +// + +#[test] +#[should_panic(expected: 'Default admin rules enforced')] +fn test_grant_role_default_admin_role() { + let mut state = setup(); + state.grant_role(DEFAULT_ADMIN_ROLE, AUTHORIZED); +} + +#[test] +fn test_grant_role() { + let mut state = setup(); + let mut spy = spy_events(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + state.grant_role(ROLE, AUTHORIZED); + + spy.assert_only_event_role_granted(contract_address, ROLE, AUTHORIZED, ADMIN); + + let has_role = state.has_role(ROLE, AUTHORIZED); + assert!(has_role); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::Effective); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), true); +} + +#[test] +fn test_grantRole() { + let mut state = setup(); + let mut spy = spy_events(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + state.grantRole(ROLE, AUTHORIZED); + + spy.assert_only_event_role_granted(contract_address, ROLE, AUTHORIZED, ADMIN); + + let has_role = state.hasRole(ROLE, AUTHORIZED); + assert!(has_role); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::Effective); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), true); +} + +#[test] +fn test_grant_role_multiple_times_for_granted_role() { + let mut state = setup(); + start_cheat_caller_address(test_address(), ADMIN); + + state.grant_role(ROLE, AUTHORIZED); + state.grant_role(ROLE, AUTHORIZED); + assert!(state.has_role(ROLE, AUTHORIZED)); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::Effective); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), true); +} + +#[test] +fn test_grant_role_when_delayed() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + state.grant_role_with_delay(ROLE, AUTHORIZED, ONE_HOUR); + + let mut spy = spy_events(); + state.grant_role(ROLE, AUTHORIZED); + spy.assert_only_event_role_granted(contract_address, ROLE, AUTHORIZED, ADMIN); + + let has_role = state.has_role(ROLE, AUTHORIZED); + assert!(has_role); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::Effective); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), true); +} + +#[test] +fn test_grantRole_when_delayed() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + state.grant_role_with_delay(ROLE, AUTHORIZED, ONE_HOUR); + + let mut spy = spy_events(); + state.grantRole(ROLE, AUTHORIZED); + spy.assert_only_event_role_granted(contract_address, ROLE, AUTHORIZED, ADMIN); + + let has_role = state.has_role(ROLE, AUTHORIZED); + assert!(has_role); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::Effective); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), true); +} + +#[test] +fn test_grantRole_multiple_times_for_granted_role() { + let mut state = setup(); + start_cheat_caller_address(test_address(), ADMIN); + + state.grantRole(ROLE, AUTHORIZED); + state.grantRole(ROLE, AUTHORIZED); + assert!(state.hasRole(ROLE, AUTHORIZED)); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::Effective); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), true); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_grant_role_unauthorized() { + let mut state = setup(); + start_cheat_caller_address(test_address(), AUTHORIZED); + state.grant_role(ROLE, AUTHORIZED); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_grantRole_unauthorized() { + let mut state = setup(); + start_cheat_caller_address(test_address(), AUTHORIZED); + state.grantRole(ROLE, AUTHORIZED); +} + +// +// grant_role_with_delay +// + +#[test] +#[should_panic(expected: 'Default admin rules enforced')] +fn test_grant_role_with_delay_default_admin_role() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + + state.grant_role_with_delay(DEFAULT_ADMIN_ROLE, AUTHORIZED, ONE_HOUR); +} + +#[test] +fn test_grant_role_with_delay() { + let mut state = setup(); + let mut spy = spy_events(); + let delay = ONE_HOUR; + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + + state.grant_role_with_delay(ROLE, AUTHORIZED, delay); + spy.assert_only_event_role_granted_with_delay(contract_address, ROLE, AUTHORIZED, ADMIN, delay); + + // Right after granting the role + let has_role = state.has_role(ROLE, AUTHORIZED); + assert_eq!(has_role, false); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::Delayed(TIMESTAMP + delay)); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), false); + + // When the delay has passed + start_cheat_block_timestamp_global(TIMESTAMP + delay); + let has_role = state.has_role(ROLE, AUTHORIZED); + assert_eq!(has_role, true); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::Effective); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), true); +} + +#[test] +#[should_panic(expected: 'Delay must be greater than 0')] +fn test_grant_role_with_zero_delay() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + state.grant_role_with_delay(ROLE, AUTHORIZED, 0); +} + +#[test] +#[should_panic(expected: 'Role is already effective')] +fn test_grant_role_with_delay_when_already_effective() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + state.grant_role(ROLE, AUTHORIZED); + state.grant_role_with_delay(ROLE, AUTHORIZED, ONE_HOUR); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_grant_role_with_delay_unauthorized() { + let mut state = setup(); + start_cheat_caller_address(test_address(), AUTHORIZED); + start_cheat_block_timestamp_global(TIMESTAMP); + state.grant_role_with_delay(ROLE, AUTHORIZED, ONE_HOUR); +} + +// +// revoke_role & revokeRole +// + +#[test] +#[should_panic(expected: 'Default admin rules enforced')] +fn test_revoke_role_default_admin_role() { + let mut state = setup(); + state.revoke_role(DEFAULT_ADMIN_ROLE, AUTHORIZED); +} + +#[test] +fn test_revoke_role_for_role_not_granted() { + let mut state = setup(); + start_cheat_caller_address(test_address(), ADMIN); + state.revoke_role(ROLE, AUTHORIZED); +} + +#[test] +fn test_revokeRole_for_role_not_granted() { + let mut state = setup(); + start_cheat_caller_address(test_address(), ADMIN); + state.revokeRole(ROLE, AUTHORIZED); +} + +#[test] +fn test_revoke_role_for_granted_role() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + + state.grant_role(ROLE, AUTHORIZED); + + let mut spy = spy_events(); + state.revoke_role(ROLE, AUTHORIZED); + + spy.assert_only_event_role_revoked(contract_address, ROLE, AUTHORIZED, ADMIN); + + let has_not_role = !state.has_role(ROLE, AUTHORIZED); + assert!(has_not_role); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::NotGranted); + assert_eq!(state.is_role_granted(ROLE, AUTHORIZED), false); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), false); +} + +#[test] +fn test_revokeRole_for_granted_role() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + + state.grantRole(ROLE, AUTHORIZED); + + let mut spy = spy_events(); + state.revokeRole(ROLE, AUTHORIZED); + + spy.assert_only_event_role_revoked(contract_address, ROLE, AUTHORIZED, ADMIN); + + let has_not_role = !state.hasRole(ROLE, AUTHORIZED); + assert!(has_not_role); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::NotGranted); + assert_eq!(state.is_role_granted(ROLE, AUTHORIZED), false); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), false); +} + +#[test] +fn test_revoke_role_for_delayed_role() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + + state.grant_role_with_delay(ROLE, AUTHORIZED, ONE_HOUR); + + let mut spy = spy_events(); + state.revoke_role(ROLE, AUTHORIZED); + + spy.assert_only_event_role_revoked(contract_address, ROLE, AUTHORIZED, ADMIN); + + let has_not_role = !state.has_role(ROLE, AUTHORIZED); + assert!(has_not_role); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::NotGranted); + assert_eq!(state.is_role_granted(ROLE, AUTHORIZED), false); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), false); +} + +#[test] +fn test_revokeRole_for_delayed_role() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + + state.grant_role_with_delay(ROLE, AUTHORIZED, ONE_HOUR); + + let mut spy = spy_events(); + state.revokeRole(ROLE, AUTHORIZED); + + spy.assert_only_event_role_revoked(contract_address, ROLE, AUTHORIZED, ADMIN); + + let has_not_role = !state.hasRole(ROLE, AUTHORIZED); + assert!(has_not_role); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::NotGranted); + assert_eq!(state.is_role_granted(ROLE, AUTHORIZED), false); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), false); +} + +#[test] +fn test_revoke_role_multiple_times_for_granted_role() { + let mut state = setup(); + start_cheat_caller_address(test_address(), ADMIN); + + state.grant_role(ROLE, AUTHORIZED); + state.revoke_role(ROLE, AUTHORIZED); + state.revoke_role(ROLE, AUTHORIZED); + + let has_not_role = !state.has_role(ROLE, AUTHORIZED); + assert!(has_not_role); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::NotGranted); + assert_eq!(state.is_role_granted(ROLE, AUTHORIZED), false); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), false); +} + +#[test] +fn test_revokeRole_multiple_times_for_granted_role() { + let mut state = setup(); + start_cheat_caller_address(test_address(), ADMIN); + + state.grantRole(ROLE, AUTHORIZED); + state.revokeRole(ROLE, AUTHORIZED); + state.revokeRole(ROLE, AUTHORIZED); + + let has_not_role = !state.hasRole(ROLE, AUTHORIZED); + assert!(has_not_role); + assert_eq!(state.get_role_status(ROLE, AUTHORIZED), RoleStatus::NotGranted); + assert_eq!(state.is_role_granted(ROLE, AUTHORIZED), false); + assert_eq!(state.is_role_effective(ROLE, AUTHORIZED), false); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_revoke_role_unauthorized() { + let mut state = setup(); + start_cheat_caller_address(test_address(), OTHER); + state.revoke_role(ROLE, AUTHORIZED); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_revokeRole_unauthorized() { + let mut state = setup(); + start_cheat_caller_address(test_address(), OTHER); + state.revokeRole(ROLE, AUTHORIZED); +} + +// +// renounce_role & renounceRole +// + +#[test] +fn test_renounce_role_default_admin_role() { + let mut state = setup(); + let contract_address = test_address(); + + start_cheat_block_timestamp_global(TIMESTAMP); + start_cheat_caller_address(contract_address, ADMIN); + state.begin_default_admin_transfer(ZERO); + + let has_role = state.has_role(DEFAULT_ADMIN_ROLE, ADMIN); + assert!(has_role); + + let mut spy = spy_events(); + start_cheat_block_timestamp_global(TIMESTAMP + ONE_HOUR); + state.renounce_role(DEFAULT_ADMIN_ROLE, ADMIN); + + spy.assert_only_event_role_revoked(contract_address, DEFAULT_ADMIN_ROLE, ADMIN, ADMIN); + + let has_not_role = !state.has_role(DEFAULT_ADMIN_ROLE, ADMIN); + assert!(has_not_role); +} + +#[test] +#[should_panic(expected: 'Default admin delay enforced')] +fn test_renounce_role_default_admin_role_pending_admin_schedule_not_set() { + let mut state = setup(); + state.renounce_role(DEFAULT_ADMIN_ROLE, ADMIN); +} + +#[test] +#[should_panic(expected: 'Default admin delay enforced')] +fn test_renounce_role_default_admin_role_pending_admin_not_zero() { + let mut state = setup(); + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, ADMIN); + state.begin_default_admin_transfer(OTHER_ADMIN); + state.renounce_role(DEFAULT_ADMIN_ROLE, ADMIN); +} + +#[test] +#[should_panic(expected: 'Default admin delay enforced')] +fn test_renounce_role_default_admin_role_pending_admin_schedule_not_passed() { + let mut state = setup(); + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, ADMIN); + state.begin_default_admin_transfer(ZERO); + + // Default admin delay (one hour) is not passed yet + state.renounce_role(DEFAULT_ADMIN_ROLE, ADMIN); +} + +#[test] +fn test_renounce_role_for_role_not_granted() { + let mut state = setup(); + start_cheat_caller_address(test_address(), AUTHORIZED); + state.renounce_role(ROLE, AUTHORIZED); +} + +#[test] +fn test_renounceRole_for_role_not_granted() { + let mut state = setup(); + start_cheat_caller_address(test_address(), AUTHORIZED); + state.renounceRole(ROLE, AUTHORIZED); +} + +#[test] +fn test_renounce_role_for_granted_role() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + + state.grant_role(ROLE, AUTHORIZED); + + let mut spy = spy_events(); + start_cheat_caller_address(contract_address, AUTHORIZED); + state.renounce_role(ROLE, AUTHORIZED); + + spy.assert_only_event_role_revoked(contract_address, ROLE, AUTHORIZED, AUTHORIZED); + + let has_not_role = !state.has_role(ROLE, AUTHORIZED); + assert!(has_not_role); +} + +#[test] +fn test_renounceRole_for_granted_role() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + + state.grantRole(ROLE, AUTHORIZED); + + let mut spy = spy_events(); + start_cheat_caller_address(contract_address, AUTHORIZED); + state.renounceRole(ROLE, AUTHORIZED); + + spy.assert_only_event_role_revoked(contract_address, ROLE, AUTHORIZED, AUTHORIZED); + + let has_not_role = !state.hasRole(ROLE, AUTHORIZED); + assert!(has_not_role); +} + +#[test] +fn test_renounce_role_for_delayed_role() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + + state.grant_role_with_delay(ROLE, AUTHORIZED, ONE_HOUR); + + let mut spy = spy_events(); + start_cheat_caller_address(contract_address, AUTHORIZED); + state.renounce_role(ROLE, AUTHORIZED); + + spy.assert_only_event_role_revoked(contract_address, ROLE, AUTHORIZED, AUTHORIZED); + + let has_not_role = !state.has_role(ROLE, AUTHORIZED); + assert!(has_not_role); +} + +#[test] +fn test_renounceRole_for_delayed_role() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + + state.grant_role_with_delay(ROLE, AUTHORIZED, ONE_HOUR); + + let mut spy = spy_events(); + start_cheat_caller_address(contract_address, AUTHORIZED); + state.renounceRole(ROLE, AUTHORIZED); + + spy.assert_only_event_role_revoked(contract_address, ROLE, AUTHORIZED, AUTHORIZED); + + let has_not_role = !state.hasRole(ROLE, AUTHORIZED); + assert!(has_not_role); +} + +#[test] +fn test_renounce_role_multiple_times_for_granted_role() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + state.grant_role(ROLE, AUTHORIZED); + + start_cheat_caller_address(contract_address, AUTHORIZED); + state.renounce_role(ROLE, AUTHORIZED); + state.renounce_role(ROLE, AUTHORIZED); + + let has_not_role = !state.has_role(ROLE, AUTHORIZED); + assert!(has_not_role); +} + +#[test] +fn test_renounceRole_multiple_times_for_granted_role() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + state.grantRole(ROLE, AUTHORIZED); + + start_cheat_caller_address(contract_address, AUTHORIZED); + state.renounceRole(ROLE, AUTHORIZED); + state.renounceRole(ROLE, AUTHORIZED); + + let has_not_role = !state.hasRole(ROLE, AUTHORIZED); + assert!(has_not_role); +} + +#[test] +#[should_panic(expected: 'Can only renounce role for self')] +fn test_renounce_role_unauthorized() { + let mut state = setup(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, ADMIN); + state.grant_role(ROLE, AUTHORIZED); + + start_cheat_caller_address(contract_address, ZERO); + state.renounce_role(ROLE, AUTHORIZED); +} + +#[test] +#[should_panic(expected: 'Can only renounce role for self')] +fn test_renounceRole_unauthorized() { + let mut state = setup(); + start_cheat_caller_address(test_address(), ADMIN); + state.grantRole(ROLE, AUTHORIZED); + + // Admin is unauthorized caller + state.renounceRole(ROLE, AUTHORIZED); +} + +// +// set_role_admin +// + +#[test] +#[should_panic(expected: 'Default admin rules enforced')] +fn test_set_role_admin_default_admin_role() { + let mut state = setup(); + state.set_role_admin(DEFAULT_ADMIN_ROLE, OTHER_ROLE); +} + +#[test] +fn test_set_role_admin() { + let mut state = setup(); + let contract_address = test_address(); + let mut spy = spy_events(); + + assert_eq!(state.get_role_admin(ROLE), DEFAULT_ADMIN_ROLE); + state.set_role_admin(ROLE, OTHER_ROLE); + + spy + .assert_only_event_role_admin_changed( + contract_address, ROLE, DEFAULT_ADMIN_ROLE, OTHER_ROLE, + ); + + let current_admin_role = state.get_role_admin(ROLE); + assert_eq!(current_admin_role, OTHER_ROLE); +} + +#[test] +fn test_new_admin_can_grant_roles() { + let mut state = setup(); + let contract_address = test_address(); + state.set_role_admin(ROLE, OTHER_ROLE); + + start_cheat_caller_address(contract_address, ADMIN); + state.grant_role(OTHER_ROLE, OTHER_ADMIN); + + start_cheat_caller_address(contract_address, OTHER_ADMIN); + state.grant_role(ROLE, AUTHORIZED); + + let has_role = state.has_role(ROLE, AUTHORIZED); + assert!(has_role); +} + +#[test] +fn test_new_admin_can_grant_roles_with_delay() { + let mut state = setup(); + let contract_address = test_address(); + state.set_role_admin(ROLE, OTHER_ROLE); + start_cheat_block_timestamp_global(TIMESTAMP); + + start_cheat_caller_address(contract_address, ADMIN); + state.grant_role(OTHER_ROLE, OTHER_ADMIN); + + let delay = ONE_HOUR; + start_cheat_caller_address(contract_address, OTHER_ADMIN); + state.grant_role_with_delay(ROLE, AUTHORIZED, delay); + + start_cheat_block_timestamp_global(TIMESTAMP + delay); + let has_role = state.has_role(ROLE, AUTHORIZED); + assert!(has_role); +} + +#[test] +fn test_new_admin_can_revoke_roles() { + let mut state = setup(); + let contract_address = test_address(); + state.set_role_admin(ROLE, OTHER_ROLE); + + start_cheat_caller_address(contract_address, ADMIN); + state.grant_role(OTHER_ROLE, OTHER_ADMIN); + + start_cheat_caller_address(contract_address, OTHER_ADMIN); + state.grant_role(ROLE, AUTHORIZED); + state.revoke_role(ROLE, AUTHORIZED); + + let has_not_role = !state.has_role(ROLE, AUTHORIZED); + assert!(has_not_role); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_previous_admin_cannot_grant_roles() { + let mut state = setup(); + state.set_role_admin(ROLE, OTHER_ROLE); + start_cheat_caller_address(test_address(), ADMIN); + state.grant_role(ROLE, AUTHORIZED); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_previous_admin_cannot_grant_roles_with_delay() { + let mut state = setup(); + state.set_role_admin(ROLE, OTHER_ROLE); + start_cheat_caller_address(test_address(), ADMIN); + start_cheat_block_timestamp_global(TIMESTAMP); + state.grant_role_with_delay(ROLE, AUTHORIZED, ONE_HOUR); +} + +#[test] +#[should_panic(expected: 'Caller is missing role')] +fn test_previous_admin_cannot_revoke_roles() { + let mut state = setup(); + state.set_role_admin(ROLE, OTHER_ROLE); + start_cheat_caller_address(test_address(), ADMIN); + state.revoke_role(ROLE, AUTHORIZED); +} + +// +// Default admin +// + +#[test] +fn test_other_role_admin_is_the_default_admin_role() { + let state = setup(); + let current_admin_role = state.get_role_admin(OTHER_ROLE); + assert_eq!(current_admin_role, DEFAULT_ADMIN_ROLE); +} + +#[test] +fn test_default_admin_role_is_its_own_admin() { + let state = setup(); + let current_admin_role = state.get_role_admin(DEFAULT_ADMIN_ROLE); + assert_eq!(current_admin_role, DEFAULT_ADMIN_ROLE); +} + +// +// Helpers +// + +#[generate_trait] +impl AccessControlDefaultAdminRulesSpyHelpersImpl of AccessControlDefaultAdminRulesSpyHelpers { + fn assert_only_event_default_admin_transfer_scheduled( + ref self: EventSpy, + contract: ContractAddress, + new_admin: ContractAddress, + accept_schedule: u64, + ) { + let expected = + AccessControlDefaultAdminRulesComponent::Event::DefaultAdminTransferScheduled( + DefaultAdminTransferScheduled { new_admin, accept_schedule }, + ); + self.assert_only_event(contract, expected); + } + + fn assert_only_event_default_admin_transfer_canceled( + ref self: EventSpy, contract: ContractAddress, + ) { + let expected = AccessControlDefaultAdminRulesComponent::Event::DefaultAdminTransferCanceled( + DefaultAdminTransferCanceled {}, + ); + self.assert_only_event(contract, expected); + } + + fn assert_only_event_default_admin_delay_change_scheduled( + ref self: EventSpy, contract: ContractAddress, new_delay: u64, effect_schedule: u64, + ) { + let expected = + AccessControlDefaultAdminRulesComponent::Event::DefaultAdminDelayChangeScheduled( + DefaultAdminDelayChangeScheduled { new_delay, effect_schedule }, + ); + self.assert_only_event(contract, expected); + } + + fn assert_only_event_default_admin_delay_change_canceled( + ref self: EventSpy, contract: ContractAddress, + ) { + let expected = + AccessControlDefaultAdminRulesComponent::Event::DefaultAdminDelayChangeCanceled( + DefaultAdminDelayChangeCanceled {}, + ); + self.assert_only_event(contract, expected); + } +} diff --git a/packages/test_common/src/mocks/access.cairo b/packages/test_common/src/mocks/access.cairo index 71e971a40..f32cff3f1 100644 --- a/packages/test_common/src/mocks/access.cairo +++ b/packages/test_common/src/mocks/access.cairo @@ -19,6 +19,46 @@ pub mod DualCaseAccessControlMock { } } +#[starknet::contract] +#[with_components(SRC5)] +pub mod DualCaseAccessControlDefaultAdminRulesMock { + use openzeppelin_access::accesscontrol::extensions::AccessControlDefaultAdminRulesComponent::InternalImpl; + use openzeppelin_access::accesscontrol::extensions::{ + AccessControlDefaultAdminRulesComponent, DefaultConfig, + }; + use starknet::ContractAddress; + + pub const INITIAL_DELAY: u64 = 3600; // 1 hour + + component!( + path: AccessControlDefaultAdminRulesComponent, + storage: access_control_dar, + event: AccessControlDAREvent, + ); + + #[abi(embed_v0)] + impl AccessControlMixinImpl = + AccessControlDefaultAdminRulesComponent::AccessControlMixinImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + access_control_dar: AccessControlDefaultAdminRulesComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AccessControlDAREvent: AccessControlDefaultAdminRulesComponent::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, initial_default_admin: ContractAddress) { + self.access_control_dar.initializer(INITIAL_DELAY, initial_default_admin); + } +} + #[starknet::contract] #[with_components(Ownable)] pub mod DualCaseOwnableMock { diff --git a/sncast_scripts/Scarb.lock b/sncast_scripts/Scarb.lock index 946508b6b..b2c564e72 100644 --- a/sncast_scripts/Scarb.lock +++ b/sncast_scripts/Scarb.lock @@ -43,7 +43,7 @@ dependencies = [ [[package]] name = "openzeppelin_testing" -version = "2.0.0" +version = "4.2.0" dependencies = [ "snforge_std", ] @@ -83,15 +83,15 @@ checksum = "sha256:cfd7c73a6f9984880249babfa8664b69c5f7209c737b1081156a284061ccd [[package]] name = "snforge_scarb_plugin" -version = "0.34.0" +version = "0.44.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:56f2b06ff2f0d8bbdfb7fb6c211fba7e4da6e5334ea70ba849af329a739faf11" +checksum = "sha256:ec8c7637b33392a53153c1e5b87a4617ddcb1981951b233ea043cad5136697e2" [[package]] name = "snforge_std" -version = "0.34.0" +version = "0.44.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:bd20964bde07e6fd0f7adb50d41216f05d66abd422ed82241030369333385876" +checksum = "sha256:d4affedfb90715b1ac417b915c0a63377ae6dd69432040e5d933130d65114702" dependencies = [ "snforge_scarb_plugin", ]