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

Validate new public key in Account #989

Merged
Merged
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
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed (Breaking)

- ERC721Component internal implementation to support transfer, mint, and burn flows going through an `_update` function (#978)
- ERC721Component implementations now require an ERC721HooksTrait implementation in scope.
- ERC1155Component implementations now require an ERC1155HooksTrait implementation in scope.
- ERC721Component implementations now require an ERC721HooksTrait implementation in scope (#978)
- ERC1155Component implementations now require an ERC1155HooksTrait implementation in scope (#982)
- AccountComponent, preset, and dispatcher now require a `signature` param in the public-key-setter functions (#989)

## 0.12.0 (2024-04-21)

Expand Down
41 changes: 37 additions & 4 deletions docs/modules/ROOT/pages/api/account.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ NOTE: {src5-component-required-note}
.PublicKeyImpl

* xref:#AccountComponent-get_public_key[`++get_public_key(self)++`]
* xref:#AccountComponent-set_public_key[`++set_public_key(self, new_public_key)++`]
* xref:#AccountComponent-set_public_key[`++set_public_key(self, new_public_key, signature)++`]

[.sub-index#AccountComponent-Embeddable-Impls-SRC6CamelOnlyImpl]
.SRC6CamelOnlyImpl
Expand All @@ -128,7 +128,7 @@ NOTE: {src5-component-required-note}
.PublicKeyCamelImpl

* xref:#AccountComponent-getPublicKey[`++getPublicKey(self)++`]
* xref:#AccountComponent-setPublicKey[`++setPublicKey(self, newPublicKey)++`]
* xref:#AccountComponent-setPublicKey[`++setPublicKey(self, newPublicKey, signature)++`]

.SRC5Impl
* xref:api/introspection.adoc#ISRC5-supports_interface[`supports_interface(self, interface_id: felt252)`]
Expand All @@ -141,6 +141,7 @@ NOTE: {src5-component-required-note}

* xref:#AccountComponent-initializer[`++initializer(self, public_key)++`]
* xref:#AccountComponent-assert_only_self[`++assert_only_self(self)++`]
* xref:#AccountComponent-assert_valid_new_owner[`++assert_valid_new_owner(self, current_owner, new_owner, signature)++`]
* xref:#AccountComponent-validate_transaction[`++validate_transaction(self)++`]
* xref:#AccountComponent-_set_public_key[`++_set_public_key(self, new_public_key)++`]
* xref:#AccountComponent-_is_valid_signature[`++_is_valid_signature(self, hash, signature)++`]
Expand Down Expand Up @@ -199,12 +200,31 @@ Returns the current public key of the account.

[.contract-item]
[[AccountComponent-set_public_key]]
==== `[.contract-item-name]#++set_public_key++#++(ref self: ContractState, new_public_key: felt252)++` [.item-kind]#external#
==== `[.contract-item-name]#++set_public_key++#++(ref self: ContractState, new_public_key: felt252, signature: Span<felt252>)++` [.item-kind]#external#

Sets a new public key for the account. Only accessible by the account calling itself through `\\__execute__`.

Requirements:

- The caller must be the contract itself.
- The signature must be valid for the new owner.

Emits both an {OwnerRemoved} and an {OwnerAdded} event.

[NOTE]
====
The message to be signed is computed in Cairo as follows:

```javascript
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm good call with using js. The Cairo highlighting looks a little bland here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, we should at some point improve the highlights extension.

let message_hash = PoseidonTrait::new()
.update_with('StarkNet Message')
.update_with('accept_ownership')
.update_with(get_contract_address())
.update_with(current_owner)
.finalize();
ericnordelo marked this conversation as resolved.
Show resolved Hide resolved
```
====

[.contract-item]
[[AccountComponent-isValidSignature]]
==== `[.contract-item-name]#++isValidSignature++#++(self: @ContractState, hash: felt252, signature: Array<felt252>) → felt252++` [.item-kind]#external#
Expand All @@ -219,7 +239,7 @@ See xref:AccountComponent-get_public_key[get_public_key].

[.contract-item]
[[AccountComponent-setPublicKey]]
==== `[.contract-item-name]#++setPublicKey++#++(ref self: ContractState, newPublicKey: felt252)++` [.item-kind]#external#
==== `[.contract-item-name]#++setPublicKey++#++(ref self: ContractState, newPublicKey: felt252, signature: Span<felt252>)++` [.item-kind]#external#

See xref:AccountComponent-set_public_key[set_public_key].

Expand All @@ -240,6 +260,19 @@ Emits an {OwnerAdded} event.

Validates that the caller is the account itself. Otherwise it reverts.

[.contract-item]
[[AccountComponent-assert_valid_new_owner]]
==== `[.contract-item-name]#++assert_valid_new_owner++#++(self: @ComponentState, current_owner: felt252, new_owner: felt252, signature: Span<felt252>)++` [.item-kind]#internal#

Validates that `new_owner` accepted the ownership of the contract through a signature.

Requirements:

- `signature` must be valid for the new owner.

WARNING: This function assumes that `current_owner` is the current owner of the contract, and
does not validate this assumption.

[.contract-item]
[[AccountComponent-validate_transaction]]
==== `[.contract-item-name]#++validate_transaction++#++(self: @ComponentState)++ → felt252` [.item-kind]#internal#
Expand Down
66 changes: 57 additions & 9 deletions src/account/account.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
/// The Account component enables contracts to behave as accounts.
#[starknet::component]
mod AccountComponent {
use core::hash::{HashStateExTrait, HashStateTrait};
use openzeppelin::account::interface;
use openzeppelin::account::utils::{MIN_TRANSACTION_VERSION, QUERY_VERSION, QUERY_OFFSET};
use openzeppelin::account::utils::{execute_calls, is_valid_stark_signature};
use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait;
use openzeppelin::introspection::src5::SRC5Component::SRC5;
use openzeppelin::introspection::src5::SRC5Component;
use poseidon::PoseidonTrait;
use starknet::account::Call;
use starknet::get_caller_address;
use starknet::get_contract_address;
Expand Down Expand Up @@ -155,11 +157,20 @@ mod AccountComponent {
/// Requirements:
///
/// - The caller must be the contract itself.
/// - The signature must be valid for the new owner.
///
/// Emits an `OwnerRemoved` event.
fn set_public_key(ref self: ComponentState<TContractState>, new_public_key: felt252) {
/// Emits both an `OwnerRemoved` and an `OwnerAdded` event.
fn set_public_key(
ref self: ComponentState<TContractState>,
new_public_key: felt252,
signature: Span<felt252>
) {
self.assert_only_self();
self.emit(OwnerRemoved { removed_owner_guid: self.Account_public_key.read() });

let current_owner = self.Account_public_key.read();
self.assert_valid_new_owner(current_owner, new_public_key, signature);

self.emit(OwnerRemoved { removed_owner_guid: current_owner });
self._set_public_key(new_public_key);
}
}
Expand Down Expand Up @@ -191,8 +202,12 @@ mod AccountComponent {
self.Account_public_key.read()
}

fn setPublicKey(ref self: ComponentState<TContractState>, newPublicKey: felt252) {
PublicKey::set_public_key(ref self, newPublicKey);
fn setPublicKey(
ref self: ComponentState<TContractState>,
newPublicKey: felt252,
signature: Span<felt252>
) {
PublicKey::set_public_key(ref self, newPublicKey, signature);
}
}

Expand All @@ -218,6 +233,31 @@ mod AccountComponent {
assert(self == caller, Errors::UNAUTHORIZED);
}

/// Validates that `new_owner` accepted the ownership of the contract.
///
/// WARNING: This function assumes that `current_owner` is the current owner of the contract, and
/// does not validate this assumption.
///
/// Requirements:
///
/// - The signature must be valid for the new owner.
fn assert_valid_new_owner(
self: @ComponentState<TContractState>,
current_owner: felt252,
new_owner: felt252,
signature: Span<felt252>
) {
let message_hash = PoseidonTrait::new()
.update_with('StarkNet Message')
.update_with('accept_ownership')
.update_with(get_contract_address())
.update_with(current_owner)
Comment on lines +250 to +254
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include the domain separator in compliance with SNIP-12?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I assume you don't think it's worth it to include the account and the new owner in the message as Marto initially proposed?

#[derive(Copy, Drop, Serde)]
struct AddOwner {
    account: ContractAddress,
    owner: felt252
}

Even though the account is already present in the hash as required by SNIP-12 and that owner is already known by the owner, I decided to make the struct overly explicit so users know what they're signing, even if it adds a bit of cost/redundancy/complexity.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this particular case, I don't think SNIP12 is suited, because we don't really need to sign a struct with nonces and so on, we just need to verify that the signer exists, and for that something like EIP 191 (version 0x45) should suffice. If we were to be SNIP 12 compliant, we would need to add a domain separator, with SNIP12Metadata, and that seems like an overhead for the use case.

The proposed format is to avoid clashes with transactions by prefixing "StarkNet Message", and accept_ownership to scope the message, and then we have the account address and the previous owner, so the user is signing who will be receiving the ownership from. To me adding the new owner to the signature is redundant since the signature already enforces the new pub key.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice that, the use case is to avoid transferring the ownership to an invalid public key.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something like EIP 191 (version 0x45) should suffice. If we were to be SNIP 12 compliant, we would need to add a domain separator, with SNIP12Metadata, and that seems like an overhead for the use case.

To me adding the new owner to the signature is redundant since the signature already enforces the new pub key.

Good points!

.finalize();

let is_valid = is_valid_stark_signature(message_hash, new_owner, signature);
assert(is_valid, Errors::INVALID_SIGNATURE);
}

/// Validates the signature for the current transaction.
/// Returns the short string `VALID` if valid, otherwise it reverts.
fn validate_transaction(self: @ComponentState<TContractState>) -> felt252 {
Expand Down Expand Up @@ -300,17 +340,25 @@ mod AccountComponent {
PublicKey::get_public_key(self)
}

fn set_public_key(ref self: ComponentState<TContractState>, new_public_key: felt252) {
PublicKey::set_public_key(ref self, new_public_key);
fn set_public_key(
ref self: ComponentState<TContractState>,
new_public_key: felt252,
signature: Span<felt252>
) {
PublicKey::set_public_key(ref self, new_public_key, signature);
}

// IPublicKeyCamel
fn getPublicKey(self: @ComponentState<TContractState>) -> felt252 {
PublicKeyCamel::getPublicKey(self)
}

fn setPublicKey(ref self: ComponentState<TContractState>, newPublicKey: felt252) {
PublicKeyCamel::setPublicKey(ref self, newPublicKey);
fn setPublicKey(
ref self: ComponentState<TContractState>,
newPublicKey: felt252,
signature: Span<felt252>
) {
PublicKeyCamel::setPublicKey(ref self, newPublicKey, signature);
}

// ISRC5
Expand Down
7 changes: 4 additions & 3 deletions src/account/dual_account.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct DualCaseAccount {
}

trait DualCaseAccountABI {
fn set_public_key(self: @DualCaseAccount, new_public_key: felt252);
fn set_public_key(self: @DualCaseAccount, new_public_key: felt252, signature: Span<felt252>);
fn get_public_key(self: @DualCaseAccount) -> felt252;
fn is_valid_signature(
self: @DualCaseAccount, hash: felt252, signature: Array<felt252>
Expand All @@ -24,8 +24,9 @@ trait DualCaseAccountABI {
}

impl DualCaseAccountImpl of DualCaseAccountABI {
fn set_public_key(self: @DualCaseAccount, new_public_key: felt252) {
let args = array![new_public_key];
fn set_public_key(self: @DualCaseAccount, new_public_key: felt252, signature: Span<felt252>) {
let mut args = array![new_public_key];
args.append_serde(signature);

try_selector_with_fallback(
*self.contract_address, selectors::set_public_key, selectors::setPublicKey, args.span()
Expand Down
8 changes: 4 additions & 4 deletions src/account/interface.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ trait IDeployable<TState> {
#[starknet::interface]
trait IPublicKey<TState> {
fn get_public_key(self: @TState) -> felt252;
fn set_public_key(ref self: TState, new_public_key: felt252);
fn set_public_key(ref self: TState, new_public_key: felt252, signature: Span<felt252>);
}

#[starknet::interface]
Expand All @@ -46,7 +46,7 @@ trait ISRC6CamelOnly<TState> {
#[starknet::interface]
trait IPublicKeyCamel<TState> {
fn getPublicKey(self: @TState) -> felt252;
fn setPublicKey(ref self: TState, newPublicKey: felt252);
fn setPublicKey(ref self: TState, newPublicKey: felt252, signature: Span<felt252>);
}

//
Expand All @@ -73,14 +73,14 @@ trait AccountABI<TState> {

// IPublicKey
fn get_public_key(self: @TState) -> felt252;
fn set_public_key(ref self: TState, new_public_key: felt252);
fn set_public_key(ref self: TState, new_public_key: felt252, signature: Span<felt252>);

// ISRC6CamelOnly
fn isValidSignature(self: @TState, hash: felt252, signature: Array<felt252>) -> felt252;

// IPublicKeyCamel
fn getPublicKey(self: @TState) -> felt252;
fn setPublicKey(ref self: TState, newPublicKey: felt252);
fn setPublicKey(ref self: TState, newPublicKey: felt252, signature: Span<felt252>);
}

//
Expand Down
4 changes: 2 additions & 2 deletions src/presets/interfaces/account.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ trait AccountUpgradeableABI<TState> {

// IPublicKey
fn get_public_key(self: @TState) -> felt252;
fn set_public_key(ref self: TState, new_public_key: felt252);
fn set_public_key(ref self: TState, new_public_key: felt252, signature: Span<felt252>);

// ISRC6CamelOnly
fn isValidSignature(self: @TState, hash: felt252, signature: Array<felt252>) -> felt252;

// IPublicKeyCamel
fn getPublicKey(self: @TState) -> felt252;
fn setPublicKey(ref self: TState, newPublicKey: felt252);
fn setPublicKey(ref self: TState, newPublicKey: felt252, signature: Span<felt252>);

// IUpgradeable
fn upgrade(ref self: TState, new_class_hash: ClassHash);
Expand Down
Loading
Loading