Skip to content

Commit

Permalink
Merge remote-tracking branch 'refs/remotes/origin/main' into integrat…
Browse files Browse the repository at this point in the history
…ion-tests

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	examples/erc20/src/lib.rs
#	examples/erc721/Cargo.toml
#	examples/erc721/src/lib.rs
  • Loading branch information
qalisander committed May 16, 2024
2 parents f8e2af7 + 01e88c7 commit 00c4e3c
Show file tree
Hide file tree
Showing 21 changed files with 823 additions and 102 deletions.
21 changes: 12 additions & 9 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
<!-- Thank you for your interest in contributing to OpenZeppelin! -->
<!--
Thank you for your interest in contributing to OpenZeppelin!
<!-- Consider opening an issue for discussion prior to submitting a PR. -->
<!-- New features will be merged faster if they were first discussed and designed with the team. -->
Consider opening an issue for discussion prior to submitting a PR. New features will be merged faster if they were first discussed and designed with the team.
Fixes #??? <!-- Fill in with issue number -->
Describe the changes introduced in this pull request. Include any context necessary for understanding the PR's purpose.
-->

<!-- Describe the changes introduced in this pull request. -->
<!-- Include any context necessary for understanding the PR's purpose. -->
<!-- Fill in with issue number -->
Resolves #???

#### PR Checklist

<!-- Before merging the pull request all of the following must be completed. -->
<!-- Feel free to submit a PR or Draft PR even if some items are pending. -->
<!-- Some of the items may not apply. -->
<!--
Before merging the pull request all of the following must be completed.
Feel free to submit a PR or Draft PR even if some items are pending.
Some of the items may not apply.
-->

- [ ] Tests
- [ ] Documentation
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ members = [
"lib/grip",
"lib/grip-proc",
"examples/erc20",
"examples/erc721",
"examples/merkle-proofs",
"examples/erc721",
"examples/ownable",
"integration",
]
default-members = [
Expand All @@ -15,8 +17,9 @@ default-members = [
"lib/grip",
"lib/grip-proc",
"examples/erc20",
"examples/merkle-proofs",
"examples/erc721",
"examples/merkle-proofs",
"examples/ownable",
]
# Explicitly set the resolver to version 2, which is the default for packages
# with edition >= 2021.
Expand Down
8 changes: 7 additions & 1 deletion contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ alloy-sol-types.workspace = true
stylus-sdk.workspace = true
stylus-proc.workspace = true
mini-alloc.workspace = true
derive_more = "0.99.17"
cfg-if = "1.0"

[dev-dependencies]
Expand All @@ -26,10 +25,17 @@ once_cell = "1.19.0"
[features]
default = []

# ERC-20
erc20 = []
erc20_metadata = ["erc20"]
erc20_burnable = ["erc20"]

# ERC-721
erc721 = []
erc721_metadata = ["erc721"]

access = []
ownable = ["access"]

# Enables using the standard library. This is not included in the default
# features, because this crate is meant to be used in a `no_std` environment.
Expand Down
4 changes: 4 additions & 0 deletions contracts/src/access/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Contracts implementing access control mechanisms.

#[cfg(feature = "ownable")]
pub mod ownable;
299 changes: 299 additions & 0 deletions contracts/src/access/ownable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
//! Contract module which provides a basic access control mechanism, where
//! there is an account (an owner) that can be granted exclusive access to
//! specific functions.
//!
//! The initial owner is set to the address provided by the deployer. This can
//! later be changed with [`Ownable::transfer_ownership`].
//!
//! This module is used through inheritance. It will make available the
//! [`Ownable::only_owner`] function, which can be called to restrict operations
//! to the owner.
use alloy_primitives::Address;
use alloy_sol_types::sol;
use stylus_proc::SolidityError;
use stylus_sdk::{
evm, msg,
stylus_proc::{external, sol_storage},
};

sol! {
/// Emitted when ownership gets transferred between accounts.
#[allow(missing_docs)]
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
}

sol! {
/// The caller account is not authorized to perform an operation.
///
/// * `account` - Account that was found to not be authorized.
#[derive(Debug)]
#[allow(missing_docs)]
error OwnableUnauthorizedAccount(address account);
/// The owner is not a valid owner account. (eg. `Address::ZERO`)
///
/// * `owner` - Account that's not allowed to become the owner.
#[derive(Debug)]
#[allow(missing_docs)]
error OwnableInvalidOwner(address owner);
}

/// An error that occurred in the implementation of an [`Ownable`] contract.
#[derive(SolidityError, Debug)]
pub enum Error {
/// The caller account is not authorized to perform an operation.
UnauthorizedAccount(OwnableUnauthorizedAccount),
/// The owner is not a valid owner account. (eg. `Address::ZERO`)
InvalidOwner(OwnableInvalidOwner),
}

sol_storage! {
/// State of an `Ownable` contract.
pub struct Ownable {
/// The current owner of this contract.
address _owner;
/// Initialization marker. If true this means that the constructor was
/// called.
///
/// This field should be unnecessary once constructors are supported in
/// the SDK.
bool _initialized;
}
}

#[external]
impl Ownable {
/// Initializes an [`Ownable`] instance with the given `initial_owner`.
///
/// # Arguments
///
/// * `&mut self` - Write access to the contract's state.
/// * `initial_owner` - The initial owner of this contract.
///
/// # Errors
///
/// * If `initial_owner` is the zero address, then [`Error::InvalidOwner`]
/// is returned.
///
/// # Panics
///
/// * If the contract is already initialized, then this function panics.
/// This ensures the contract is constructed only once.
pub fn constructor(&mut self, initial_owner: Address) -> Result<(), Error> {
let is_initialized = self._initialized.get();
assert!(!is_initialized, "Ownable has already been initialized");

if initial_owner == Address::ZERO {
return Err(Error::InvalidOwner(OwnableInvalidOwner {
owner: Address::ZERO,
}));
}

self._transfer_ownership(initial_owner);
self._initialized.set(true);

Ok(())
}

/// Returns the address of the current owner.
pub fn owner(&self) -> Address {
self._owner.get()
}

/// Checks if the [`msg::sender`] is set as the owner.
///
/// # Errors
///
/// * If called by any account other than the owner returns
/// [`Error::UnauthorizedAccount`].
pub fn only_owner(&self) -> Result<(), Error> {
let account = msg::sender();
if self.owner() != account {
return Err(Error::UnauthorizedAccount(
OwnableUnauthorizedAccount { account },
));
}

Ok(())
}

/// Transfers ownership of the contract to a new account (`new_owner`). Can
/// only be called by the current owner.
///
/// # Arguments
///
/// * `&mut self` - Write access to the contract's state.
/// * `new_owner` - The next owner of this contract.
///
/// # Errors
///
/// * If `new_owner` is the zero address, then this function returns an
/// [`Error::OwnableInvalidOwner`] error.
pub fn transfer_ownership(
&mut self,
new_owner: Address,
) -> Result<(), Error> {
self.only_owner()?;

if new_owner == Address::ZERO {
return Err(Error::InvalidOwner(OwnableInvalidOwner {
owner: Address::ZERO,
}));
}

self._transfer_ownership(new_owner);

Ok(())
}

/// Leaves the contract without owner. It will not be possible to call
/// [`only_owner`] functions. Can only be called by the current owner.
///
/// NOTE: Renouncing ownership will leave the contract without an owner,
/// thereby disabling any functionality that is only available to the owner.
///
/// # Errors
///
/// * If not called by the owner, then an [`Error::UnauthorizedAccount`] is
/// returned.
pub fn renounce_ownership(&mut self) -> Result<(), Error> {
self.only_owner()?;
self._transfer_ownership(Address::ZERO);
Ok(())
}
}

impl Ownable {
/// Transfers ownership of the contract to a new account (`new_owner`).
/// Internal function without access restriction.
///
/// # Arguments
///
/// * `&mut self` - Write access to the contract's state.
/// * `new_owner` - Account that's gonna be the next owner.
pub fn _transfer_ownership(&mut self, new_owner: Address) {
let old_owner = self._owner.get();
self._owner.set(new_owner);

evm::log(OwnershipTransferred {
previousOwner: old_owner,
newOwner: new_owner,
});
}
}

#[cfg(all(test, feature = "std"))]
mod tests {
use alloy_primitives::{address, Address, U256};
use stylus_sdk::{
msg,
storage::{StorageAddress, StorageBool, StorageType},
};

use super::{Error, Ownable};

impl Default for Ownable {
fn default() -> Self {
let root = U256::ZERO;
Ownable {
_owner: unsafe { StorageAddress::new(root, 0) },
_initialized: unsafe {
StorageBool::new(root + U256::from(32), 0)
},
}
}
}

const ALICE: Address = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");

#[grip::test]
fn rejects_zero_address_initial_owner(contract: Ownable) {
// FIXME: Once constructors are supported this check should fail.
assert_eq!(contract._owner.get(), Address::ZERO);

let err = contract.constructor(Address::ZERO).unwrap_err();
assert!(matches!(err, Error::InvalidOwner(_)));
}

#[grip::test]
#[should_panic = "Ownable has already been initialized"]
fn initializes_owner_once(contract: Ownable) {
let result = contract.constructor(msg::sender());
assert!(result.is_ok());

let owner = contract._owner.get();
assert_eq!(owner, msg::sender());

let _ = contract.constructor(ALICE);
}

#[grip::test]
fn reads_owner(contract: Ownable) {
let result = contract.constructor(msg::sender());
assert!(result.is_ok());

let owner = contract.owner();
assert_eq!(owner, msg::sender());
}

#[grip::test]
fn transfers_ownership(contract: Ownable) {
let result = contract.constructor(msg::sender());
assert!(result.is_ok());

let _ = contract
.transfer_ownership(ALICE)
.expect("should transfer ownership");
let owner = contract._owner.get();
assert_eq!(owner, ALICE);
}

#[grip::test]
fn prevents_non_onwers_from_transferring(contract: Ownable) {
// Alice must be set as owner, because we can't set the msg::sender yet.
let result = contract.constructor(ALICE);
assert!(result.is_ok());

let bob = address!("B0B0cB49ec2e96DF5F5fFB081acaE66A2cBBc2e2");
let err = contract.transfer_ownership(bob).unwrap_err();
assert!(matches!(err, Error::UnauthorizedAccount(_)));
}

#[grip::test]
fn prevents_reaching_stuck_state(contract: Ownable) {
let result = contract.constructor(msg::sender());
assert!(result.is_ok());

let err = contract.transfer_ownership(Address::ZERO).unwrap_err();
assert!(matches!(err, Error::InvalidOwner(_)));
}

#[grip::test]
fn loses_ownership_after_renouncing(contract: Ownable) {
let result = contract.constructor(msg::sender());
assert!(result.is_ok());

let _ = contract.renounce_ownership();
let owner = contract._owner.get();
assert_eq!(owner, Address::ZERO);
}

#[grip::test]
fn prevents_non_owners_from_renouncing(contract: Ownable) {
// Alice must be set as owner, because we can't set the msg::sender yet.
let result = contract.constructor(ALICE);
assert!(result.is_ok());

let err = contract.renounce_ownership().unwrap_err();
assert!(matches!(err, Error::UnauthorizedAccount(_)));
}

#[grip::test]
fn recovers_access_using_internal_transfer(contract: Ownable) {
let result = contract.constructor(msg::sender());
assert!(result.is_ok());

contract._transfer_ownership(ALICE);
let owner = contract._owner.get();
assert_eq!(owner, ALICE);
}
}
Loading

0 comments on commit 00c4e3c

Please sign in to comment.