From 00aec266d3eb7b5a195da3d16310fb20e3f4cc9b Mon Sep 17 00:00:00 2001 From: jaykayudo Date: Thu, 16 Jan 2025 11:14:10 +0100 Subject: [PATCH 1/3] chore: add vault contract --- contracts/src/interfaces/ierc20.cairo | 19 ++ contracts/src/interfaces/ivault.cairo | 23 +++ contracts/src/lib.cairo | 27 +-- contracts/src/types.cairo | 13 ++ contracts/src/vault.cairo | 275 ++++++++++++++++++++++++++ 5 files changed, 336 insertions(+), 21 deletions(-) create mode 100644 contracts/src/interfaces/ierc20.cairo create mode 100644 contracts/src/interfaces/ivault.cairo create mode 100644 contracts/src/types.cairo create mode 100644 contracts/src/vault.cairo diff --git a/contracts/src/interfaces/ierc20.cairo b/contracts/src/interfaces/ierc20.cairo new file mode 100644 index 0000000..64c9d9d --- /dev/null +++ b/contracts/src/interfaces/ierc20.cairo @@ -0,0 +1,19 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IERC20 { + fn name(self: @TContractState) -> ByteArray; + fn symbol(self: @TContractState) -> ByteArray; + fn decimals(self: @TContractState) -> u8; + + fn total_supply(self: @TContractState) -> u256; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + + fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; +} diff --git a/contracts/src/interfaces/ivault.cairo b/contracts/src/interfaces/ivault.cairo new file mode 100644 index 0000000..4e10156 --- /dev/null +++ b/contracts/src/interfaces/ivault.cairo @@ -0,0 +1,23 @@ +use core::starknet::ContractAddress; +use core::starknet::ClassHash; +use win_saved::types::YieldSourceData; + +#[derive(Drop, Serde)] +pub struct WinnerStruct { + pub address: ContractAddress, + pub date: u64, + pub amount: u256 +} + +#[starknet::interface] +pub trait IVault { + fn deposit(ref self: TContractState, amount: u256); + fn withdraw(ref self: TContractState, amount: u256); + fn draw(ref self: TContractState, random_value: u32); + fn get_total_assets(self: @TContractState) -> u256; + fn get_yield_source_data(self: @TContractState) -> YieldSourceData; + fn get_recent_winners(self: @TContractState) -> Array; + fn pause(ref self: TContractState); + fn unpause(ref self: TContractState); + fn upgrade(ref self: TContractState, new_class_hash: ClassHash); +} diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index c55f19b..462a29e 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -1,25 +1,10 @@ -fn main() -> u32 { - fib(16) -} +pub mod types; -fn fib(mut n: u32) -> u32 { - let mut a: u32 = 0; - let mut b: u32 = 1; - while n != 0 { - n = n - 1; - let temp = b; - b = a + b; - a = temp; - }; - a +pub mod interfaces { + pub mod ivault; + pub mod ierc20; } -#[cfg(test)] -mod tests { - use super::fib; - #[test] - fn it_works() { - assert(fib(16) == 987, 'it works!'); - } -} +pub mod vault; + diff --git a/contracts/src/types.cairo b/contracts/src/types.cairo new file mode 100644 index 0000000..66cd115 --- /dev/null +++ b/contracts/src/types.cairo @@ -0,0 +1,13 @@ +use starknet::ContractAddress; + +#[derive(Copy, Drop, Serde)] +pub struct YieldSourceData { + pub lending_accumulator: felt252, + pub deposit_limit: felt252, +} + +#[derive(Copy, Drop, Serde)] +pub struct UserBalanceStruct { + pub address: ContractAddress, + pub amount: u256, +} diff --git a/contracts/src/vault.cairo b/contracts/src/vault.cairo new file mode 100644 index 0000000..22529ec --- /dev/null +++ b/contracts/src/vault.cairo @@ -0,0 +1,275 @@ +#[starknet::contract] +pub mod Vault { + use core::num::traits::Zero; + use core::starknet::{ + ContractAddress, get_caller_address, get_contract_address, get_block_timestamp, ClassHash + }; + use core::starknet::storage::{ + StoragePointerReadAccess, StoragePointerWriteAccess, StoragePathEntry ,Vec, VecTrait, MutableVecTrait + }; + use openzeppelin_access::ownable::OwnableComponent; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_security::{PausableComponent, ReentrancyGuardComponent}; + use openzeppelin_upgrades::upgradeable::UpgradeableComponent; + use win_saved::interfaces::{ + ivault::{IVault, WinnerStruct}, ierc20::{IERC20Dispatcher, IERC20DispatcherTrait} + }; + use win_saved::types::{YieldSourceData, UserBalanceStruct}; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: PausableComponent, storage: pausable, event: PausableEvent); + component!( + path: ReentrancyGuardComponent, storage: reentrancyguard, event: ReentrancyGuardEvent + ); + component!(path: UpgradeableComponent, storage: upgradable, event: UpgradableEvent); + + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[abi(embed_v0)] + impl PausableImpl = PausableComponent::PausableImpl; + impl PausableInternalImpl = PausableComponent::InternalImpl; + + impl ReentrancyGuardInternalImpl = ReentrancyGuardComponent::InternalImpl; + + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + yield_token: ContractAddress, + yield_source: ContractAddress, + winners: Vec, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + pausable: PausableComponent::Storage, + #[substorage(v0)] + reentrancyguard: ReentrancyGuardComponent::Storage, + #[substorage(v0)] + upgradable: UpgradeableComponent::Storage, + } + + #[starknet::storage_node] + struct Winner { + address: ContractAddress, + amount: u256, + date: u64, + claimed: bool, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + UserDepositedEvent: UserDepositedEvent, + UserWithdrawalEvent: UserWithdrawalEvent, + PrizeDrawEvent: PrizeDrawEvent, + UserWinEvent: UserWinEvent, + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + PausableEvent: PausableComponent::Event, + #[flat] + ReentrancyGuardEvent: ReentrancyGuardComponent::Event, + #[flat] + UpgradableEvent: UpgradeableComponent::Event, + } + + #[derive(Drop, starknet::Event)] + struct UserDepositedEvent { + #[key] + user: ContractAddress, + amount: u256, + date: u64, + } + #[derive(Drop, starknet::Event)] + struct UserWithdrawalEvent { + #[key] + user: ContractAddress, + amount: u256, + date: u64, + } + #[derive(Drop, starknet::Event)] + struct PrizeDrawEvent { + date: u64, + } + #[derive(Drop, starknet::Event)] + struct UserWinEvent { + #[key] + user: ContractAddress, + amount: u256, + date: u64, + } + + #[constructor] + fn constructor( + ref self: ContractState, yield_token: ContractAddress, yield_source: ContractAddress + ) { + let owner = get_caller_address(); + self.ownable.initializer(owner); + // get yield token name for creater vault token (share) name + let erc20_dispatcher = IERC20Dispatcher { contract_address: yield_token }; + let token_name = format!("WinSaved_{}", erc20_dispatcher.name()); + let token_symbol = format!("ws{}", erc20_dispatcher.symbol()); + self.yield_token.write(yield_token); + self.yield_source.write(yield_source); + self.erc20.initializer(token_name, token_symbol); + } + + #[abi(embed_v0)] + impl VaultImpl of IVault { + fn deposit(ref self: ContractState, amount: u256) { + // check paused + self.pausable.assert_not_paused(); + self.reentrancyguard.start(); + let caller = get_caller_address(); + let contract_address = get_contract_address(); + //transfer token from user + let erc20_address = self.yield_token.read(); + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + erc20_dispatcher.transfer_from(caller, contract_address, amount); + // make call to yield source to deposit + // mint token + self.erc20.mint(caller, amount); + let current_date_time = get_block_timestamp(); + self.emit(UserDepositedEvent { user: caller, amount: amount, date: current_date_time }); + self.reentrancyguard.end(); + } + fn pause(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.pausable.pause(); + } + fn unpause(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.pausable.unpause(); + } + fn withdraw(ref self: ContractState, amount: u256) { + self.pausable.assert_not_paused(); + self.reentrancyguard.start(); + let caller = get_caller_address(); + let contract_address = get_contract_address(); + // get user vault token balance and assert if balance is equal to amount + let user_balance = self.erc20.balance_of(caller); + assert!(user_balance >= amount, "Insufficient balance"); + // make call to yield source to withdraw amount + //transfer token to user + let erc20_address = self.yield_token.read(); + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + // burn token + self.erc20.burn(caller, amount); + erc20_dispatcher.transfer(caller, amount); + + let current_date_time = get_block_timestamp(); + self + .emit( + UserWithdrawalEvent { user: caller, amount: amount, date: current_date_time } + ); + self.reentrancyguard.end(); + } + fn draw(ref self: ContractState, random_value: u32) { + self.ownable.assert_only_owner(); + // check whether last draw time is greater than draw duration + + // check whether yield is available + let users = self.get_all_users(); + let sorted_holders = self.sort_users_by_balance(users); + // get the top half for randomization + let top_average: u32 = sorted_holders.len() / 2; + let mut top_average_holders: Array = array![]; + for idx in 0..top_average { + top_average_holders.append(*sorted_holders.at(idx)); + }; + + let rand_num = random_value % top_average_holders.len(); + let winner = *top_average_holders.at(rand_num.into()); + let win_amount = self.disperse_prize_to_winner(winner); + self.emit(PrizeDrawEvent { date: get_block_timestamp() }); + self + .emit( + UserWinEvent { user: winner, amount: win_amount, date: get_block_timestamp() } + ); + } + fn get_total_assets(self: @ContractState) -> u256 { + // access total assets form yield source + 10 + } + fn get_yield_source_data(self: @ContractState) -> YieldSourceData { + // access yield source connector interface + // change later when connector is created + YieldSourceData { lending_accumulator: 10, deposit_limit: 10, } + } + fn get_recent_winners(self: @ContractState) -> Array { + let mut winners_array: Array = array![]; + for index in 0 + ..self + .winners + .len() { + let current = self.winners.at(index); + winners_array + .append( + WinnerStruct { + address: current.address.read(), + amount: current.amount.read(), + date: current.date.read() + } + ); + }; + winners_array + } + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + assert!(new_class_hash.is_non_zero(), "Zero ClassHash detected.. aborting"); + self.upgradable.upgrade(new_class_hash); + } + } + + #[generate_trait] + impl InternalImpl of InternalImplTrait { + fn get_all_users(self: @ContractState) -> Array { + // query event for all users that has deposited and check balance of vault token + // return users with balance greater than zero + let all_holders: Array = array![]; + all_holders + } + /// Sort users by vault token balance + fn sort_users_by_balance( + self: @ContractState, users: Array + ) -> Array { + let sorted_holders: Array = array![]; + sorted_holders + } + fn disperse_prize_to_winner(ref self: ContractState, user: ContractAddress) -> u256 { + // functionality to disperse yield to winner + // liquidate yield from yield source to contract and transfer to winner + let yield_amount: u256 = + 1000; // access yield source after liquidation for the exact amount + // transfer yield amount to winner via dispatcher + + // add winner to winner struct + self + .winners + .append() + .write( + Winner { + address: user, + amount: yield_amount, + date: get_block_timestamp(), + claimed: true + } + ); + let erc20_address = self.yield_token.read(); + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + erc20_dispatcher.transfer(user, yield_amount); + + yield_amount + } + } +} + From 0511e9e2ea7a99e15858cb9c7c33d55416588e26 Mon Sep 17 00:00:00 2001 From: jaykayudo Date: Fri, 17 Jan 2025 21:42:31 +0100 Subject: [PATCH 2/3] fix: fix errors in contract --- contracts/src/interfaces/ivault.cairo | 2 +- contracts/src/vault.cairo | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/src/interfaces/ivault.cairo b/contracts/src/interfaces/ivault.cairo index 04f6432..0d6552f 100644 --- a/contracts/src/interfaces/ivault.cairo +++ b/contracts/src/interfaces/ivault.cairo @@ -11,7 +11,7 @@ pub struct WinnerStruct { #[starknet::interface] pub trait IVault { - fn get_vault_details(self: @ContractState) -> VaultDetails; + fn get_vault_details(self: @TContractState) -> VaultDetails; fn deposit(ref self: TContractState, amount: u256); fn withdraw(ref self: TContractState, amount: u256); fn draw(ref self: TContractState, random_value: u32); diff --git a/contracts/src/vault.cairo b/contracts/src/vault.cairo index f6fa320..bcd58c5 100644 --- a/contracts/src/vault.cairo +++ b/contracts/src/vault.cairo @@ -5,7 +5,7 @@ pub mod Vault { ContractAddress, get_caller_address, get_contract_address, get_block_timestamp, ClassHash }; use core::starknet::storage::{ - StoragePointerReadAccess, StoragePointerWriteAccess, StoragePathEntry, Vec, VecTrait, + StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait, MutableVecTrait }; use openzeppelin_access::ownable::OwnableComponent; @@ -30,6 +30,8 @@ pub mod Vault { impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; impl ERC20InternalImpl = ERC20Component::InternalImpl; + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; impl OwnableInternalImpl = OwnableComponent::InternalImpl; #[abi(embed_v0)] @@ -145,7 +147,7 @@ pub mod Vault { let total_yield = yield_source_dispatcher.get_yield_generated(yield_token_address); VaultDetails { yield_token: TokenData { - symbol: erc20_dispatcher.symbol(), address: yield_token_address + symbol: yield_erc20_dispatcher.symbol(), address: yield_token_address }, vault_token: TokenData { symbol: self.erc20.symbol(), address: get_contract_address() @@ -189,7 +191,6 @@ pub mod Vault { self.pausable.assert_not_paused(); self.reentrancyguard.start(); let caller = get_caller_address(); - let contract_address = get_contract_address(); // get user vault token balance and assert if balance is equal to amount let user_balance = self.erc20.balance_of(caller); assert!(user_balance >= amount, "Insufficient balance"); @@ -243,7 +244,7 @@ pub mod Vault { let yield_source_dispatcher = IYieldSourceDispatcher { contract_address: self.yield_source.read() }; - yield_source_dispatcher.get_total_value_locked() + yield_source_dispatcher.get_total_value_locked(get_contract_address()) } fn get_yield_source_data(self: @ContractState) -> YieldSourceData { // access yield source connector interface From d427935974a6b6d53cb90977a3a18b92495a5634 Mon Sep 17 00:00:00 2001 From: jaykayudo Date: Fri, 17 Jan 2025 21:56:17 +0100 Subject: [PATCH 3/3] fix: fix compiler error --- contracts/src/types.cairo | 4 ++-- contracts/src/vault.cairo | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/src/types.cairo b/contracts/src/types.cairo index b5ebd37..5ef925d 100644 --- a/contracts/src/types.cairo +++ b/contracts/src/types.cairo @@ -12,13 +12,13 @@ pub struct UserBalanceStruct { pub amount: u256, } -#[derive(Copy, Drop, Serde)] +#[derive(Drop, Serde)] pub struct TokenData { pub symbol: ByteArray, pub address: ContractAddress } -#[derive(Copy, Drop, Serde)] +#[derive(Drop, Serde)] pub struct VaultDetails { pub yield_token: TokenData, pub vault_token: TokenData, diff --git a/contracts/src/vault.cairo b/contracts/src/vault.cairo index bcd58c5..69f3253 100644 --- a/contracts/src/vault.cairo +++ b/contracts/src/vault.cairo @@ -5,8 +5,7 @@ pub mod Vault { ContractAddress, get_caller_address, get_contract_address, get_block_timestamp, ClassHash }; use core::starknet::storage::{ - StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait, - MutableVecTrait + StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait, MutableVecTrait }; use openzeppelin_access::ownable::OwnableComponent; use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl};