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

Delegated token #49

Closed
wants to merge 15 commits into from
Closed
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
257 changes: 257 additions & 0 deletions src/delegated_token.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
use starknet::{ContractAddress};

#[starknet::interface]
pub trait IDelegatedToken<TContractState> {
// 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<ContractAddress, ContractAddress>,
balances: LegacyMap<ContractAddress, u128>,
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<ContractState> {
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<ContractState> {
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<ContractState> {
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());
}
}
}
111 changes: 111 additions & 0 deletions src/delegated_token_test.cairo
Original file line number Diff line number Diff line change
@@ -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<felt252> = 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);
}
2 changes: 1 addition & 1 deletion src/governor_test.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/interfaces/erc20.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use starknet::{ContractAddress};

#[starknet::interface]
pub(crate) trait IERC20<TContractState> {
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;
Expand All @@ -10,3 +11,10 @@ pub(crate) trait IERC20<TContractState> {
) -> bool;
fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool;
}

#[starknet::interface]
pub(crate) trait IERC20Metadata<TContractState> {
fn name(self: @TContractState) -> felt252;
fn symbol(self: @TContractState) -> felt252;
fn decimals(self: @TContractState) -> u8;
}
Loading
Loading