Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add optional delay to AccessControl role grants #1317

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/access/src/accesscontrol.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pub mod accesscontrol;
pub mod account_role_info;
pub mod interface;

pub use accesscontrol::AccessControlComponent;

pub const DEFAULT_ADMIN_ROLE: felt252 = 0;
pub use accesscontrol::AccessControlComponent::DEFAULT_ADMIN_ROLE;
224 changes: 203 additions & 21 deletions packages/access/src/accesscontrol/accesscontrol.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,55 @@
/// Extra precautions should be taken to secure accounts with this role.
#[starknet::component]
pub mod AccessControlComponent {
use crate::accesscontrol::account_role_info::AccountRoleInfo;
use crate::accesscontrol::interface;
use crate::accesscontrol::interface::RoleStatus;
use openzeppelin_introspection::src5::SRC5Component;
use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl;
use openzeppelin_introspection::src5::SRC5Component::SRC5Impl;
use starknet::ContractAddress;
use starknet::get_caller_address;
use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess};

pub const DEFAULT_ADMIN_ROLE: felt252 = 0;

#[storage]
pub struct Storage {
pub AccessControl_role_admin: Map<felt252, felt252>,
pub AccessControl_role_member: Map<(felt252, ContractAddress), bool>,
pub AccessControl_role_member: Map<(felt252, ContractAddress), AccountRoleInfo>,
}

#[event]
#[derive(Drop, PartialEq, starknet::Event)]
pub enum Event {
RoleGranted: RoleGranted,
RoleGrantedWithDelay: RoleGrantedWithDelay,
RoleRevoked: RoleRevoked,
RoleAdminChanged: RoleAdminChanged,
}

/// 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.
/// or the deployer address if `_grant_role` is called from the constructor.
#[derive(Drop, PartialEq, starknet::Event)]
pub struct RoleGranted {
pub role: felt252,
pub account: ContractAddress,
pub sender: ContractAddress,
}

/// 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.
#[derive(Drop, PartialEq, starknet::Event)]
pub struct RoleGrantedWithDelay {
pub role: felt252,
pub account: ContractAddress,
pub sender: ContractAddress,
pub delay: u64,
}

/// Emitted when `role` is revoked for `account`.
///
/// `sender` is the account that originated the contract call:
Expand All @@ -77,6 +93,8 @@ pub mod AccessControlComponent {
pub mod 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';
}

#[embeddable_as(AccessControlImpl)]
Expand All @@ -86,11 +104,11 @@ pub mod AccessControlComponent {
+SRC5Component::HasComponent<TContractState>,
+Drop<TContractState>,
> of interface::IAccessControl<ComponentState<TContractState>> {
/// Returns whether `account` has been granted `role`.
/// Returns whether `account` can act as `role`.
fn has_role(
self: @ComponentState<TContractState>, role: felt252, account: ContractAddress,
) -> bool {
self.AccessControl_role_member.read((role, account))
self.is_role_effective(role, account)
}

/// Returns the admin role that controls `role`.
Expand Down Expand Up @@ -143,7 +161,7 @@ pub mod AccessControlComponent {
fn renounce_role(
ref self: ComponentState<TContractState>, role: felt252, account: ContractAddress,
) {
let caller = get_caller_address();
let caller = starknet::get_caller_address();
assert(caller == account, Errors::INVALID_CALLER);
self._revoke_role(role, account);
}
Expand Down Expand Up @@ -186,6 +204,48 @@ pub mod AccessControlComponent {
}
}

#[embeddable_as(AccessControlWithDelayImpl)]
impl AccessControlWithDelay<
TContractState,
+HasComponent<TContractState>,
+SRC5Component::HasComponent<TContractState>,
+Drop<TContractState>,
> of interface::IAccessControlWithDelay<ComponentState<TContractState>> {
/// 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<TContractState>, role: felt252, account: ContractAddress,
) -> RoleStatus {
self.resolve_role_status(role, account)
}

/// Attempts to grant `role` to `account` with the specified activation delay.
///
/// May emit a `RoleGrantedWithDelay` event.
///
/// Requirements:
///
/// - The caller must have `role`'s admin role.
/// - delay must be greater than 0.
/// - the `role` must not be already effective for `account`.
fn grant_role_with_delay(
ref self: ComponentState<TContractState>,
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,
Expand All @@ -199,13 +259,72 @@ pub mod AccessControlComponent {
src5_component.register_interface(interface::IACCESSCONTROL_ID);
}

/// Validates that the caller has the given role. Otherwise it panics.
/// Validates that the caller can act as the given role. Otherwise it panics.
fn assert_only_role(self: @ComponentState<TContractState>, role: felt252) {
let caller: ContractAddress = get_caller_address();
let authorized = AccessControl::has_role(self, role, caller);
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<TContractState>, 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<TContractState>, 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<TContractState>, 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.
Expand All @@ -214,24 +333,66 @@ pub mod AccessControlComponent {
fn set_role_admin(
ref self: ComponentState<TContractState>, role: felt252, admin_role: felt252,
) {
let previous_admin_role: felt252 = AccessControl::get_role_admin(@self, role);
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`.
/// 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.
///
/// May emit a `RoleGranted` event.
fn _grant_role(
ref self: ComponentState<TContractState>, role: felt252, account: ContractAddress,
) {
if !AccessControl::has_role(@self, role, account) {
let caller: ContractAddress = get_caller_address();
self.AccessControl_role_member.write((role, account), true);
self.emit(RoleGranted { role, account, sender: caller });
}
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.
///
/// May emit a `RoleGrantedWithDelay` event.
///
/// Requirements:
///
/// - delay must be greater than 0.
/// - the `role` must not be already effective for `account`.
fn _grant_role_with_delay(
ref self: ComponentState<TContractState>,
role: felt252,
account: ContractAddress,
delay: u64,
) {
assert(delay > 0, Errors::INVALID_DELAY);
match self.resolve_role_status(role, account) {
RoleStatus::Effective => core::panic_with_felt252(Errors::ALREADY_EFFECTIVE),
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`.
Expand All @@ -242,11 +403,16 @@ pub mod AccessControlComponent {
fn _revoke_role(
ref self: ComponentState<TContractState>, role: felt252, account: ContractAddress,
) {
if AccessControl::has_role(@self, role, account) {
let caller: ContractAddress = get_caller_address();
self.AccessControl_role_member.write((role, account), false);
self.emit(RoleRevoked { role, account, sender: caller });
}
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 });
},
};
}
}

Expand Down Expand Up @@ -315,6 +481,22 @@ pub mod AccessControlComponent {
AccessControlCamel::renounceRole(ref self, role, account);
}

// IAccessControlWithDelay
fn get_role_status(
self: @ComponentState<TContractState>, role: felt252, account: ContractAddress,
) -> RoleStatus {
AccessControlWithDelay::get_role_status(self, role, account)
}

fn grant_role_with_delay(
ref self: ComponentState<TContractState>,
role: felt252,
account: ContractAddress,
delay: u64,
) {
AccessControlWithDelay::grant_role_with_delay(ref self, role, account, delay);
}

// ISRC5
fn supports_interface(
self: @ComponentState<TContractState>, interface_id: felt252,
Expand Down
Loading