diff --git a/.snfoundry_cache/.prev_tests_failed b/.snfoundry_cache/.prev_tests_failed new file mode 100644 index 0000000..e69de29 diff --git a/Scarb.lock b/Scarb.lock index 5026cce..2c0acad 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -1,6 +1,137 @@ # Code generated by scarb DO NOT EDIT. version = 1 +[[package]] +name = "openzeppelin" +version = "0.17.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:7e77855aaba0825a2a12cad72d52d85380a9fab732007754b3c5d98908918ce7" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_finance", + "openzeppelin_governance", + "openzeppelin_introspection", + "openzeppelin_merkle_tree", + "openzeppelin_presets", + "openzeppelin_security", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_access" +version = "0.17.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:541bb8fdf1ad17fe0d275b00acb9f0d7f56ea5534741e21535ac3fda2c600281" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_account" +version = "0.17.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:c4e11609fdd1f4c3d3004cd1468711bd2ea664739c9e59a4b270567fe4c23ee3" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_finance" +version = "0.17.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:9adcbec76ee8ed08be8d87c6af6014aa7080d67578816f5ba77f4376b25bc165" +dependencies = [ + "openzeppelin_access", + "openzeppelin_token", +] + +[[package]] +name = "openzeppelin_governance" +version = "0.17.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:b7e0142d88d69a8c367aea8c9dc7f659f27372551efc23f39a0cf71a189c1302" +dependencies = [ + "openzeppelin_access", + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_introspection" +version = "0.17.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:892433a4a1ea0fc9cf7cdb01e06ddc2782182abcc188e4ea5dd480906d006cf8" + +[[package]] +name = "openzeppelin_merkle_tree" +version = "0.17.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:3c338fa07cbaba8034051a42967816800abe535ef7d709a929175616603dccf9" + +[[package]] +name = "openzeppelin_presets" +version = "0.17.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:0a39e0effff133ab7fb003961ee2986438ee09b53608ce0d71aca24459879597" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_finance", + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_upgrades", +] + +[[package]] +name = "openzeppelin_security" +version = "0.17.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:6e2dee39d87f9ddec2ad37e33e80cf0d8b6c6927fd7950f220dbc2baea658d43" + +[[package]] +name = "openzeppelin_token" +version = "0.17.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:77997a7e217b69674c34b402dc0c7b2210540db66a56087572679c31896eaabb" +dependencies = [ + "openzeppelin_account", + "openzeppelin_governance", + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_upgrades" +version = "0.17.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:a0fa5934f2924e1e85ec8f8c5b7dcd95c25295c029d3a745ba87b3191146004d" + +[[package]] +name = "openzeppelin_utils" +version = "0.17.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:36d93e353f42fd6b824abcd8b4b51c3f5d02c893c5f886ae81403b0368aa5fde" + [[package]] name = "rosettacontracts" version = "0.1.0" +dependencies = [ + "openzeppelin", + "snforge_std", +] + +[[package]] +name = "snforge_scarb_plugin" +version = "0.31.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67" + +[[package]] +name = "snforge_std" +version = "0.31.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67" +dependencies = [ + "snforge_scarb_plugin", +] diff --git a/Scarb.toml b/Scarb.toml index 815716e..89e22aa 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -9,5 +9,8 @@ sierra = true [dependencies] -starknet = "2.6.4" +starknet = "2.8.2" +openzeppelin = "0.17.0" +[dev-dependencies] +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.31.0" } \ No newline at end of file diff --git a/src/accounts/base.cairo b/src/accounts/base.cairo index b63e33d..e2064bf 100644 --- a/src/accounts/base.cairo +++ b/src/accounts/base.cairo @@ -2,7 +2,7 @@ pub type EthPublicKey = starknet::secp256k1::Secp256k1Point; #[starknet::interface] pub trait IRosettaAccount { fn __execute__(self: @TState, calls: Array) -> Array>; - fn __validate__(self: @TState, calls: Array) -> felt252; + fn __validate__(self: @TState, calls: Array) -> felt252; fn is_valid_signature(self: @TState, hash: felt252, signature: Array) -> felt252; fn supports_interface(self: @TState, interface_id: felt252) -> bool; fn __validate_declare__(self: @TState, class_hash: felt252) -> felt252; @@ -20,52 +20,141 @@ pub trait IRosettaAccount { #[starknet::contract(account)] mod RosettaAccount { use super::EthPublicKey; - use starknet::{EthAddress, get_execution_info, get_contract_address}; + use core::num::traits::Zero; + use starknet::{ + EthAddress, get_execution_info, get_contract_address, get_caller_address, get_tx_info + }; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use rosettacontracts::accounts::utils::{is_valid_eth_signature, Secp256k1PointStorePacking}; + + pub mod Errors { + pub const INVALID_CALLER: felt252 = 'Rosetta: invalid caller'; + pub const INVALID_SIGNATURE: felt252 = 'Rosetta: invalid signature'; + pub const INVALID_TX_VERSION: felt252 = 'Rosetta: invalid tx version'; + pub const UNAUTHORIZED: felt252 = 'Rosetta: unauthorized'; + } #[storage] struct Storage { - ethereum_address: EthAddress + ethereum_address: EthAddress, + ethereum_public_key: EthPublicKey } #[constructor] - fn constructor(ref self: ContractState) {} + fn constructor(ref self: ContractState, eth_account: EthAddress) { + self.ethereum_address.write(eth_account); + } #[abi(embed_v0)] impl AccountImpl of super::IRosettaAccount { - fn __execute__(self: @TState, calls: Array) -> Array> {} + // Instead of Array we use Array since we pass different values to the + // parameter + fn __execute__(self: @ContractState, calls: Array) -> Array> { + let sender = get_caller_address(); + assert(sender.is_zero(), Errors::INVALID_CALLER); + // TODO: Check tx version - fn __validate__(self: @TState, calls: Array) -> felt252 {} + // TODO: Exec calls + } - fn is_valid_signature(self: @TState, hash: felt252, signature: Array) -> felt252 {} + fn __validate__(self: @ContractState, calls: Array) -> felt252 { + // TODO: check if validations enough + self.validate_transaction() + } - fn supports_interface(self: @TState, interface_id: felt252) -> bool {} + fn is_valid_signature( + self: @ContractState, hash: felt252, signature: Array + ) -> felt252 { + if self._is_valid_signature(hash, signature.span()) { + starknet::VALIDATED + } else { + 0 + } + } - fn __validate_declare__(self: @TState, class_hash: felt252) -> felt252 {} + fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { + true + } + + fn __validate_declare__(self: @ContractState, class_hash: felt252) -> felt252 { + // TODO: check if validations enough + self.validate_transaction() + } fn __validate_deploy__( - self: @TState, + self: @ContractState, class_hash: felt252, contract_address_salt: felt252, public_key: EthPublicKey - ) -> felt252 {} + ) -> felt252 { + // TODO: check if validations enough + self.validate_transaction() + } - fn get_public_key(self: @TState) -> EthPublicKey {} + fn get_public_key(self: @ContractState) -> EthPublicKey { + self.ethereum_public_key.read() + } + // We dont need that function fn set_public_key( - ref self: TState, new_public_key: EthPublicKey, signature: Span + ref self: ContractState, new_public_key: EthPublicKey, signature: Span ) {} - fn isValidSignature(self: @TState, hash: felt252, signature: Array) -> felt252 { + fn isValidSignature( + self: @ContractState, hash: felt252, signature: Array + ) -> felt252 { self.is_valid_signature(hash, signature) } - fn getPublicKey(self: @TState) -> EthPublicKey { + fn getPublicKey(self: @ContractState) -> EthPublicKey { self.get_public_key() } - fn setPublicKey(ref self: TState, newPublicKey: EthPublicKey, signature: Span) { + // We dont need that function + fn setPublicKey( + ref self: ContractState, newPublicKey: EthPublicKey, signature: Span + ) { self.set_public_key(newPublicKey, signature) } } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn initializer(ref self: ContractState, ethPubKey: EthPublicKey) { + // Write pubkey to storage + self._set_public_key(ethPubKey); + } + + fn assert_only_self(self: @ContractState) { + let caller = get_caller_address(); + let self = get_contract_address(); + assert(self == caller, Errors::UNAUTHORIZED); + } + + // Overwrites ethereum public key. We may remove that function since we only need to + // write during initialization. + fn _set_public_key(ref self: ContractState, new_public_key: EthPublicKey) { + self.ethereum_public_key.write(new_public_key); + } + + /// Validates the signature for the current transaction. + /// Returns the short string `VALID` if valid, otherwise it reverts. + fn validate_transaction(self: @ContractState) -> felt252 { + let tx_info = get_tx_info().unbox(); + let tx_hash = tx_info.transaction_hash; + let signature = tx_info.signature; + assert(self._is_valid_signature(tx_hash, signature), Errors::INVALID_SIGNATURE); + starknet::VALIDATED + } + + /// Returns whether the given signature is valid for the given hash + /// using the account's current public key. + fn _is_valid_signature( + self: @ContractState, hash: felt252, signature: Span + ) -> bool { + let public_key: EthPublicKey = self.ethereum_public_key.read(); + is_valid_eth_signature(hash, public_key, signature) + } + } } diff --git a/src/accounts/utils.cairo b/src/accounts/utils.cairo new file mode 100644 index 0000000..9493040 --- /dev/null +++ b/src/accounts/utils.cairo @@ -0,0 +1,77 @@ +use starknet::secp256_trait; +use rosettacontracts::accounts::base::{EthPublicKey}; + +#[derive(Copy, Drop, Serde)] +pub struct EthSignature { + pub r: u256, + pub s: u256, +} + +pub fn is_valid_eth_signature( + msg_hash: felt252, public_key: EthPublicKey, signature: Span +) -> bool { + let mut signature = signature; + let signature: EthSignature = Serde::deserialize(ref signature) + .expect('Signature: Invalid format.'); + + secp256_trait::is_valid_signature(msg_hash.into(), signature.r, signature.s, public_key) +} + +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (account/utils/secp256k1.cairo) + +use core::fmt::{Formatter, Error}; +use starknet::SyscallResultTrait; +use starknet::secp256_trait::{Secp256Trait, Secp256PointTrait}; +use starknet::secp256k1::Secp256k1Point; +use starknet::storage_access::StorePacking; + +/// Packs a Secp256k1Point into a (felt252, felt252). +/// +/// The packing is done as follows: +/// - First felt contains x.low (x being the x-coordinate of the point). +/// - Second felt contains x.high and the parity bit, at the least significant bits (2 * x.high + +/// parity). +pub impl Secp256k1PointStorePacking of StorePacking { + fn pack(value: Secp256k1Point) -> (felt252, felt252) { + let (x, y) = value.get_coordinates().unwrap_syscall(); + + let parity = y % 2; + let xhigh_and_parity = 2 * x.high.into() + parity.try_into().unwrap(); + + (x.low.into(), xhigh_and_parity) + } + + fn unpack(value: (felt252, felt252)) -> Secp256k1Point { + let (xlow, xhigh_and_parity) = value; + let xhigh_and_parity: u256 = xhigh_and_parity.into(); + + let x = u256 { + low: xlow.try_into().unwrap(), high: (xhigh_and_parity / 2).try_into().unwrap(), + }; + let parity = xhigh_and_parity % 2 == 1; + + // Expects parity odd to be true + Secp256Trait::secp256_ec_get_point_from_x_syscall(x, parity) + .unwrap_syscall() + .expect('Secp256k1Point: Invalid point.') + } +} + +pub impl Secp256k1PointPartialEq of PartialEq { + #[inline(always)] + fn eq(lhs: @Secp256k1Point, rhs: @Secp256k1Point) -> bool { + (*lhs).get_coordinates().unwrap_syscall() == (*rhs).get_coordinates().unwrap_syscall() + } + #[inline(always)] + fn ne(lhs: @Secp256k1Point, rhs: @Secp256k1Point) -> bool { + !(lhs == rhs) + } +} + +pub impl DebugSecp256k1Point of core::fmt::Debug { + fn fmt(self: @Secp256k1Point, ref f: Formatter) -> Result<(), Error> { + let (x, y) = (*self).get_coordinates().unwrap_syscall(); + write!(f, "({x:?},{y:?})") + } +} diff --git a/src/factory.cairo b/src/factory.cairo index 9057557..3ebc174 100644 --- a/src/factory.cairo +++ b/src/factory.cairo @@ -5,13 +5,16 @@ trait IFactory { fn lens(self: @TContractState) -> ContractAddress; fn current_account_class(self: @TContractState) -> ClassHash; fn precalculate_starknet_address(self: @TContractState, address: EthAddress) -> ContractAddress; + fn deploy_account(self: @TContractState, address: EthAddress) -> ContractAddress; } #[starknet::contract] mod Factory { use core::option::OptionTrait; use starknet::{ContractAddress, ClassHash, EthAddress}; + use starknet::syscalls::{deploy_syscall}; use core::traits::{Into, TryInto}; + use openzeppelin::utils::deployments::{calculate_contract_address_from_deploy_syscall}; #[storage] struct Storage { @@ -46,8 +49,33 @@ mod Factory { fn precalculate_starknet_address( self: @ContractState, address: EthAddress ) -> ContractAddress { - // todo - 0.try_into().unwrap() + // TODO: Tests + + let eth_address_felt: felt252 = address.into(); + calculate_contract_address_from_deploy_syscall( + eth_address_felt, + self.account_class.read(), + array![eth_address_felt].span(), + 0.try_into().unwrap() + ) + } + + /// Deploys new rosettanet account. Fails if account already deployed + /// # Params + /// `address` - Ethereum Address that will be used to deploy starknet account. + /// # Returns + /// `ContractAddress` - Newly deployed starknet account + fn deploy_account(self: @ContractState, address: EthAddress) -> ContractAddress { + // TODO: Tests + let eth_address_felt: felt252 = address.into(); + + let (account, _) = deploy_syscall( + self.account_class.read(), eth_address_felt, array![eth_address_felt].span(), true + ) + .unwrap(); + + // Todo: register lens if needed ?? Or we can use precalculate + account } } } diff --git a/src/lens/lens_dev.cairo b/src/lens/lens_dev.cairo index 6a5d17d..8da11ca 100644 --- a/src/lens/lens_dev.cairo +++ b/src/lens/lens_dev.cairo @@ -77,7 +77,8 @@ mod LensDev { } } - // Function just for development tests. We can match any address with any eth address we want. + // Function just for development tests. We can match any address with any eth address we + // want. // Helps developing. fn register_address_dev( ref self: ContractState, address: ContractAddress, eth: EthAddress diff --git a/src/lib.cairo b/src/lib.cairo index 64f626a..c3ad520 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,2 +1,7 @@ -mod factory; -mod lens; +pub mod factory; +pub mod lens; + +pub mod accounts { + pub mod base; + pub mod utils; +} diff --git a/src/verifier/utils.cairo b/src/verifier/utils.cairo index 7d1f15c..061fe30 100644 --- a/src/verifier/utils.cairo +++ b/src/verifier/utils.cairo @@ -1,10 +1,11 @@ -/// Returns starknet calldata span of felt252s +/// Returns starknet calldata span of felt252s /// This function do not checks function selector /// # Params /// `offsets` - Calldata read offsets, parse done according to these values /// `calldata` - Actual EVM calldata, each element presents one slot /// # Returns -/// `Span` - Parsed and converted calldata which is going to be passed to call_contract_syscall. +/// `Span` - Parsed and converted calldata which is going to be passed to +/// call_contract_syscall. pub fn parse_calldata(offsets: Span, calldata: Span) -> Span {} @@ -12,14 +13,16 @@ pub fn parse_calldata(offsets: Span, calldata: Span) -> Span, signature: u16) -> felt252 {} /// Parse parameters bit offsets according their sizes in ethereum slot /// It has to be called after function name is found and matched. /// # Params -/// `function` - Ethereum function name and parameters e.g. balanceOf(address), witharray(uint256[]), withtuple((uint128,uint64,uint256),address) +/// `function` - Ethereum function name and parameters e.g. balanceOf(address), +/// witharray(uint256[]), withtuple((uint128,uint64,uint256),address) /// # Returns /// `Span` - Calldata read offsets in bits pub fn parse_calldata_offsets(function: ByteArray) -> Span {} diff --git a/src/accounts.cairo b/tests/factory_tests.cairo similarity index 100% rename from src/accounts.cairo rename to tests/factory_tests.cairo