diff --git a/contracts/Scarb.lock b/contracts/Scarb.lock index 6a5a0ed..d18c248 100644 --- a/contracts/Scarb.lock +++ b/contracts/Scarb.lock @@ -5,110 +5,20 @@ version = 1 name = "kudos" version = "0.1.0" dependencies = [ - "openzeppelin", "snforge_std", ] -[[package]] -name = "openzeppelin" -version = "0.16.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" -dependencies = [ - "openzeppelin_access", - "openzeppelin_account", - "openzeppelin_governance", - "openzeppelin_introspection", - "openzeppelin_merkle_tree", - "openzeppelin_presets", - "openzeppelin_security", - "openzeppelin_token", - "openzeppelin_upgrades", - "openzeppelin_utils", -] - -[[package]] -name = "openzeppelin_access" -version = "0.16.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" -dependencies = [ - "openzeppelin_introspection", - "openzeppelin_utils", -] - -[[package]] -name = "openzeppelin_account" -version = "0.16.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" -dependencies = [ - "openzeppelin_introspection", - "openzeppelin_utils", -] - -[[package]] -name = "openzeppelin_governance" -version = "0.16.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" -dependencies = [ - "openzeppelin_access", - "openzeppelin_introspection", -] - -[[package]] -name = "openzeppelin_introspection" -version = "0.16.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" - -[[package]] -name = "openzeppelin_merkle_tree" -version = "0.16.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" - -[[package]] -name = "openzeppelin_presets" -version = "0.16.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" -dependencies = [ - "openzeppelin_access", - "openzeppelin_account", - "openzeppelin_introspection", - "openzeppelin_token", - "openzeppelin_upgrades", -] - -[[package]] -name = "openzeppelin_security" -version = "0.16.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" - -[[package]] -name = "openzeppelin_token" -version = "0.16.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" -dependencies = [ - "openzeppelin_account", - "openzeppelin_governance", - "openzeppelin_introspection", -] - -[[package]] -name = "openzeppelin_upgrades" -version = "0.16.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" - -[[package]] -name = "openzeppelin_utils" -version = "0.16.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" - [[package]] name = "snforge_scarb_plugin" -version = "0.1.0" -source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.30.0#196f06b251926697c3d66800f2a93ae595e76496" +version = "0.2.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:2e4ce3ebe3f49548bd26908391b5d78537a765d827df0d96c32aeb88941d0d67" [[package]] name = "snforge_std" version = "0.30.0" -source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.30.0#196f06b251926697c3d66800f2a93ae595e76496" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:2f3c4846881813ac0f5d1460981249c9f5e2a6831e752beedf9b70975495b4ec" dependencies = [ "snforge_scarb_plugin", ] diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml index c606fc8..8bbcf98 100644 --- a/contracts/Scarb.toml +++ b/contracts/Scarb.toml @@ -11,9 +11,9 @@ sort-module-level-items = true [dependencies] starknet = "2.8.2" -openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.16.0" } [dev-dependencies] -snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.30.0" } +assert_macros = "2.8.2" +snforge_std = "0.30.0" [[target.starknet-contract]] diff --git a/contracts/src/credential_registry/component.cairo b/contracts/src/credential_registry/component.cairo index 58f2443..b625d25 100644 --- a/contracts/src/credential_registry/component.cairo +++ b/contracts/src/credential_registry/component.cairo @@ -2,7 +2,6 @@ pub mod CredentialRegistryComponent { use core::num::traits::zero::Zero; use kudos::credential_registry::interface::ICredentialRegistry; - use openzeppelin::account::interface::{AccountABIDispatcherTrait, AccountABIDispatcher}; use starknet::storage::{ StoragePointerReadAccess, StoragePointerWriteAccess, StoragePathEntry, Map }; @@ -12,8 +11,6 @@ pub mod CredentialRegistryComponent { pub struct Storage { credentials: Map::, user_to_credentials: Map::, - credentials_w_pin: Map::, - user_to_credentials_w_pin: Map::, total_credentials: u128, } @@ -28,12 +25,10 @@ pub mod CredentialRegistryComponent { #[key] pub address: ContractAddress, pub hash: felt252, - pub hash_w_pin: felt252 } pub mod CredentialRegistryErrors { pub const CREDENTIAL_REGISTERED: felt252 = 'User prev registered cred'; - pub const CREDENTIAL_W_PIN_REGISTERED: felt252 = 'User prev registered cred_w_pin'; pub const CREDENTIAL_INVALID: felt252 = 'User provided is invalid'; pub const INVALID_SIGNATURE: felt252 = 'Invalid signature provided'; } @@ -43,24 +38,18 @@ pub mod CredentialRegistryComponent { TContractState, +HasComponent > of ICredentialRegistry> { fn register_credentials( - ref self: ComponentState, - hash: felt252, - signature: Array, - hash_w_pin: felt252, - signature_w_pin: Array + ref self: ComponentState, hash: felt252, signature: Array, ) { - assert(hash != hash_w_pin, CredentialRegistryErrors::CREDENTIAL_INVALID); let address = get_caller_address(); self._register_credentials(hash, address, signature); - self._register_credential_w_pin(hash_w_pin, address, signature_w_pin); let prev_total = self.total_credentials.read(); self.total_credentials.write(prev_total + 1); // TODO: mint $KUDOS here - self.emit(CredentialsRegistered { address, hash, hash_w_pin }) + self.emit(CredentialsRegistered { address, hash }) } fn get_credential( @@ -75,18 +64,6 @@ pub mod CredentialRegistryComponent { self.credentials.entry(hash).read() } - fn get_credential_w_pin( - self: @ComponentState, address: ContractAddress - ) -> felt252 { - self.user_to_credentials_w_pin.entry(address).read() - } - - fn get_credential_address_w_pin( - self: @ComponentState, hash: felt252 - ) -> ContractAddress { - self.credentials_w_pin.entry(hash).read() - } - fn get_total_credentials(self: @ComponentState) -> u128 { self.total_credentials.read() } @@ -111,35 +88,8 @@ pub mod CredentialRegistryComponent { CredentialRegistryErrors::CREDENTIAL_REGISTERED ); - let account = AccountABIDispatcher { contract_address }; - assert( - account.is_valid_signature(hash, signature) == starknet::VALIDATED, - CredentialRegistryErrors::INVALID_SIGNATURE - ); - self.credentials.entry(hash).write(contract_address); self.user_to_credentials.entry(contract_address).write(hash); } - - fn _register_credential_w_pin( - ref self: ComponentState, - hash: felt252, - contract_address: ContractAddress, - signature: Array - ) { - assert( - self.credentials_w_pin.entry(hash).read() == contract_address_const::<0>(), - CredentialRegistryErrors::CREDENTIAL_W_PIN_REGISTERED - ); - - let account = AccountABIDispatcher { contract_address }; - assert( - account.is_valid_signature(hash, signature) == starknet::VALIDATED, - CredentialRegistryErrors::INVALID_SIGNATURE - ); - - self.credentials_w_pin.entry(hash).write(contract_address); - self.user_to_credentials_w_pin.entry(contract_address).write(hash); - } } } diff --git a/contracts/src/credential_registry/interface.cairo b/contracts/src/credential_registry/interface.cairo index 8ab5a66..a6fa831 100644 --- a/contracts/src/credential_registry/interface.cairo +++ b/contracts/src/credential_registry/interface.cairo @@ -2,17 +2,9 @@ use starknet::ContractAddress; #[starknet::interface] pub trait ICredentialRegistry { - fn register_credentials( - ref self: TState, - hash: felt252, - signature: Array, - hash_w_pin: felt252, - signature_w_pin: Array - ); + fn register_credentials(ref self: TState, hash: felt252, signature: Array,); fn get_credential(self: @TState, address: ContractAddress) -> felt252; fn get_credential_address(self: @TState, hash: felt252) -> ContractAddress; - fn get_credential_w_pin(self: @TState, address: ContractAddress) -> felt252; - fn get_credential_address_w_pin(self: @TState, hash: felt252) -> ContractAddress; fn get_total_credentials(self: @TState) -> u128; fn is_registered(self: @TState, address: ContractAddress) -> bool; } diff --git a/contracts/src/tests/mocks/credential_registry_mock.cairo b/contracts/src/credential_registry/tests/mock_credential_registry.cairo similarity index 95% rename from contracts/src/tests/mocks/credential_registry_mock.cairo rename to contracts/src/credential_registry/tests/mock_credential_registry.cairo index e6b35b7..e01c40d 100644 --- a/contracts/src/tests/mocks/credential_registry_mock.cairo +++ b/contracts/src/credential_registry/tests/mock_credential_registry.cairo @@ -1,5 +1,5 @@ #[starknet::contract] -pub(crate) mod CredentialRegistryMock { +pub mod CredentialRegistryMock { use kudos::credential_registry::component::CredentialRegistryComponent; component!( diff --git a/contracts/src/credential_registry/tests/test_credential_registry.cairo b/contracts/src/credential_registry/tests/test_credential_registry.cairo new file mode 100644 index 0000000..7f4e424 --- /dev/null +++ b/contracts/src/credential_registry/tests/test_credential_registry.cairo @@ -0,0 +1,26 @@ +use kudos::credential_registry::ICredentialRegistry; + +use kudos::credential_registry::component::{ + CredentialRegistryComponent, CredentialRegistryComponent::InternalImpl +}; +use super::mocks::mock_credential_registry::CredentialRegistryMock; + +// +// Setup +// + +type ComponentState = + CredentialRegistryComponent::ComponentState; + +impl ComponentStateDefault of Default { + fn default() -> ComponentState { + CredentialRegistryComponent::component_state_for_testing() + } +} + +#[test] +fn test_initial_values() { + let mut registry: ComponentState = Default::default(); + + assert!(registry.get_total_credentials() == 0); +} diff --git a/contracts/src/interface.cairo b/contracts/src/interface.cairo new file mode 100644 index 0000000..40acc4f --- /dev/null +++ b/contracts/src/interface.cairo @@ -0,0 +1,10 @@ +#[starknet::interface] +pub trait IKudos { + fn give_kudos( + ref self: TState, + amount: felt252, + sender_credentials: felt252, + receiver_credentials: felt252, + description: felt252, + ); +} diff --git a/contracts/src/kudos.cairo b/contracts/src/kudos.cairo index 7e65e18..733e6f0 100644 --- a/contracts/src/kudos.cairo +++ b/contracts/src/kudos.cairo @@ -1,30 +1,30 @@ #[starknet::contract] pub mod Kudos { - use openzeppelin::access::ownable::OwnableComponent; - use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + // use kudos::token::{IERC20, IERC20Metadata}; + use kudos::IKudos; + use kudos::oz16::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use kudos::oz16::ownable::OwnableComponent; use starknet::ContractAddress; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); component!(path: ERC20Component, storage: erc20, event: ERC20Event); - // Ownable Mixin #[abi(embed_v0)] - impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl; - impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl ERC20Impl = ERC20Component::ERC20Impl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; - // ERC20 Mixin #[abi(embed_v0)] - impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; - impl ERC20InternalImpl = ERC20Component::InternalImpl; + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; // TODO: Embed the credential registry component #[storage] struct Storage { - #[substorage(v0)] - ownable: OwnableComponent::Storage, #[substorage(v0)] erc20: ERC20Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, } // TODO: define contract errors @@ -35,27 +35,32 @@ pub mod Kudos { #[event] #[derive(Drop, starknet::Event)] enum Event { - #[flat] - OwnableEvent: OwnableComponent::Event, #[flat] ERC20Event: ERC20Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, } #[constructor] fn constructor( - ref self: ContractState, - name: ByteArray, - symbol: ByteArray, - initial_supply: u256, - recipient: ContractAddress, - owner: ContractAddress + ref self: ContractState, name: ByteArray, symbol: ByteArray, owner: ContractAddress ) { - self.ownable.initializer(owner); self.erc20.initializer(name, symbol); + self.ownable.initializer(owner); } // TODO: -// - define and implement IKudos interface including `give_kudos` -// - allow the credential registry to `mint` to a recipient -// - don't expose `transfer` only `transfer_from` and make sure only the credential registry can -// call it - write tests to ensure this + // - define and implement IKudos interface including `give_kudos` + // - allow the credential registry to `mint` to a recipient + // - don't expose `transfer` only `transfer_from` and make sure only the credential registry can + // call it - write tests to ensure this + #[abi(embed_v0)] + impl Kudos of IKudos { + fn give_kudos( + ref self: ContractState, + amount: felt252, + sender_credentials: felt252, + receiver_credentials: felt252, + description: felt252, + ) {} + } } diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index 009e4e9..7755d13 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -1,24 +1,23 @@ -mod kudos; +mod interface; +pub mod kudos; +pub use interface::{IKudos, IKudosDispatcher, IKudosDispatcherTrait}; -mod credential_registry { +pub mod oz16 { + pub mod erc20; + + mod interfaces; + pub mod ownable; + pub use interfaces::{ + IERC20, IERC20Dispatcher, IERC20DispatcherTrait, IOwnable, IOwnableDispatcher, + IOwnableDispatcherTrait + }; +} + +pub mod credential_registry { pub mod component; - mod interface; + mod interface; pub use interface::{ ICredentialRegistry, ICredentialRegistryDispatcher, ICredentialRegistryDispatcherTrait }; } - -mod tests { - #[cfg(test)] - pub(crate) mod common; - #[cfg(test)] - mod test_credential_registry; - mod mocks { - pub(crate) mod account_mock; - pub(crate) mod credential_registry_mock; - } - pub(crate) mod utils { - pub(crate) mod constants; - } -} diff --git a/contracts/src/oz16/erc20.cairo b/contracts/src/oz16/erc20.cairo new file mode 100644 index 0000000..cfabff8 --- /dev/null +++ b/contracts/src/oz16/erc20.cairo @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.16.0 (token/erc20/erc20.cairo) + +/// # ERC20 Component +/// +/// The ERC20 component provides an implementation of the IERC20 interface as well as +/// non-standard implementations that can be used to create an ERC20 contract. This +/// component is agnostic regarding how tokens are created, which means that developers +/// must create their own token distribution mechanism. +/// See [the documentation] +/// (https://docs.openzeppelin.com/contracts-cairo/0.16.0/guides/erc20-supply) +/// for examples. +#[starknet::component] +pub mod ERC20Component { + use core::num::traits::Bounded; + use core::num::traits::Zero; + use kudos::oz16::IERC20; + use starknet::ContractAddress; + use starknet::get_caller_address; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess + }; + + #[storage] + pub struct Storage { + pub ERC20_name: ByteArray, + pub ERC20_symbol: ByteArray, + pub ERC20_total_supply: u256, + pub ERC20_balances: Map, + pub ERC20_allowances: Map<(ContractAddress, ContractAddress), u256>, + } + + #[event] + #[derive(Drop, PartialEq, starknet::Event)] + pub enum Event { + Transfer: Transfer, + Approval: Approval, + } + + /// Emitted when tokens are moved from address `from` to address `to`. + #[derive(Drop, PartialEq, starknet::Event)] + pub struct Transfer { + #[key] + pub from: ContractAddress, + #[key] + pub to: ContractAddress, + pub value: u256 + } + + /// Emitted when the allowance of a `spender` for an `owner` is set by a call + /// to `approve`. `value` is the new allowance. + #[derive(Drop, PartialEq, starknet::Event)] + pub struct Approval { + #[key] + pub owner: ContractAddress, + #[key] + pub spender: ContractAddress, + pub value: u256 + } + + pub mod Errors { + pub const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0'; + pub const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0'; + pub const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0'; + pub const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0'; + pub const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0'; + pub const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0'; + pub const INSUFFICIENT_BALANCE: felt252 = 'ERC20: insufficient balance'; + pub const INSUFFICIENT_ALLOWANCE: felt252 = 'ERC20: insufficient allowance'; + } + + // + // Hooks + // + + pub trait ERC20HooksTrait { + fn before_update( + ref self: ComponentState, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) {} + + fn after_update( + ref self: ComponentState, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) {} + } + + // + // External + // + + #[embeddable_as(ERC20Impl)] + impl ERC20< + TContractState, +HasComponent, +ERC20HooksTrait + > of IERC20> { + /// Returns the name of the token. + fn name(self: @ComponentState) -> ByteArray { + self.ERC20_name.read() + } + + /// Returns the ticker symbol of the token, usually a shorter version of the name. + fn symbol(self: @ComponentState) -> ByteArray { + self.ERC20_symbol.read() + } + + /// Returns the number of decimals used to get its user representation. + fn decimals(self: @ComponentState) -> u8 { + 18 + } + /// Returns the value of tokens in existence. + fn total_supply(self: @ComponentState) -> u256 { + self.ERC20_total_supply.read() + } + + /// Returns the amount of tokens owned by `account`. + fn balance_of(self: @ComponentState, account: ContractAddress) -> u256 { + self.ERC20_balances.read(account) + } + + /// Returns the remaining number of tokens that `spender` is + /// allowed to spend on behalf of `owner` through `transfer_from`. + /// This is zero by default. + /// This value changes when `approve` or `transfer_from` are called. + fn allowance( + self: @ComponentState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + self.ERC20_allowances.read((owner, spender)) + } + + /// Moves `amount` tokens from the caller's token balance to `to`. + /// + /// Requirements: + /// + /// - `recipient` is not the zero address. + /// - The caller has a balance of at least `amount`. + /// + /// Emits a `Transfer` event. + fn transfer( + ref self: ComponentState, recipient: ContractAddress, amount: u256 + ) -> bool { + let sender = get_caller_address(); + self._transfer(sender, recipient, amount); + true + } + + /// Moves `amount` tokens from `from` to `to` using the allowance mechanism. + /// `amount` is then deducted from the caller's allowance. + /// + /// Requirements: + /// + /// - `sender` is not the zero address. + /// - `sender` must have a balance of at least `amount`. + /// - `recipient` is not the zero address. + /// - The caller has an allowance of `sender`'s tokens of at least `amount`. + /// + /// Emits a `Transfer` event. + fn transfer_from( + ref self: ComponentState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + let caller = get_caller_address(); + self._spend_allowance(sender, caller, amount); + self._transfer(sender, recipient, amount); + true + } + + /// Sets `amount` as the allowance of `spender` over the caller’s tokens. + /// + /// Requirements: + /// + /// - `spender` is not the zero address. + /// + /// Emits an `Approval` event. + fn approve( + ref self: ComponentState, spender: ContractAddress, amount: u256 + ) -> bool { + let caller = get_caller_address(); + self._approve(caller, spender, amount); + true + } + } + + // + // Internal + // + + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent, impl Hooks: ERC20HooksTrait + > of InternalTrait { + /// Initializes the contract by setting the token name and symbol. + /// To prevent reinitialization, this should only be used inside of a contract's + /// constructor. + fn initializer( + ref self: ComponentState, name: ByteArray, symbol: ByteArray + ) { + self.ERC20_name.write(name); + self.ERC20_symbol.write(symbol); + } + + /// Creates a `value` amount of tokens and assigns them to `account`. + /// + /// Requirements: + /// + /// - `recipient` is not the zero address. + /// + /// Emits a `Transfer` event with `from` set to the zero address. + fn mint( + ref self: ComponentState, recipient: ContractAddress, amount: u256 + ) { + assert(!recipient.is_zero(), Errors::MINT_TO_ZERO); + self.update(Zero::zero(), recipient, amount); + } + + /// Destroys `amount` of tokens from `account`. + /// + /// Requirements: + /// + /// - `account` is not the zero address. + /// - `account` must have at least a balance of `amount`. + /// + /// Emits a `Transfer` event with `to` set to the zero address. + fn burn(ref self: ComponentState, account: ContractAddress, amount: u256) { + assert(!account.is_zero(), Errors::BURN_FROM_ZERO); + self.update(account, Zero::zero(), amount); + } + + + /// Transfers an `amount` of tokens from `from` to `to`, or alternatively mints (or burns) + /// if `from` (or `to`) is the zero address. + /// + /// NOTE: This function can be extended using the `ERC20HooksTrait`, to add + /// functionality before and/or after the transfer, mint, or burn. + /// + /// Emits a `Transfer` event. + fn update( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + amount: u256 + ) { + Hooks::before_update(ref self, from, to, amount); + + let zero_address = Zero::zero(); + if (from == zero_address) { + let total_supply = self.ERC20_total_supply.read(); + self.ERC20_total_supply.write(total_supply + amount); + } else { + let from_balance = self.ERC20_balances.read(from); + assert(from_balance >= amount, Errors::INSUFFICIENT_BALANCE); + self.ERC20_balances.write(from, from_balance - amount); + } + + if (to == zero_address) { + let total_supply = self.ERC20_total_supply.read(); + self.ERC20_total_supply.write(total_supply - amount); + } else { + let to_balance = self.ERC20_balances.read(to); + self.ERC20_balances.write(to, to_balance + amount); + } + + self.emit(Transfer { from, to, value: amount }); + + Hooks::after_update(ref self, from, to, amount); + } + + /// Internal method that moves an `amount` of tokens from `from` to `to`. + /// + /// Requirements: + /// + /// - `sender` is not the zero address. + /// - `sender` must have at least a balance of `amount`. + /// - `recipient` is not the zero address. + /// + /// Emits a `Transfer` event. + fn _transfer( + ref self: ComponentState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO); + assert(!recipient.is_zero(), Errors::TRANSFER_TO_ZERO); + self.update(sender, recipient, amount); + } + + /// Internal method that sets `amount` as the allowance of `spender` over the + /// `owner`s tokens. + /// + /// Requirements: + /// + /// - `owner` is not the zero address. + /// - `spender` is not the zero address. + /// + /// Emits an `Approval` event. + fn _approve( + ref self: ComponentState, + owner: ContractAddress, + spender: ContractAddress, + amount: u256 + ) { + assert(!owner.is_zero(), Errors::APPROVE_FROM_ZERO); + assert(!spender.is_zero(), Errors::APPROVE_TO_ZERO); + self.ERC20_allowances.write((owner, spender), amount); + self.emit(Approval { owner, spender, value: amount }); + } + + /// Updates `owner`s allowance for `spender` based on spent `amount`. + /// Does not update the allowance value in case of infinite allowance. + /// + /// Requirements: + /// + /// - `spender` must have at least an allowance of `amount` from `owner`. + /// + /// Possibly emits an `Approval` event. + fn _spend_allowance( + ref self: ComponentState, + owner: ContractAddress, + spender: ContractAddress, + amount: u256 + ) { + let current_allowance = self.ERC20_allowances.read((owner, spender)); + if current_allowance != Bounded::MAX { + assert(current_allowance >= amount, Errors::INSUFFICIENT_ALLOWANCE); + self._approve(owner, spender, current_allowance - amount); + } + } + } +} + +/// An empty implementation of the ERC20 hooks to be used in basic ERC20 preset contracts. +pub impl ERC20HooksEmptyImpl of ERC20Component::ERC20HooksTrait {} diff --git a/contracts/src/oz16/interfaces.cairo b/contracts/src/oz16/interfaces.cairo new file mode 100644 index 0000000..a6f133e --- /dev/null +++ b/contracts/src/oz16/interfaces.cairo @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.16.0 (token/erc20/interface.cairo) + +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IERC20 { + fn name(self: @TState) -> ByteArray; + fn symbol(self: @TState) -> ByteArray; + fn decimals(self: @TState) -> u8; + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; +} + +#[starknet::interface] +pub trait IOwnable { + fn owner(self: @TState) -> ContractAddress; + fn transfer_ownership(ref self: TState, new_owner: ContractAddress); + fn renounce_ownership(ref self: TState); +} diff --git a/contracts/src/oz16/ownable.cairo b/contracts/src/oz16/ownable.cairo new file mode 100644 index 0000000..db0409f --- /dev/null +++ b/contracts/src/oz16/ownable.cairo @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.16.0 (access/ownable/ownable.cairo) + +/// # Ownable Component +/// +/// The Ownable component 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 can be set by using the `initializer` function in +/// construction time. This can later be changed with `transfer_ownership`. +/// +/// The component also offers functionality for a two-step ownership +/// transfer where the new owner first has to accept their ownership to +/// finalize the transfer. +#[starknet::component] +pub mod OwnableComponent { + use core::num::traits::Zero; + use kudos::oz16::IOwnable; + use starknet::ContractAddress; + use starknet::get_caller_address; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + #[storage] + pub struct Storage { + pub Ownable_owner: ContractAddress, + pub Ownable_pending_owner: ContractAddress + } + + #[event] + #[derive(Drop, PartialEq, starknet::Event)] + pub enum Event { + OwnershipTransferred: OwnershipTransferred, + OwnershipTransferStarted: OwnershipTransferStarted + } + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct OwnershipTransferred { + #[key] + pub previous_owner: ContractAddress, + #[key] + pub new_owner: ContractAddress, + } + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct OwnershipTransferStarted { + #[key] + pub previous_owner: ContractAddress, + #[key] + pub new_owner: ContractAddress, + } + + pub mod Errors { + pub const NOT_OWNER: felt252 = 'Caller is not the owner'; + pub const NOT_PENDING_OWNER: felt252 = 'Caller is not the pending owner'; + pub const ZERO_ADDRESS_CALLER: felt252 = 'Caller is the zero address'; + pub const ZERO_ADDRESS_OWNER: felt252 = 'New owner is the zero address'; + } + + #[embeddable_as(OwnableImpl)] + impl Ownable< + TContractState, +HasComponent + > of IOwnable> { + /// Returns the address of the current owner. + fn owner(self: @ComponentState) -> ContractAddress { + self.Ownable_owner.read() + } + + /// Transfers ownership of the contract to a new address. + /// + /// Requirements: + /// + /// - `new_owner` is not the zero address. + /// - The caller is the contract owner. + /// + /// Emits an `OwnershipTransferred` event. + fn transfer_ownership( + ref self: ComponentState, new_owner: ContractAddress + ) { + assert(!new_owner.is_zero(), Errors::ZERO_ADDRESS_OWNER); + self.assert_only_owner(); + self._transfer_ownership(new_owner); + } + + /// Leaves the contract without owner. It will not be possible to call `assert_only_owner` + /// functions anymore. Can only be called by the current owner. + /// + /// Requirements: + /// + /// - The caller is the contract owner. + /// + /// Emits an `OwnershipTransferred` event. + fn renounce_ownership(ref self: ComponentState) { + self.assert_only_owner(); + self._transfer_ownership(Zero::zero()); + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + /// Sets the contract's initial owner. + /// + /// This function should be called at construction time. + fn initializer(ref self: ComponentState, owner: ContractAddress) { + self._transfer_ownership(owner); + } + + /// Panics if called by any account other than the owner. Use this + /// to restrict access to certain functions to the owner. + fn assert_only_owner(self: @ComponentState) { + let owner = self.Ownable_owner.read(); + let caller = get_caller_address(); + assert(!caller.is_zero(), Errors::ZERO_ADDRESS_CALLER); + assert(caller == owner, Errors::NOT_OWNER); + } + + /// Transfers ownership of the contract to a new address and resets + /// the pending owner to the zero address. + /// + /// Internal function without access restriction. + /// + /// Emits an `OwnershipTransferred` event. + fn _transfer_ownership( + ref self: ComponentState, new_owner: ContractAddress + ) { + self.Ownable_pending_owner.write(Zero::zero()); + + let previous_owner: ContractAddress = self.Ownable_owner.read(); + self.Ownable_owner.write(new_owner); + self + .emit( + OwnershipTransferred { previous_owner: previous_owner, new_owner: new_owner } + ); + } + + /// Sets a new pending owner. + /// + /// Internal function without access restriction. + /// + /// Emits an `OwnershipTransferStarted` event. + fn _propose_owner(ref self: ComponentState, new_owner: ContractAddress) { + let previous_owner = self.Ownable_owner.read(); + self.Ownable_pending_owner.write(new_owner); + self + .emit( + OwnershipTransferStarted { + previous_owner: previous_owner, new_owner: new_owner + } + ); + } + } +} diff --git a/contracts/src/tests/common.cairo b/contracts/src/tests/common.cairo deleted file mode 100644 index bed79a7..0000000 --- a/contracts/src/tests/common.cairo +++ /dev/null @@ -1,9 +0,0 @@ -use kudos::tests::utils::constants::PUBLIC_KEY; -use snforge_std::{declare, ContractClassTrait, DeclareResultTrait}; -use starknet::ContractAddress; - -pub fn setup_account() -> ContractAddress { - let account_mock = declare("AccountMock").unwrap().contract_class(); - let (contract_address, _) = account_mock.deploy(@array![PUBLIC_KEY]).unwrap(); - contract_address -} diff --git a/contracts/src/tests/mocks/account_mock.cairo b/contracts/src/tests/mocks/account_mock.cairo deleted file mode 100644 index 7fb1315..0000000 --- a/contracts/src/tests/mocks/account_mock.cairo +++ /dev/null @@ -1,39 +0,0 @@ -#[starknet::contract(account)] -pub(crate) mod AccountMock { - use openzeppelin::account::AccountComponent; - use openzeppelin::introspection::src5::SRC5Component; - - component!(path: AccountComponent, storage: account, event: AccountEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - #[abi(embed_v0)] - impl SRC6Impl = AccountComponent::SRC6Impl; - #[abi(embed_v0)] - impl PublicKeyImpl = AccountComponent::PublicKeyImpl; - impl AccountInternalImpl = AccountComponent::InternalImpl; - - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - struct Storage { - #[substorage(v0)] - account: AccountComponent::Storage, - #[substorage(v0)] - src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - AccountEvent: AccountComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, public_key: felt252) { - self.account.initializer(public_key); - } -} diff --git a/contracts/src/tests/test_credential_registry.cairo b/contracts/src/tests/test_credential_registry.cairo deleted file mode 100644 index 1c96577..0000000 --- a/contracts/src/tests/test_credential_registry.cairo +++ /dev/null @@ -1,123 +0,0 @@ -use kudos::credential_registry::component::{ - CredentialRegistryComponent, CredentialRegistryComponent::InternalImpl -}; -use kudos::credential_registry::{ - ICredentialRegistry, ICredentialRegistryDispatcher, ICredentialRegistryDispatcherTrait -}; -use kudos::tests::common::setup_account; -use kudos::tests::mocks::credential_registry_mock::CredentialRegistryMock; -use kudos::tests::utils::constants::{BAD_SIGNATURE, GOOD_SIGNATURE, GOOD_SIGNATURE_W_PIN}; -use snforge_std::{ - declare, ContractClassTrait, spy_events, EventSpyAssertionsTrait, start_cheat_caller_address, - DeclareResultTrait -}; -use starknet::ContractAddress; - -type ComponentState = - CredentialRegistryComponent::ComponentState; - -fn COMPONENT_STATE() -> ComponentState { - CredentialRegistryComponent::component_state_for_testing() -} - -fn setup() -> (ICredentialRegistryDispatcher, ContractAddress) { - let credential_registry_mock = declare("CredentialRegistryMock").unwrap().contract_class(); - let (contract_address, _) = credential_registry_mock.deploy(@array![]).unwrap(); - (ICredentialRegistryDispatcher { contract_address }, contract_address) -} - -fn setup_component() -> (ComponentState, ContractAddress) { - let mut state = COMPONENT_STATE(); - - (state, setup_account()) -} - -#[test] -fn test_register_good_credentials() { - let account_address = setup_account(); - let (mut credential_registry, credential_registry_address) = setup(); - - let (msg_hash, sig) = GOOD_SIGNATURE(); - let (msg_hash_w_pin, sig_w_pin) = GOOD_SIGNATURE_W_PIN(); - - let mut spy = spy_events(); - - start_cheat_caller_address(credential_registry_address, account_address); - credential_registry.register_credentials(msg_hash, sig, msg_hash_w_pin, sig_w_pin); - - spy - .assert_emitted( - @array![ - ( - credential_registry_address, - CredentialRegistryComponent::Event::CredentialsRegistered( - CredentialRegistryComponent::CredentialsRegistered { - address: account_address, hash: msg_hash, hash_w_pin: msg_hash_w_pin - } - ) - ) - ] - ); -} - -#[test] -fn test_register_good_credentials_internal() { - let (mut state, account_address) = setup_component(); - - let (msg_hash, sig) = GOOD_SIGNATURE(); - state._register_credentials(msg_hash, account_address, sig); - assert!(state.get_credential(account_address) == msg_hash, "incorrect credential"); - assert!(state.get_credential_address(msg_hash) == account_address, "incorrect address"); -} - -#[test] -fn test_register_good_credentials_w_pin_internal() { - let (mut state, account_address) = setup_component(); - - let (msg_hash, sig) = GOOD_SIGNATURE_W_PIN(); - state._register_credential_w_pin(msg_hash, account_address, sig); - assert!( - state.get_credential_w_pin(account_address) == msg_hash, "incorrect credential with pin" - ); - assert!( - state.get_credential_address_w_pin(msg_hash) == account_address, "incorrect addr with pin" - ); -} - -#[test] -#[should_panic(expected: ('Invalid signature provided',))] -fn test_register_bad_credentials() { - let (mut state, account_address) = setup_component(); - - let (msg_hash, sig) = BAD_SIGNATURE(); - state._register_credentials(msg_hash, account_address, sig); -} - -#[test] -#[should_panic(expected: ('Invalid signature provided',))] -fn test_register_bad_credentials_w_pin() { - let (mut state, account_address) = setup_component(); - - let (msg_hash, sig) = BAD_SIGNATURE(); - state._register_credential_w_pin(msg_hash, account_address, sig); -} - -#[test] -#[should_panic(expected: ('User prev registered cred',))] -fn test_register_duplicate_credentials() { - let (mut state, account_address) = setup_component(); - - let (msg_hash, sig) = GOOD_SIGNATURE(); - state._register_credentials(msg_hash, account_address, sig.clone()); - state._register_credentials(msg_hash, account_address, sig); -} - -#[test] -#[should_panic(expected: ('User prev registered cred_w_pin',))] -fn test_register_duplicate_credentials_w_pin() { - let (mut state, account_address) = setup_component(); - - let (msg_hash, sig) = GOOD_SIGNATURE_W_PIN(); - state._register_credential_w_pin(msg_hash, account_address, sig.clone()); - state._register_credential_w_pin(msg_hash, account_address, sig); -} diff --git a/contracts/src/tests/utils/constants.cairo b/contracts/src/tests/utils/constants.cairo deleted file mode 100644 index 7d5be33..0000000 --- a/contracts/src/tests/utils/constants.cairo +++ /dev/null @@ -1,50 +0,0 @@ -use starknet::{ContractAddress, contract_address_const}; - -pub(crate) const PRIVATE_KEY: felt252 = 0xDEADBEEF; -pub(crate) const PUBLIC_KEY: felt252 = - 0x5eeb3e0d88756352e5b7015667431490b631ea109bb6e31d65bb3bef604c186; -pub(crate) const MSG_HASH: felt252 = - 0x492f5c648d6e2c5592504078f28ae39fae7b702a6d6977e7024adbaf1c7ec66; - -pub(crate) fn CALLER() -> ContractAddress { - contract_address_const::<'CALLER'>() -} - -pub(crate) fn RECEIVER() -> ContractAddress { - contract_address_const::<'RECEIVER'>() -} - -pub(crate) fn KUDOS() -> ContractAddress { - contract_address_const::<'KUDOS'>() -} - -pub(crate) fn KUDIS() -> ContractAddress { - contract_address_const::<'KUDIS'>() -} - -pub(crate) fn BAD_SIGNATURE() -> (felt252, Array) { - (0x1, array![0x2, 0x3]) -} - -/// Signatures were computed using starknet.py -pub(crate) fn GOOD_SIGNATURE() -> (felt252, Array) { - let msg_hash = 0x492f5c648d6e2c5592504078f28ae39fae7b702a6d6977e7024adbaf1c7ec66; - ( - msg_hash, - array![ - 0x7a5ec675936dddddb949162f4b7f73f5c6e532ecff47249bb0e096047a33a2, - 0x110cfbb7bb5ae0b416d250d7ff715c55c41dd33564595d96f998fcc15f94338 - ] - ) -} - -pub(crate) fn GOOD_SIGNATURE_W_PIN() -> (felt252, Array) { - let msg_hash = 0x5265a44510b3d941cd17945b82b72aa79557062c786c900e30189de0f79d749; - ( - msg_hash, - array![ - 0xfb4c5230c78b66e8f78b558f891c090bba36b1bcef67fb91e503be43cb509b, - 0x612435be895bd326dbcf5fd43f8952b144c163b324563b948a4186f516c43c3 - ] - ) -} diff --git a/contracts/tests/lib.cairo b/contracts/tests/lib.cairo new file mode 100644 index 0000000..df40048 --- /dev/null +++ b/contracts/tests/lib.cairo @@ -0,0 +1,23 @@ +mod utils; + +use kudos::oz16::{IERC20Dispatcher, IERC20DispatcherTrait}; + +#[test] +fn test_erc20_metadata() { + let token = IERC20Dispatcher { contract_address: utils::setup() }; + + assert_eq!(token.name(), utils::NAME()); + assert_eq!(token.symbol(), utils::SYMBOL()); + assert_eq!(token.decimals(), utils::DECIMALS); + assert_eq!(token.total_supply(), 0); +} + + +#[test] +fn test_bad_erc20_metadata() { + let token = IERC20Dispatcher { contract_address: utils::setup() }; + + assert!(token.name() != "WRONG_NAME"); + assert!(token.symbol() != "WRONG_SYMBOL"); + assert!(token.decimals() != 0); +} diff --git a/contracts/tests/utils.cairo b/contracts/tests/utils.cairo new file mode 100644 index 0000000..8b0845b --- /dev/null +++ b/contracts/tests/utils.cairo @@ -0,0 +1,54 @@ +use snforge_std::{declare, ContractClassTrait, DeclareResultTrait}; +use starknet::{ContractAddress, contract_address_const}; + +pub const DECIMALS: u8 = 18; + +pub fn CALLER() -> ContractAddress { + contract_address_const::<'CALLER'>() +} + +pub fn RECEIVER() -> ContractAddress { + contract_address_const::<'RECEIVER'>() +} + +pub fn SENDER() -> ContractAddress { + contract_address_const::<'SENDER'>() +} + +pub fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() +} + +pub fn NAME() -> ByteArray { + "Kudos" +} + + +pub fn SYMBOL() -> ByteArray { + "KUDOS" +} + +pub fn setup() -> ContractAddress { + let mut calldata: Array = array![]; + calldata.append_serde(NAME()); + calldata.append_serde(SYMBOL()); + calldata.append_serde(OWNER()); + + declare_deploy("Kudos", calldata) +} + +pub fn declare_deploy(contract_name: ByteArray, calldata: Array) -> ContractAddress { + let contract = declare(contract_name).unwrap().contract_class(); + let (contract_address, _) = contract.deploy(@calldata).unwrap(); + contract_address +} + +pub trait SerializedAppend { + fn append_serde(ref self: Array, value: T); +} + +impl SerializedAppendImpl, impl TDrop: Drop> of SerializedAppend { + fn append_serde(ref self: Array, value: T) { + value.serialize(ref self); + } +} diff --git a/contracts/src/tests/utils/sig.py b/scripts/sig.py similarity index 100% rename from contracts/src/tests/utils/sig.py rename to scripts/sig.py diff --git a/contracts/src/tests/utils/sso_credentials_type.json b/scripts/sso_credentials_type.json similarity index 100% rename from contracts/src/tests/utils/sso_credentials_type.json rename to scripts/sso_credentials_type.json