Skip to content

Commit

Permalink
Add staking
Browse files Browse the repository at this point in the history
  • Loading branch information
tensojka committed Jun 11, 2024
1 parent 5a013ea commit 47dd0cc
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 50 deletions.
1 change: 0 additions & 1 deletion Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ cairo-version = "2.6.3"
starknet = ">=2.0.0"
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.10.0" }

# can be fixed by doing import super::testing from tests
[dev-dependencies]
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.23.0" }

Expand Down
3 changes: 3 additions & 0 deletions src/constants.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ const PROPOSAL_VOTING_SECONDS: u64 = consteval_int!(60 * 60 * 24 * 7);
const USDC_ADDRESS: felt252 = 0x53c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8;
const ETH_ADDRESS: felt252 = 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7;
const BTC_ADDRESS: felt252 = 0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac;


const UNLOCK_DATE: u64 = 1722470399; // GMT Wed Jul 31 2024 23:59:59 GMT+0000 – for staking
1 change: 1 addition & 0 deletions src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod treasury_types {
mod constants;
mod contract;
mod discussion;
mod staking;
mod merkle_tree;
mod proposals;
mod token;
Expand Down
159 changes: 139 additions & 20 deletions src/staking.cairo
Original file line number Diff line number Diff line change
@@ -1,31 +1,150 @@
#[starknet::interface]
trait IAirdrop<TContractState> {
fn stake(
ref self: TContractState, length: u64, amount: u128
) -> u32; // returns stake ID
fn unstake(
ref self: TContractState, id: u32
);
trait IStaking<TContractState> {
fn stake(ref self: TContractState, length: u64, amount: u128) -> u32; // returns stake ID
fn unstake(ref self: TContractState, id: u32);
fn unstake_airdrop(ref self: TContractState, amount: u128);


// owner only
// set_curve_point
fn set_curve_point(ref self: TContractState, length: u64, conversion_rate: u16);
}

#[starknet::component]
mod staking {
use cubit::f128;
use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address};
use konoha::traits::{
get_governance_token_address_self, IERC20Dispatcher, IERC20DispatcherTrait
};
use konoha::constants::UNLOCK_DATE;

#[storage]
struct Storage {
stake: LegacyMap::<(ContractAddress, u32), (u128, u128, u64, u64)>, // STAKE(address, ID) → (amount staked, amount veCARM, start date, length of stake)
curve: LegacyMap::<u64, f128> // length of stake > CARM to veCARM conversion rate (in cubit f128)
stake: LegacyMap::<
(ContractAddress, u32), (u128, u128, u64, u64)
>, // STAKE(address, ID) → (amount staked, amount voting token, start date, length of stake)
curve: LegacyMap::<
u64, u16
>, // length of stake > CARM to veCARM conversion rate (conversion rate is expressed in % – 2:1 is 200)
floating_token_address: ContractAddress
}

#[embeddable_as(StakingImpl)]
impl Staking<
TContractState, +HasComponent<TContractState>,
> of super::IStaking<ComponentState<TContractState>> {
fn stake(ref self: ComponentState<TContractState>, length: u64, amount: u128) -> u32 {
let caller = get_caller_address();

assert(amount != 0, 'amount to stake is zero');
let conversion_rate: u16 = self.curve.read(length);
assert(conversion_rate != 0, 'unsupported stake length');

let floating_token = IERC20Dispatcher {
contract_address: self.floating_token_address.read()
};
floating_token.transfer_from(caller, get_contract_address(), amount.into());

let amount_voting_token = (amount * conversion_rate.into()) / 100;
let free_id = self.get_free_stake_id(caller);

self
.stake
.write(
(caller, free_id), (amount, amount_voting_token, get_block_timestamp(), length)
);

let voting_token = IERC20Dispatcher {
contract_address: get_governance_token_address_self()
};
voting_token.mint(caller, amount_voting_token.into());

free_id
}

fn unstake(ref self: ComponentState<TContractState>, id: u32) {
let caller = get_caller_address();
let res: (u128, u128, u64, u64) = self.stake.read((caller, id));
let (amount_staked, amount_voting_token, start_date, length) = res;

assert(amount_staked != 0, 'no stake found, check stake id');
let unlock_date = start_date + length;
assert(get_block_timestamp() > unlock_date, 'unlock time not yet reached');

let voting_token = IERC20Dispatcher {
contract_address: get_governance_token_address_self()
};
voting_token.burn(caller, amount_voting_token.into());

let floating_token = IERC20Dispatcher {
contract_address: self.floating_token_address.read()
};
// user gets back the same amount of tokens they put in.
// the payoff is in holding voting tokens, which make the user eligible for distributions of protocol revenue
// works for tokens with fixed max float
floating_token.transfer(caller, amount_staked.into());
}

fn unstake_airdrop(ref self: ComponentState<TContractState>, amount: u128) {
assert(get_block_timestamp() > UNLOCK_DATE, 'tokens not yet unlocked');

let caller = get_caller_address();

let total_staked = self.get_total_staked_accounted(caller); // manually staked tokens
let voting_token = IERC20Dispatcher {
contract_address: get_governance_token_address_self()
};
let voting_token_balance = voting_token.balance_of(caller).try_into().unwrap();
assert(
voting_token_balance > total_staked, 'no extra tokens to unstake'
); // potentially unnecessary (underflow checks), but provides for a better error message
let to_unstake = voting_token_balance - total_staked;

// burn voting token, mint floating token
let voting_token = IERC20Dispatcher {
contract_address: get_governance_token_address_self()
};
voting_token.burn(caller, to_unstake.into());
let floating_token = IERC20Dispatcher {
contract_address: self.floating_token_address.read()
};
floating_token.mint(caller, to_unstake.into());
}

fn set_curve_point(
ref self: ComponentState<TContractState>, length: u64, conversion_rate: u16
) {
let caller = get_caller_address();
let myaddr = get_contract_address();
assert(caller == myaddr, 'can only call from proposal');
self.curve.write(length, conversion_rate);
}
}

// impl, etc, embeddable_as StakingImpl...
// fn stake()
// fn unstake(){
// here, keep in mind the special case where the user has veCARM and no corresponding entry in the stake storage var.
// what id to use? to be decided
//}
}
#[generate_trait]
impl InternalImpl<
TContractState, +HasComponent<TContractState>
> of InternalTrait<TContractState> {
fn get_free_stake_id(
self: @ComponentState<TContractState>, address: ContractAddress
) -> u32 {
self._get_free_stake_id(address, 0)
}

fn _get_free_stake_id(
self: @ComponentState<TContractState>, address: ContractAddress, id: u32
) -> u32 {
let res: (u128, u128, u64, u64) = self.stake.read((address, id));
let (newamt, _a, _b, _c) = res;
if (newamt == 0) {
id
} else {
self._get_free_stake_id(address, id + 1)
}
}

fn get_total_staked_accounted(
self: @ComponentState<TContractState>, address: ContractAddress
) -> u128 {
// to handle the special case where the user has been airdropped (locked) voting tokens
42
}
}
}
10 changes: 8 additions & 2 deletions src/traits.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,24 @@ fn get_amm_address_self() -> ContractAddress {

#[starknet::interface]
trait IERC20<TContractState> {
fn name(self: @TContractState) -> felt252;
fn symbol(self: @TContractState) -> felt252;
fn name(self: @TContractState) -> ByteArray;
fn symbol(self: @TContractState) -> ByteArray;
fn decimals(self: @TContractState) -> u8;
fn totalSupply(self: @TContractState) -> u256;
fn total_supply(self: @TContractState) -> u256;
fn balanceOf(self: @TContractState, account: ContractAddress) -> u256;
fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
fn transferFrom(
ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256
) -> bool;
fn transfer_from(
ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256
) -> bool;
fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool;
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256);
fn burn(ref self: TContractState, recipient: ContractAddress, amount: u256);
}

#[starknet::interface]
Expand Down
99 changes: 72 additions & 27 deletions src/voting_token.cairo
Original file line number Diff line number Diff line change
@@ -1,65 +1,73 @@
// This is the locked Cairo token.
// TODO upgradability

#[starknet::contract]
mod VotingToken {
use openzeppelin::token::erc20::interface::{IERC20, IERC20CamelOnly};
use openzeppelin::token::erc20::ERC20Component;
use starknet::ContractAddress;
use openzeppelin::token::erc20::erc20::ERC20Component::{InternalTrait, ERC20MetadataImpl};
use openzeppelin::access::ownable::ownable::OwnableComponent::InternalTrait as OwnableInternalTrait;
use openzeppelin::token::erc20::{ERC20Component};
use starknet::{ClassHash, ContractAddress};
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::access::ownable::interface::IOwnableTwoStep;
use konoha::traits::IERC20;
use openzeppelin::upgrades::upgradeable::UpgradeableComponent;
use openzeppelin::upgrades::interface::IUpgradeable;

component!(path: ERC20Component, storage: erc20, event: ERC20Event);
component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);

impl ERC20Impl = ERC20Component::ERC20Impl<ContractState>;
#[abi(embed_v0)]
impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl<ContractState>;
impl OwnableTwoStepImpl = OwnableComponent::OwnableTwoStepImpl<ContractState>;

impl InternalImpl = ERC20Component::InternalImpl<ContractState>;
impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl<ContractState>;

#[storage]
struct Storage {
#[substorage(v0)]
erc20: ERC20Component::Storage
erc20: ERC20Component::Storage,
#[substorage(v0)]
ownable: OwnableComponent::Storage,
#[substorage(v0)]
upgradeable: UpgradeableComponent::Storage,
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
ERC20Event: ERC20Component::Event
ERC20Event: ERC20Component::Event,
#[flat]
OwnableEvent: OwnableComponent::Event,
#[flat]
UpgradeableEvent: UpgradeableComponent::Event,
}

#[constructor]
fn constructor(ref self: ContractState, fixed_supply: u256, recipient: ContractAddress) {
let name = "Konoha Sepolia Deployment Test Token";
let symbol = "KONOHA";
fn constructor(ref self: ContractState, owner: ContractAddress) {
let name = "Konoha Sepolia Voting";
let symbol = "veKONOHA";

self.erc20.initializer(name, symbol);
self.erc20._mint(recipient, fixed_supply);
self.ownable.initializer(owner);
}

#[abi(embed_v0)]
impl VotingTokenCamelOnly of IERC20CamelOnly<ContractState> {
fn totalSupply(self: @ContractState) -> u256 {
self.erc20.total_supply()
impl VotingToken of IERC20<ContractState> {
// READ
fn name(self: @ContractState) -> ByteArray {
ERC20MetadataImpl::name(self)
}

fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 {
self.erc20.balance_of(account)
fn symbol(self: @ContractState) -> ByteArray {
ERC20MetadataImpl::symbol(self)
}

fn transferFrom(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: u256
) -> bool {
assert(false, 'token locked, unwrap first');
false
fn decimals(self: @ContractState) -> u8 {
18
}
}

#[abi(embed_v0)]
impl VotingToken of IERC20<ContractState> {
fn total_supply(self: @ContractState) -> u256 {
self.erc20.total_supply()
}
Expand All @@ -74,6 +82,7 @@ mod VotingToken {
self.erc20.allowance(owner, spender)
}

// WRITE
fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
assert(false, 'token locked, unwrap first');
false
Expand All @@ -92,5 +101,41 @@ mod VotingToken {
fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool {
self.erc20.approve(spender, amount)
}

fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) {
self.ownable.assert_only_owner();
self.erc20._mint(recipient, amount)
}

fn burn(ref self: ContractState, recipient: ContractAddress, amount: u256) {
self.ownable.assert_only_owner();
self.erc20._burn(recipient, amount)
}

fn totalSupply(self: @ContractState) -> u256 {
self.erc20.total_supply()
}

fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 {
self.erc20.balance_of(account)
}

fn transferFrom(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: u256
) -> bool {
assert(false, 'token locked, unwrap first');
false
}
}

#[abi(embed_v0)]
impl UpgradeableImpl of IUpgradeable<ContractState> {
fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
self.ownable.assert_only_owner();
self.upgradeable._upgrade(new_class_hash);
}
}
}

0 comments on commit 47dd0cc

Please sign in to comment.