Skip to content

Commit 27514ba

Browse files
feat: Ownable2Step (#352)
Resolves #290 - Introduce a new Ownable2Step contract - Make `onlyOwner()` internal in Ownable and implement `MethodError` to be able to use the error in our new contract(we've already seen we will need this for others) - Propose we use nextest instead of vainilla `cargo test` in the CI. It is faster(not as relevant right now) and the output is so much nicer #### PR Checklist - [x] Unit Tests - [x] e2e Tests - [x] Documentation --------- Co-authored-by: Daniel Bigos <[email protected]>
1 parent 0815bdd commit 27514ba

File tree

16 files changed

+926
-48
lines changed

16 files changed

+926
-48
lines changed

.github/pull_request_template.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ Some of the items may not apply.
1919

2020
- [ ] Tests
2121
- [ ] Documentation
22+
- [ ] Changelog

.github/workflows/test.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ permissions:
1111
contents: read
1212
on:
1313
push:
14-
branches: [ main, release/* ]
14+
branches: [main, release/*]
1515
paths-ignore:
1616
- "**.md"
1717
- "**.adoc"
@@ -32,7 +32,7 @@ jobs:
3232
matrix:
3333
# Run on stable and beta to ensure that tests won't break on the next
3434
# version of the rust toolchain.
35-
toolchain: [ stable, beta ]
35+
toolchain: [stable, beta]
3636
steps:
3737
- uses: actions/checkout@v4
3838
with:
@@ -44,6 +44,11 @@ jobs:
4444
toolchain: ${{ matrix.toolchain }}
4545
rustflags: ""
4646

47+
- name: "Install nextest"
48+
uses: taiki-e/install-action@v2
49+
with:
50+
tool: cargo-nextest
51+
4752
- name: Cargo generate-lockfile
4853
# Enable this ci template to run regardless of whether the lockfile is
4954
# checked in or not.
@@ -52,7 +57,7 @@ jobs:
5257

5358
# https://twitter.com/jonhoo/status/1571290371124260865
5459
- name: Run unit tests
55-
run: cargo test --locked --features std --all-targets
60+
run: cargo nextest run --locked --features std --all-targets
5661

5762
# https://github.com/rust-lang/cargo/issues/6669
5863
- name: Run doc tests
@@ -64,7 +69,7 @@ jobs:
6469
strategy:
6570
fail-fast: false
6671
matrix:
67-
os: [ macos-latest ]
72+
os: [macos-latest]
6873
# Windows fails because of `stylus-proc`.
6974
# os: [macos-latest, windows-latest]
7075
steps:
@@ -78,12 +83,17 @@ jobs:
7883
toolchain: stable
7984
rustflags: ""
8085

86+
- name: "Install nextest"
87+
uses: taiki-e/install-action@v2
88+
with:
89+
tool: cargo-nextest
90+
8191
- name: Cargo generate-lockfile
8292
if: hashFiles('Cargo.lock') == ''
8393
run: cargo generate-lockfile
8494

8595
- name: Run unit tests
86-
run: cargo test --locked --features std --all-targets
96+
run: cargo nextest run --locked --features std --all-targets
8797
coverage:
8898
# Use llvm-cov to build and collect coverage and outputs in a format that
8999
# is compatible with codecov.io.

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- ERC-1155 Multi Token Standard. #275
1313
- `SafeErc20` Utility. #289
1414
- Finite Fields arithmetics. #376
15+
- `Ownable2Step` contract. #352
16+
- `IOwnable` trait. #352
17+
18+
### Changed(breaking)
19+
20+
- Removed `only_owner` from the public interface of `Ownable`. #352
1521

1622
### Changed
1723

Cargo.lock

Lines changed: 15 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ members = [
1919
"examples/basic/token",
2020
"examples/basic/script",
2121
"examples/ecdsa",
22+
"examples/ownable-two-step",
2223
"examples/safe-erc20",
2324
"benches",
2425
]
@@ -38,6 +39,7 @@ default-members = [
3839
"examples/safe-erc20",
3940
"examples/merkle-proofs",
4041
"examples/ownable",
42+
"examples/ownable-two-step",
4143
"examples/access-control",
4244
"examples/basic/token",
4345
"examples/ecdsa",

contracts/src/access/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
//! Contracts implementing access control mechanisms.
22
pub mod control;
33
pub mod ownable;
4+
pub mod ownable_two_step;

contracts/src/access/ownable.rs

Lines changed: 91 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@
1010
//! to the owner.
1111
use alloy_primitives::Address;
1212
use alloy_sol_types::sol;
13+
use openzeppelin_stylus_proc::interface_id;
1314
use stylus_sdk::{
15+
call::MethodError,
1416
evm, msg,
1517
stylus_proc::{public, sol_storage, SolidityError},
1618
};
1719

1820
sol! {
1921
/// Emitted when ownership gets transferred between accounts.
22+
///
23+
/// * `previous_owner` - Address of the previous owner.
24+
/// * `new_owner` - Address of the new owner.
2025
#[allow(missing_docs)]
2126
event OwnershipTransferred(address indexed previous_owner, address indexed new_owner);
2227
}
@@ -45,6 +50,12 @@ pub enum Error {
4550
InvalidOwner(OwnableInvalidOwner),
4651
}
4752

53+
impl MethodError for Error {
54+
fn encode(self) -> alloc::vec::Vec<u8> {
55+
self.into()
56+
}
57+
}
58+
4859
sol_storage! {
4960
/// State of an `Ownable` contract.
5061
pub struct Ownable {
@@ -53,32 +64,21 @@ sol_storage! {
5364
}
5465
}
5566

56-
#[public]
57-
impl Ownable {
58-
/// Returns the address of the current owner.
59-
pub fn owner(&self) -> Address {
60-
self._owner.get()
61-
}
67+
/// Interface for an [`Ownable`] contract.
68+
#[interface_id]
69+
pub trait IOwnable {
70+
/// The error type associated to the trait implementation.
71+
type Error: Into<alloc::vec::Vec<u8>>;
6272

63-
/// Checks if the [`msg::sender`] is set as the owner.
73+
/// Returns the address of the current owner.
6474
///
65-
/// # Errors
75+
/// # Arguments
6676
///
67-
/// If called by any account other than the owner, then the error
68-
/// [`Error::UnauthorizedAccount`] is returned.
69-
pub fn only_owner(&self) -> Result<(), Error> {
70-
let account = msg::sender();
71-
if self.owner() != account {
72-
return Err(Error::UnauthorizedAccount(
73-
OwnableUnauthorizedAccount { account },
74-
));
75-
}
77+
/// * `&self` - Read access to the contract's state.
78+
fn owner(&self) -> Address;
7679

77-
Ok(())
78-
}
79-
80-
/// Transfers ownership of the contract to a new account (`new_owner`). Can
81-
/// only be called by the current owner.
80+
/// Transfers ownership of the contract to a new account (`new_owner`).
81+
/// Can only be called by the current owner.
8282
///
8383
/// # Arguments
8484
///
@@ -89,13 +89,52 @@ impl Ownable {
8989
///
9090
/// If `new_owner` is the zero address, then the error
9191
/// [`OwnableInvalidOwner`] is returned.
92-
pub fn transfer_ownership(
92+
///
93+
/// # Events
94+
///
95+
/// Emits a [`OwnershipTransferred`] event.
96+
fn transfer_ownership(
97+
&mut self,
98+
new_owner: Address,
99+
) -> Result<(), Self::Error>;
100+
101+
/// Leaves the contract without owner. It will not be possible to call
102+
/// functions that require `only_owner`. Can only be called by the current
103+
/// owner.
104+
///
105+
/// NOTE: Renouncing ownership will leave the contract without an owner,
106+
/// thereby disabling any functionality that is only available to the owner.
107+
///
108+
/// # Arguments
109+
///
110+
/// * `&mut self` - Write access to the contract's state.
111+
///
112+
/// # Errors
113+
///
114+
/// If not called by the owner, then the error
115+
/// [`Error::UnauthorizedAccount`] is returned.
116+
///
117+
/// # Events
118+
///
119+
/// Emits a [`OwnershipTransferred`] event.
120+
fn renounce_ownership(&mut self) -> Result<(), Self::Error>;
121+
}
122+
123+
#[public]
124+
impl IOwnable for Ownable {
125+
type Error = Error;
126+
127+
fn owner(&self) -> Address {
128+
self._owner.get()
129+
}
130+
131+
fn transfer_ownership(
93132
&mut self,
94133
new_owner: Address,
95-
) -> Result<(), Error> {
134+
) -> Result<(), Self::Error> {
96135
self.only_owner()?;
97136

98-
if new_owner == Address::ZERO {
137+
if new_owner.is_zero() {
99138
return Err(Error::InvalidOwner(OwnableInvalidOwner {
100139
owner: Address::ZERO,
101140
}));
@@ -106,31 +145,46 @@ impl Ownable {
106145
Ok(())
107146
}
108147

109-
/// Leaves the contract without owner. It will not be possible to call
110-
/// [`Self::only_owner`] functions. Can only be called by the current owner.
148+
fn renounce_ownership(&mut self) -> Result<(), Self::Error> {
149+
self.only_owner()?;
150+
self._transfer_ownership(Address::ZERO);
151+
Ok(())
152+
}
153+
}
154+
155+
impl Ownable {
156+
/// Checks if the [`msg::sender`] is set as the owner.
111157
///
112-
/// NOTE: Renouncing ownership will leave the contract without an owner,
113-
/// thereby disabling any functionality that is only available to the owner.
158+
/// # Arguments
159+
///
160+
/// * `&self` - Read access to the contract's state.
114161
///
115162
/// # Errors
116163
///
117-
/// If not called by the owner, then the error
164+
/// If called by any account other than the owner, then the error
118165
/// [`Error::UnauthorizedAccount`] is returned.
119-
pub fn renounce_ownership(&mut self) -> Result<(), Error> {
120-
self.only_owner()?;
121-
self._transfer_ownership(Address::ZERO);
166+
pub fn only_owner(&self) -> Result<(), Error> {
167+
let account = msg::sender();
168+
if self.owner() != account {
169+
return Err(Error::UnauthorizedAccount(
170+
OwnableUnauthorizedAccount { account },
171+
));
172+
}
173+
122174
Ok(())
123175
}
124-
}
125176

126-
impl Ownable {
127177
/// Transfers ownership of the contract to a new account (`new_owner`).
128178
/// Internal function without access restriction.
129179
///
130180
/// # Arguments
131181
///
132182
/// * `&mut self` - Write access to the contract's state.
133-
/// * `new_owner` - Account that's gonna be the next owner.
183+
/// * `new_owner` - Account that is going to be the next owner.
184+
///
185+
/// # Events
186+
///
187+
/// Emits a [`OwnershipTransferred`] event.
134188
pub fn _transfer_ownership(&mut self, new_owner: Address) {
135189
let previous_owner = self._owner.get();
136190
self._owner.set(new_owner);
@@ -143,7 +197,7 @@ mod tests {
143197
use alloy_primitives::{address, Address};
144198
use stylus_sdk::msg;
145199

146-
use super::{Error, Ownable};
200+
use super::{Error, IOwnable, Ownable};
147201

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

0 commit comments

Comments
 (0)