diff --git a/src/delegated_token.cairo b/src/delegated_token.cairo new file mode 100644 index 0000000..f7195c8 --- /dev/null +++ b/src/delegated_token.cairo @@ -0,0 +1,257 @@ +use starknet::{ContractAddress}; + +#[starknet::interface] +pub trait IDelegatedToken { + // Returns the address of the staker that this staked token wrapper uses + fn get_staker(self: @TContractState) -> ContractAddress; + + // Get the address to whom the owner is delegated to + fn get_delegated_to(self: @TContractState, owner: ContractAddress) -> ContractAddress; + + // Delegates any staked tokens from the caller to the owner + fn delegate(ref self: TContractState, to: ContractAddress); + + // Transfers the approved amount of the staked token to this contract and mints an ERC20 representing the staked amount + fn deposit(ref self: TContractState); + + // Same as above but with a specified amount + fn deposit_amount(ref self: TContractState, amount: u128); + + // Withdraws the entire staked balance from the contract from the caller + fn withdraw(ref self: TContractState); + + // Withdraws the specified amount of token from the contract from the caller + fn withdraw_amount(ref self: TContractState, amount: u128); +} + +#[starknet::contract] +pub mod DelegatedToken { + use core::num::traits::zero::{Zero}; + use core::option::{OptionTrait}; + use core::traits::{Into, TryInto}; + use governance::interfaces::erc20::{ + IERC20, IERC20Metadata, IERC20MetadataDispatcher, IERC20MetadataDispatcherTrait, + IERC20Dispatcher, IERC20DispatcherTrait + }; + use governance::staker::{IStakerDispatcher, IStakerDispatcherTrait}; + use starknet::{ + get_caller_address, get_contract_address, get_block_timestamp, + storage_access::{StorePacking} + }; + use super::{IDelegatedToken, ContractAddress}; + + #[storage] + struct Storage { + staker: IStakerDispatcher, + delegated_to: LegacyMap, + balances: LegacyMap, + allowances: LegacyMap<(ContractAddress, ContractAddress), u128>, + total_supply: u128, + name: felt252, + symbol: felt252, + } + + #[constructor] + fn constructor( + ref self: ContractState, staker: IStakerDispatcher, name: felt252, symbol: felt252 + ) { + self.staker.write(staker); + self.name.write(name); + self.symbol.write(symbol); + } + + #[derive(starknet::Event, PartialEq, Debug, Drop)] + pub struct Deposit { + pub from: ContractAddress, + pub amount: u128, + } + + #[derive(starknet::Event, PartialEq, Debug, Drop)] + pub struct Withdrawal { + pub from: ContractAddress, + pub amount: u128, + } + + + #[derive(starknet::Event, PartialEq, Debug, Drop)] + pub struct Delegation { + pub from: ContractAddress, + pub to: ContractAddress, + } + + #[derive(starknet::Event, PartialEq, Debug, Drop)] + pub struct Transfer { + pub from: ContractAddress, + pub to: ContractAddress, + pub amount: u256, + } + #[derive(starknet::Event, PartialEq, Debug, Drop)] + pub struct Approval { + pub owner: ContractAddress, + pub spender: ContractAddress, + pub amount: u256, + } + + #[derive(starknet::Event, Drop)] + #[event] + enum Event { + Deposit: Deposit, + Withdrawal: Withdrawal, + Delegation: Delegation, + Transfer: Transfer, + Approval: Approval, + } + + #[generate_trait] + impl InternalMethods of InternalMethodsTrait { + fn move_delegates( + self: @ContractState, from: ContractAddress, to: ContractAddress, amount: u128 + ) { + let staker = self.staker.read(); + let token = IERC20Dispatcher { contract_address: staker.get_token() }; + + staker.withdraw_amount(from, get_contract_address(), amount); + assert(token.approve(staker.contract_address, amount.into()), 'APPROVE_FAILED'); + staker.stake(to); + } + } + + #[abi(embed_v0)] + impl DelegatedTokenERC20Metadata of IERC20Metadata { + fn name(self: @ContractState) -> felt252 { + self.name.read() + } + fn symbol(self: @ContractState) -> felt252 { + self.symbol.read() + } + fn decimals(self: @ContractState) -> u8 { + IERC20MetadataDispatcher { + contract_address: IStakerDispatcher { contract_address: self.get_staker() } + .get_token() + } + .decimals() + } + } + + #[abi(embed_v0)] + impl DelegatedTokenERC20 of IERC20 { + fn totalSupply(self: @ContractState) -> u256 { + self.total_supply.read().into() + } + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + self.balances.read(account).into() + } + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + self.allowances.read((owner, spender)).into() + } + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + let from = get_caller_address(); + let balance = self.balances.read(from); + assert(balance.into() >= amount, 'INSUFFICIENT_BALANCE'); + let small_amount: u128 = amount.try_into().unwrap(); + self.balances.write(from, balance - small_amount); + self.balances.write(recipient, self.balances.read(recipient) + small_amount); + self.emit(Transfer { from, to: recipient, amount: amount }); + self + .move_delegates( + self.get_delegated_to(from), self.get_delegated_to(recipient), small_amount + ); + true + } + fn transferFrom( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + let spender = get_caller_address(); + let allowance = self.allowances.read((sender, spender)); + assert(allowance.into() >= amount, 'INSUFFICIENT_ALLOWANCE'); + let small_amount: u128 = amount.try_into().unwrap(); + self.allowances.write((sender, spender), allowance - small_amount); + + let balance = self.balances.read(sender); + + self.balances.write(sender, balance - small_amount); + self.balances.write(recipient, self.balances.read(recipient) + small_amount); + self.emit(Transfer { from: sender, to: recipient, amount: amount }); + + self + .move_delegates( + self.get_delegated_to(sender), self.get_delegated_to(recipient), small_amount + ); + + true + } + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + let owner = get_caller_address(); + let small_amount: u128 = amount.try_into().expect('AMOUNT_EXCEEDS_U128'); + self.allowances.write((owner, spender), small_amount); + self.emit(Approval { owner, spender, amount }); + true + } + } + + #[abi(embed_v0)] + impl DelegatedTokenImpl of IDelegatedToken { + fn get_staker(self: @ContractState) -> ContractAddress { + self.staker.read().contract_address + } + + fn get_delegated_to(self: @ContractState, owner: ContractAddress) -> ContractAddress { + self.delegated_to.read(owner) + } + + + fn delegate(ref self: ContractState, to: ContractAddress) { + let caller = get_caller_address(); + let previous_delegated_to = self.delegated_to.read(caller); + self.delegated_to.write(caller, to); + self + .move_delegates( + previous_delegated_to, to, self.balanceOf(caller).try_into().unwrap() + ); + self.emit(Delegation { from: caller, to }); + } + + fn deposit_amount(ref self: ContractState, amount: u128) { + let staker = self.staker.read(); + let token = IERC20Dispatcher { contract_address: staker.get_token() }; + let caller = get_caller_address(); + assert( + token.transferFrom(caller, get_contract_address(), amount.into()), + 'TRANSFER_FROM_FAILED' + ); + assert(token.approve(staker.contract_address, amount.into()), 'APPROVE_FAILED'); + staker.stake(self.delegated_to.read(caller)); + + self.balances.write(caller, self.balances.read(caller) + amount); + self.total_supply.write(self.total_supply.read() + amount); + } + + fn deposit(ref self: ContractState) { + self + .deposit_amount( + IERC20Dispatcher { contract_address: self.staker.read().get_token() } + .allowance(get_caller_address(), get_contract_address()) + .try_into() + .unwrap() + ); + } + + fn withdraw_amount(ref self: ContractState, amount: u128) { + let caller = get_caller_address(); + + self.balances.write(caller, self.balances.read(caller) - amount); + self.total_supply.write(self.total_supply.read() - amount); + + self.staker.read().withdraw_amount(self.delegated_to.read(caller), caller, amount); + } + + fn withdraw(ref self: ContractState) { + self.withdraw_amount(self.balanceOf(get_caller_address()).try_into().unwrap()); + } + } +} diff --git a/src/delegated_token_test.cairo b/src/delegated_token_test.cairo new file mode 100644 index 0000000..6cd6053 --- /dev/null +++ b/src/delegated_token_test.cairo @@ -0,0 +1,111 @@ +use core::array::SpanTrait; +use core::array::{ArrayTrait}; +use core::num::traits::zero::{Zero}; +use core::option::{OptionTrait}; + +use core::result::{Result, ResultTrait}; +use core::serde::Serde; +use core::traits::{TryInto}; +use governance::delegated_token::{ + IDelegatedToken, IDelegatedTokenDispatcher, IDelegatedTokenDispatcherTrait, DelegatedToken +}; + +use governance::execution_state::{ExecutionState}; +use governance::governor_test::{advance_time}; +use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; +use governance::staker::{IStakerDispatcher, IStakerDispatcherTrait}; +use governance::staker_test::{setup as setup_staker}; +use starknet::account::{Call}; +use starknet::{ + get_contract_address, syscalls::deploy_syscall, ClassHash, contract_address_const, + ContractAddress, get_block_timestamp, + testing::{set_block_timestamp, set_contract_address, pop_log} +}; + + +fn deploy(staker: IStakerDispatcher, name: felt252, symbol: felt252) -> IDelegatedTokenDispatcher { + let mut constructor_args: Array = ArrayTrait::new(); + Serde::serialize(@(staker, name, symbol), ref constructor_args); + + let (address, _) = deploy_syscall( + DelegatedToken::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_args.span(), true + ) + .expect('DEPLOY_GV_FAILED'); + return IDelegatedTokenDispatcher { contract_address: address }; +} + +fn setup() -> (IStakerDispatcher, IERC20Dispatcher, IDelegatedTokenDispatcher) { + let (staker, token) = setup_staker(1000000); + let delegated_token = deploy(staker, 'Staked Token', 'vSTT'); + + (staker, token, delegated_token) +} + + +#[test] +fn test_setup() { + let (staker, token, dt) = setup(); + + assert_eq!(dt.get_staker(), staker.contract_address); + assert_eq!( + IStakerDispatcher { contract_address: dt.get_staker() }.get_token(), token.contract_address + ); +} + + +#[test] +fn test_deposit() { + let (staker, token, dt) = setup(); + token.approve(dt.contract_address, 100); + let delegatee = contract_address_const::<'delegate'>(); + dt.delegate(delegatee); + dt.deposit(); + assert_eq!(staker.get_delegated(delegatee), 100); + assert_eq!( + IERC20Dispatcher { contract_address: dt.contract_address } + .balanceOf(get_contract_address()), + 100 + ); + assert_eq!(IERC20Dispatcher { contract_address: dt.contract_address }.totalSupply(), 100); +} + +#[test] +fn test_deposit_then_transfer() { + let (staker, token, dt) = setup(); + token.approve(dt.contract_address, 100); + let delegatee = contract_address_const::<'delegate'>(); + let recipient = contract_address_const::<'recipient'>(); + dt.delegate(delegatee); + dt.deposit(); + IERC20Dispatcher { contract_address: dt.contract_address }.transfer(recipient, 75); + assert_eq!(staker.get_delegated(delegatee), 25); + assert_eq!(staker.get_delegated(Zero::zero()), 75); + assert_eq!(IERC20Dispatcher { contract_address: dt.contract_address }.totalSupply(), 100); +} + +#[test] +fn test_deposit_then_delegate() { + let (staker, token, dt) = setup(); + token.approve(dt.contract_address, 100); + let delegatee = contract_address_const::<'delegate'>(); + dt.deposit(); + assert_eq!(staker.get_delegated(Zero::zero()), 100); + assert_eq!(staker.get_delegated(delegatee), 0); + + dt.delegate(delegatee); + assert_eq!(staker.get_delegated(Zero::zero()), 0); + assert_eq!(staker.get_delegated(delegatee), 100); +} + +#[test] +fn test_withdraw() { + let (staker, token, dt) = setup(); + token.approve(dt.contract_address, 100); + let delegatee = contract_address_const::<'delegate'>(); + dt.delegate(delegatee); + dt.deposit(); + dt.withdraw(); + assert_eq!(staker.get_delegated(delegatee), 0); + assert_eq!(staker.get_delegated(Zero::zero()), 0); + assert_eq!(IERC20Dispatcher { contract_address: dt.contract_address }.totalSupply(), 0); +} diff --git a/src/governor_test.cairo b/src/governor_test.cairo index 93e62e7..f5dda64 100644 --- a/src/governor_test.cairo +++ b/src/governor_test.cairo @@ -47,7 +47,7 @@ pub(crate) fn anyone() -> ContractAddress { 'anyone'.try_into().unwrap() } -fn advance_time(by: u64) -> u64 { +pub(crate) fn advance_time(by: u64) -> u64 { let next = get_block_timestamp() + by; set_block_timestamp(next); next diff --git a/src/interfaces/erc20.cairo b/src/interfaces/erc20.cairo index 34cdc22..ab7f16c 100644 --- a/src/interfaces/erc20.cairo +++ b/src/interfaces/erc20.cairo @@ -2,6 +2,7 @@ use starknet::{ContractAddress}; #[starknet::interface] pub(crate) trait IERC20 { + fn totalSupply(self: @TContractState) -> u256; fn balanceOf(self: @TContractState, account: ContractAddress) -> u256; fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; @@ -10,3 +11,10 @@ pub(crate) trait IERC20 { ) -> bool; fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; } + +#[starknet::interface] +pub(crate) trait IERC20Metadata { + fn name(self: @TContractState) -> felt252; + fn symbol(self: @TContractState) -> felt252; + fn decimals(self: @TContractState) -> u8; +} diff --git a/src/lib.cairo b/src/lib.cairo index 286e6ad..fa517d5 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -6,6 +6,9 @@ pub(crate) mod airdrop_test; pub mod call_trait; #[cfg(test)] pub(crate) mod call_trait_test; +pub mod delegated_token; +#[cfg(test)] +pub mod delegated_token_test; pub mod execution_state; #[cfg(test)] pub(crate) mod execution_state_test; diff --git a/src/test/test_token.cairo b/src/test/test_token.cairo index 888d82a..6cd0476 100644 --- a/src/test/test_token.cairo +++ b/src/test/test_token.cairo @@ -11,6 +11,7 @@ pub(crate) mod TestToken { struct Storage { balances: LegacyMap, allowances: LegacyMap<(ContractAddress, ContractAddress), u256>, + total_supply: u256, } #[derive(starknet::Event, PartialEq, Debug, Drop)] @@ -29,11 +30,15 @@ pub(crate) mod TestToken { #[constructor] fn constructor(ref self: ContractState, recipient: ContractAddress, amount: u256) { self.balances.write(recipient, amount); - self.emit(Transfer { from: Zero::zero(), to: recipient, value: amount }) + self.emit(Transfer { from: Zero::zero(), to: recipient, value: amount }); + self.total_supply.write(amount); } #[abi(embed_v0)] impl IERC20Impl of IERC20 { + fn totalSupply(self: @ContractState) -> u256 { + self.total_supply.read() + } fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { self.balances.read(account).into() }