Skip to main content

miden_standards/account/access/
ownable2step.rs

1use miden_protocol::account::component::{
2    AccountComponentCode,
3    AccountComponentMetadata,
4    FeltSchema,
5    StorageSchema,
6    StorageSlotSchema,
7};
8use miden_protocol::account::{
9    AccountComponent,
10    AccountComponentName,
11    AccountId,
12    AccountStorage,
13    StorageSlot,
14    StorageSlotName,
15};
16use miden_protocol::errors::AccountIdError;
17use miden_protocol::utils::sync::LazyLock;
18use miden_protocol::{Felt, Word};
19
20use super::account_id_from_felt_pair;
21use crate::account::account_component_code;
22
23account_component_code!(OWNABLE2STEP_CODE, "miden-standards-access-ownable2step.masp");
24
25static OWNER_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
26    StorageSlotName::new("miden::standards::access::ownable2step::owner_config")
27        .expect("storage slot name should be valid")
28});
29
30/// Two-step ownership management for account components.
31///
32/// This struct holds the current owner and any nominated (pending) owner. A nominated owner
33/// must explicitly accept the transfer before it takes effect, preventing accidental transfers
34/// to incorrect addresses.
35///
36/// ## Security considerations
37///
38/// Access control is based on the note sender (the account ID that created the note), which
39/// authenticates *which account* created a note but not the *code* that executed when it was
40/// created. It is meaningful only when every account registered as owner enforces strong
41/// authentication. Registering a permissionless account (for example one using `no_auth`) as
42/// owner provides no access restriction: anyone can make such an account emit a note with an
43/// arbitrary script root and that account's ID as sender, defeating the owner check.
44///
45/// ## Storage Layout
46///
47/// The ownership data is stored in a single word:
48///
49/// ```text
50/// Word:  [owner_suffix, owner_prefix, nominated_owner_suffix, nominated_owner_prefix]
51///         word[0]       word[1]        word[2]                  word[3]
52/// ```
53pub struct Ownable2Step {
54    /// The current owner of the component. `None` when ownership has been renounced.
55    owner: Option<AccountId>,
56    nominated_owner: Option<AccountId>,
57}
58
59impl Ownable2Step {
60    /// The name of the component.
61    pub const NAME: &'static str = "miden::standards::access::ownable2step";
62
63    /// Returns the canonical [`AccountComponentName`] of this component.
64    pub const fn name() -> AccountComponentName {
65        AccountComponentName::from_static_str(Self::NAME)
66    }
67
68    /// Returns the [`AccountComponentCode`] of this component.
69    pub fn code() -> &'static AccountComponentCode {
70        &OWNABLE2STEP_CODE
71    }
72
73    // CONSTRUCTORS
74    // --------------------------------------------------------------------------------------------
75
76    /// Creates a new [`Ownable2Step`] with the given owner and no nominated owner.
77    pub fn new(owner: AccountId) -> Self {
78        Self {
79            owner: Some(owner),
80            nominated_owner: None,
81        }
82    }
83
84    /// Reads ownership data from account storage, validating any non-zero account IDs.
85    ///
86    /// Returns an error if either owner or nominated owner contains an invalid (but non-zero)
87    /// account ID.
88    pub fn try_from_storage(storage: &AccountStorage) -> Result<Self, Ownable2StepError> {
89        let word: Word = storage
90            .get_item(Self::slot_name())
91            .map_err(Ownable2StepError::StorageLookupFailed)?;
92
93        Self::try_from_word(word)
94    }
95
96    /// Reconstructs an [`Ownable2Step`] from a raw storage word.
97    ///
98    /// Format: `[owner_suffix, owner_prefix, nominated_suffix, nominated_prefix]`
99    pub fn try_from_word(word: Word) -> Result<Self, Ownable2StepError> {
100        let owner = account_id_from_felt_pair(word[0], word[1])
101            .map_err(Ownable2StepError::InvalidOwnerId)?;
102
103        let nominated_owner = account_id_from_felt_pair(word[2], word[3])
104            .map_err(Ownable2StepError::InvalidNominatedOwnerId)?;
105
106        Ok(Self { owner, nominated_owner })
107    }
108
109    // PUBLIC ACCESSORS
110    // --------------------------------------------------------------------------------------------
111
112    /// Returns the [`StorageSlotName`] where ownership data is stored.
113    pub fn slot_name() -> &'static StorageSlotName {
114        &OWNER_CONFIG_SLOT_NAME
115    }
116
117    /// Returns the storage slot schema for the ownership configuration slot.
118    pub fn slot_schema() -> (StorageSlotName, StorageSlotSchema) {
119        (
120            Self::slot_name().clone(),
121            StorageSlotSchema::value(
122                "Ownership data (owner and nominated owner)",
123                [
124                    FeltSchema::felt("owner_suffix"),
125                    FeltSchema::felt("owner_prefix"),
126                    FeltSchema::felt("nominated_suffix"),
127                    FeltSchema::felt("nominated_prefix"),
128                ],
129            ),
130        )
131    }
132
133    /// Returns the current owner, or `None` if ownership has been renounced.
134    pub fn owner(&self) -> Option<AccountId> {
135        self.owner
136    }
137
138    /// Returns the nominated owner, or `None` if no transfer is in progress.
139    pub fn nominated_owner(&self) -> Option<AccountId> {
140        self.nominated_owner
141    }
142
143    /// Converts this ownership data into a [`StorageSlot`].
144    pub fn to_storage_slot(&self) -> StorageSlot {
145        StorageSlot::with_value(Self::slot_name().clone(), self.to_word())
146    }
147
148    /// Converts this ownership data into a raw [`Word`].
149    pub fn to_word(&self) -> Word {
150        let (owner_suffix, owner_prefix) = match self.owner {
151            Some(id) => (id.suffix(), id.prefix().as_felt()),
152            None => (Felt::ZERO, Felt::ZERO),
153        };
154        let (nominated_suffix, nominated_prefix) = match self.nominated_owner {
155            Some(id) => (id.suffix(), id.prefix().as_felt()),
156            None => (Felt::ZERO, Felt::ZERO),
157        };
158        [owner_suffix, owner_prefix, nominated_suffix, nominated_prefix].into()
159    }
160
161    /// Returns the [`AccountComponentMetadata`] for this component.
162    pub fn component_metadata() -> AccountComponentMetadata {
163        let storage_schema =
164            StorageSchema::new([Self::slot_schema()]).expect("storage schema should be valid");
165
166        AccountComponentMetadata::new(Self::NAME)
167            .with_description("Two-step ownership management component")
168            .with_storage_schema(storage_schema)
169    }
170}
171
172impl From<Ownable2Step> for AccountComponent {
173    fn from(ownership: Ownable2Step) -> Self {
174        let storage_slot = ownership.to_storage_slot();
175        let metadata = Ownable2Step::component_metadata();
176
177        AccountComponent::new(Ownable2Step::code().clone(), vec![storage_slot], metadata).expect(
178            "Ownable2Step component should satisfy the requirements of a valid account component",
179        )
180    }
181}
182
183// OWNABLE2STEP ERROR
184// ================================================================================================
185
186/// Errors that can occur when reading [`Ownable2Step`] data from storage.
187#[derive(Debug, thiserror::Error)]
188pub enum Ownable2StepError {
189    #[error("failed to read ownership slot from storage")]
190    StorageLookupFailed(#[source] miden_protocol::errors::AccountError),
191    #[error("invalid owner account ID in storage")]
192    InvalidOwnerId(#[source] AccountIdError),
193    #[error("invalid nominated owner account ID in storage")]
194    InvalidNominatedOwnerId(#[source] AccountIdError),
195}