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

VRGDA - sell a token at a target rate #17

Closed
wants to merge 4 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
8 changes: 8 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Code generated by scarb DO NOT EDIT.
version = 1

[[package]]
name = "cubit"
version = "1.2.0"
source = "git+https://github.com/raphaeldkhn/cubit#e6331ebf98c5d5f442a0e5edefe0b367c8e270d9"

[[package]]
name = "governance"
version = "0.1.0"
dependencies = [
"cubit",
]
1 change: 1 addition & 0 deletions Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ homepage = "https://ekubo.org"
cairo-version = "2.3.0"

[dependencies]
cubit = { git = "https://github.com/raphaeldkhn/cubit" }
starknet = "=2.3.0"

[[target.starknet-contract]]
Expand Down
2 changes: 2 additions & 0 deletions src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ mod governor;
mod governance_token;
mod timelock;
mod call_trait;
mod token_vrgda;
mod vrgda;

#[cfg(test)]
mod tests;
2 changes: 2 additions & 0 deletions src/tests.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ mod timelock_test;
mod call_trait_test;
#[cfg(test)]
mod utils;
#[cfg(test)]
mod vrgda_test;
114 changes: 114 additions & 0 deletions src/tests/vrgda_test.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use cubit::f128::{Fixed, FixedTrait};
use governance::vrgda::{VRGDAParameters, VRGDAParametersTrait};
use debug::{PrintTrait};


#[test]
#[available_gas(30000000)]
fn test_decay_constant() {
assert(
VRGDAParameters {
target_price: FixedTrait::ONE(),
num_sold_per_time_unit: 1,
price_decay_percent: FixedTrait::ONE() / FixedTrait::new_unscaled(2, false)
}
.decay_constant() == FixedTrait::new(12786309186476892720, true),
'decay_constant'
);
}

#[test]
#[available_gas(30000000)]
fn test_p_at_time_zero() {
assert(
VRGDAParameters {
target_price: FixedTrait::ONE(),
num_sold_per_time_unit: 1,
price_decay_percent: FixedTrait::ONE() / FixedTrait::new_unscaled(2, false)
}
.p(
time_units_since_start: FixedTrait::ZERO(), sold: 0
) == FixedTrait::new(0x17154754c6a1bf740, false),
'p'
);
}

#[test]
#[available_gas(30000000)]
fn test_p_at_time_one() {
assert(
VRGDAParameters {
target_price: FixedTrait::ONE(),
num_sold_per_time_unit: 1,
price_decay_percent: FixedTrait::ONE() / FixedTrait::new_unscaled(2, false)
}
.p(
time_units_since_start: FixedTrait::ONE(), sold: 0
) == FixedTrait::new(0xb8aa3aa6350dfba0, false),
'p'
);
}

#[test]
#[available_gas(30000000)]
fn test_p_time_one_more_sold_per_time_unit() {
assert(
VRGDAParameters {
target_price: FixedTrait::ONE(),
num_sold_per_time_unit: 1000,
price_decay_percent: FixedTrait::ONE() / FixedTrait::new_unscaled(2, false)
}
.p(
time_units_since_start: FixedTrait::ONE(), sold: 0
) == FixedTrait::new(0x8009f8be9ba94b6f, false),
'p'
);
}

#[test]
#[available_gas(30000000)]
fn test_p_time_zero_many_sold() {
assert(
VRGDAParameters {
target_price: FixedTrait::ONE(),
num_sold_per_time_unit: 1000,
price_decay_percent: FixedTrait::ONE() / FixedTrait::new_unscaled(2, false)
}
.p(
time_units_since_start: FixedTrait::ZERO(), sold: 1000
) == FixedTrait::new(0x20032fd0c57d40b42, false),
'p'
);
}

#[test]
#[available_gas(30000000)]
fn test_p_sold_on_schedule() {
assert(
VRGDAParameters {
target_price: FixedTrait::ONE(),
num_sold_per_time_unit: 1000,
price_decay_percent: FixedTrait::ONE() / FixedTrait::new_unscaled(2, false)
}
.p(
time_units_since_start: FixedTrait::ZERO(), sold: 1000
) == FixedTrait::new(0x20032fd0c57d40b42, false),
'p'
);
}

#[test]
#[available_gas(30000000)]
fn test_quote_batch_sold_example_schedule() {
assert(
VRGDAParameters {
target_price: FixedTrait::ONE(),
num_sold_per_time_unit: 1000,
price_decay_percent: FixedTrait::ONE() / FixedTrait::new_unscaled(2, false)
}
.quote_batch(
time_units_since_start: FixedTrait::ZERO(), sold: 0, amount: 10
) == FixedTrait::new(185108235227361442154, false), // ~= 10.0347
'p'
);
}
141 changes: 141 additions & 0 deletions src/token_vrgda.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use starknet::{ContractAddress};
use governance::vrgda::{VRGDAParameters, VRGDAParametersTrait};

#[starknet::interface]
trait ITokenVRGDA<TContractState> {
// Buy the token with the ETH transferred to this contract, receiving at least min_amount_out
// The result is transferred to the recipient
fn buy(ref self: TContractState, buy_amount: u64, max_amount_in: u128) -> u128;

// Withdraw the proceeds, must be called by the benefactor
fn withdraw_proceeds(ref self: TContractState);
}

#[starknet::interface]
trait IERC20<TContractState> {
fn balanceOf(self: @TContractState, account: ContractAddress) -> u256;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
}

// The buyer must transfer this token to the VRGDA contract before calling buy
#[starknet::interface]
trait IOptionToken<TContractState> {
fn burn(ref self: TContractState, amount: u128);
}

#[starknet::contract]
mod TokenVRGDA {
use super::{
ContractAddress, ITokenVRGDA, IERC20Dispatcher, IERC20DispatcherTrait,
IOptionTokenDispatcher, IOptionTokenDispatcherTrait, VRGDAParameters, VRGDAParametersTrait
};
use cubit::f128::{Fixed, FixedTrait};
use starknet::{get_caller_address, get_contract_address, contract_address_const};

#[storage]
struct Storage {
// The token that is used to buy the token
payment_token: IERC20Dispatcher,
// The token that gives the user the right to purchase the sold token
option_token: IOptionTokenDispatcher,
// The token that is sold
sold_token: IERC20Dispatcher,
// the configuration of the vrgda
vrgda_parameters: VRGDAParameters,
// The address that receives the payment token from the sale of the tokens
benefactor: ContractAddress,
// The amount of tokens that have been used to purchase the sold token and have not been withdrawn
// Used to compute how much was paid
reserves: u128,
// The number of sold tokens thus far
amount_sold: u128,
}

#[derive(starknet::Event, Drop)]
struct Buy {
buyer: ContractAddress,
paid: u128,
sold: u128,
}

#[derive(starknet::Event, Drop)]
#[event]
enum Event {
Buy: Buy,
}

#[constructor]
fn constructor(
ref self: ContractState,
payment_token: IERC20Dispatcher,
option_token: IOptionTokenDispatcher,
sold_token: IERC20Dispatcher,
vrgda_parameters: VRGDAParameters,
benefactor: ContractAddress
) {
self.payment_token.write(payment_token);
self.option_token.write(option_token);
self.sold_token.write(sold_token);
self.vrgda_parameters.write(vrgda_parameters);
self.benefactor.write(benefactor);
}

#[external(v0)]
impl TokenVRGDAImpl of ITokenVRGDA<ContractState> {
fn buy(ref self: ContractState, buy_amount: u64, max_amount_in: u128) -> u128 {
let payment_token = self.payment_token.read();
let option_token = self.option_token.read();
let sold_token = self.sold_token.read();

let reserves = self.reserves.read();

let payment_balance: u128 = payment_token
.balanceOf(get_contract_address())
.try_into()
.expect('PAID_OVERFLOW');

let paid: u128 = payment_balance - reserves;

self.reserves.write(payment_balance);

let amount_sold = self.amount_sold.read();

let quote = self
.vrgda_parameters
.read()
.quote_batch(
time_units_since_start: FixedTrait::new(0, false),
sold: amount_sold.try_into().unwrap(),
amount: buy_amount
);
let sold: u128 = 0;

// assert(quote >= max_amount_in, 'PURCHASED');

// Account for the newly sold amount before transferring anything
self.amount_sold.write(amount_sold + sold);

// assert(
// sold
// .into() <= (amount_sold.into()
// + option_token.balanceOf(get_contract_address())),
// 'INSUFFICIENT_OPTION_TOKENS'
// );

self.emit(Buy { buyer: get_caller_address(), paid, sold });

sold_token.transfer(get_caller_address(), sold.into());

sold
}

fn withdraw_proceeds(ref self: ContractState) {
let benefactor = self.benefactor.read();
assert(get_caller_address() == benefactor, 'BENEFACTOR_ONLY');
let reserves = self.reserves.read();
self.reserves.write(0);
self.payment_token.read().transfer(benefactor, reserves.into());
}
}
}

41 changes: 41 additions & 0 deletions src/vrgda.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use cubit::f128::{Fixed, FixedTrait};

#[derive(Drop, Copy, Serde, starknet::Store)]
struct VRGDAParameters {
// The price at which the VRGDA should aim to sell the tokens
target_price: Fixed,
// How many tokens should be sold per time unit, in combination with the target price determines the rate
num_sold_per_time_unit: u64,
// How the price decays per time unit, if none are sold
price_decay_percent: Fixed,
}

#[generate_trait]
impl VRGDAParametersTraitImpl of VRGDAParametersTrait {
fn decay_constant(self: VRGDAParameters) -> Fixed {
(FixedTrait::ONE() - self.price_decay_percent).ln()
}

fn p_integral(self: VRGDAParameters, time_units_since_start: Fixed, sold: u64) -> Fixed {
-(self.target_price
* FixedTrait::new_unscaled(self.num_sold_per_time_unit.into(), false)
* (FixedTrait::ONE() - self.price_decay_percent)
.pow(
time_units_since_start
- (FixedTrait::new_unscaled(sold.into(), false)
/ FixedTrait::new_unscaled(self.num_sold_per_time_unit.into(), false))
)
/ self.decay_constant())
}

fn quote_batch(
self: VRGDAParameters, time_units_since_start: Fixed, sold: u64, amount: u64
) -> Fixed {
self.p_integral(time_units_since_start, sold + amount)
- self.p_integral(time_units_since_start, sold)
}

fn p(self: VRGDAParameters, time_units_since_start: Fixed, sold: u64) -> Fixed {
self.quote_batch(time_units_since_start, sold, amount: 1)
}
}