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

Add stableswap #115

Merged
merged 4 commits into from
Aug 21, 2024
Merged
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
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion amm/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ help: # Show help for each of the Makefile recipes.
AMM_CONTRACTS = ./contracts
AMM_CONTRACTS_PATHS := $(shell find $(AMM_CONTRACTS) -mindepth 1 -maxdepth 1 -type d)

CONTRACTS := factory_contract pair_contract router_contract
CONTRACTS := factory_contract pair_contract router_contract stable_pool_contract mock_rate_provider_contract

INK_DEV_IMAGE := "public.ecr.aws/p6e8q1z1/ink-dev:2.1.0"
SCRIPT_DIR := $(shell cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
Expand Down
33 changes: 33 additions & 0 deletions amm/contracts/mock_rate_provider/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[package]
name = "mock_rate_provider_contract"
version = "0.1.0"
authors = ["Cardinal Cryptography"]
edition = "2021"

[dependencies]
ink = { version = "=4.3.0", default-features = false }

scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [
"derive",
] }
scale-info = { version = "2.9", default-features = false, features = [
"derive",
], optional = true }


traits = { path = "../../traits", default-features = false }

[lib]
name = "mock_rate_provider_contract"
path = "lib.rs"
doctest = false

[features]
default = ["std"]
std = [
"ink/std",
"scale/std",
"scale-info/std",
"traits/std",
]
ink-as-dependency = []
31 changes: 31 additions & 0 deletions amm/contracts/mock_rate_provider/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#![cfg_attr(not(feature = "std"), no_std, no_main)]

#[ink::contract]
mod mock_rate_provider {
#[ink(storage)]
pub struct MockRateProviderContract {
rate: u128,
}

impl MockRateProviderContract {
#[ink(constructor)]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
rate: 10u128.pow(12u32),
}
}

#[ink(message)]
pub fn set_rate(&mut self, rate: u128) {
self.rate = rate;
}
}

impl traits::RateProvider for MockRateProviderContract {
#[ink(message)]
fn get_rate(&mut self) -> u128 {
self.rate
}
}
}
37 changes: 37 additions & 0 deletions amm/contracts/stable_pool/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "stable_pool_contract"
version = "0.1.0"
authors = ["Cardinal Cryptography"]
edition = "2021"

[dependencies]
ink = { version = "=4.3.0", default-features = false }

scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [
"derive",
] }
scale-info = { version = "2.9", default-features = false, features = [
"derive",
], optional = true }

psp22 = { version = "=0.2.2" , default-features = false }

traits = { path = "../../traits", default-features = false }
amm-helpers = { path = "../../../helpers", default-features = false }

[lib]
name = "stable_pool_contract"
path = "lib.rs"
doctest = false

[features]
default = ["std"]
std = [
"ink/std",
"scale/std",
"scale-info/std",
"psp22/std",
"traits/std",
"amm-helpers/std",
]
ink-as-dependency = []
242 changes: 242 additions & 0 deletions amm/contracts/stable_pool/amp_coef.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
use amm_helpers::{
constants::stable_pool::{MAX_AMP, MAX_AMP_CHANGE, MIN_AMP, MIN_RAMP_DURATION},
ensure,
};
use ink::env::DefaultEnvironment;
use traits::{MathError, StablePoolError};

#[derive(Default, Debug, scale::Encode, scale::Decode, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
feature = "std",
derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
)]
pub struct AmpCoef {
/// Initial amplification coefficient.
init_amp_coef: u128,
/// Target for ramping up amplification coefficient.
future_amp_coef: u128,
/// Initial amplification time.
init_time: u64,
/// Stop ramp up amplification time.
future_time: u64,
}

impl AmpCoef {
pub fn new(init_amp_coef: u128) -> Result<Self, StablePoolError> {
ensure!(init_amp_coef >= MIN_AMP, StablePoolError::AmpCoefTooLow);
ensure!(init_amp_coef <= MAX_AMP, StablePoolError::AmpCoefTooHigh);
Ok(Self {
init_amp_coef,
future_amp_coef: init_amp_coef,
init_time: 0,
future_time: 0,
})
}

pub fn compute_amp_coef(&self) -> Result<u128, MathError> {
let current_time = ink::env::block_timestamp::<DefaultEnvironment>();
if current_time < self.future_time {
let time_range = self
.future_time
.checked_sub(self.init_time)
.ok_or(MathError::SubUnderflow(51))?;
let time_delta = current_time
.checked_sub(self.init_time)
.ok_or(MathError::SubUnderflow(52))?;

// Compute amp factor based on ramp time
let amp_range = self.future_amp_coef.abs_diff(self.init_amp_coef);
let amp_delta = amp_range
.checked_mul(time_delta as u128)
.ok_or(MathError::MulOverflow(51))?
.checked_div(time_range as u128)
.ok_or(MathError::DivByZero(51))?;
if self.future_amp_coef >= self.init_amp_coef {
// Ramp up
self.init_amp_coef
.checked_add(amp_delta)
.ok_or(MathError::AddOverflow(1))
} else {
// Ramp down
self.init_amp_coef
.checked_sub(amp_delta)
.ok_or(MathError::SubUnderflow(55))
}
} else {
Ok(self.future_amp_coef)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be OK to return self.init_amp_coef as well? We modify it in the { } above, if we're still ramping up/down. It just seems weird to return "future" as the target.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there was ramping, there's a difference between the init_amp_coef and the future_amp_coef. Over the ramping period the init_amp_coef +/- amp_delta gets closer to the future_amp_coef as current_time increases. When the ramping is over it should return the future_amp_coef because future_time <= current_time (the else {...} block).

Maybe we can reconsider the naming: rename future_* to target_*?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I didn't notice we don't return from the first block.

When the ramping is over, isn't it the case that init_amp_coef and future_amp_coef will be equal?

Also, while the ramping lasts, this method will return future value of Amp coef – is that correct? Imagine ramping is spread across 1000 blocks, so every block the A increases by 1/1000 (a a_delta) . We'd return the A_init + 1000 * a_delta here though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the ramping is over, isn't it the case that init_amp_coef and future_amp_coef will be equal?

Only if the ramping was stopped with stop_ramp_amp_coef(...) which sets both init_amp_coef and future_amp_coef to the value computed for the current time (block).

Also, while the ramping lasts, this method will return future value of Amp coef – is that correct?

When it lasts (current_time < future_time) it will return init_amp_coef +/- amp_delta.
The value of init_amp_coef does not change after the ramping ends because is not necessary since we know that after the ramping ends the value of the A should be equal to the future_amp_coef

Imagine ramping is spread across 1000 blocks, so every block the A increases by 1/1000 (a a_delta) . We'd return the A_init + 1000 * a_delta here though.

Something like that. Let's assume that A = 11, the current_block = 50 and we want to ramp it to 100 when we reach 1000 block. The values are set to:

  • init_amp_coef = 11
  • init_time = 50
  • future_amp_coef = 100
  • future_time = 1000

At a given block N where N < future_time, we can compute A like this:
A = init_amp_coef + (future_amp_coef - init_amp_coef) * (N - init_time) / (future_time - init_time) rounded down

E.g. N = 500: 11 + (100 - 11) * (500 - 50) / (1000 - 50) = ~53,16 = 53

If N is greater than future_time we should return the future_amp_coef.

Because we can't handle floats we can't tell by how much A will change every block (using integers) but we are able to tell what the value of A should be at a given block and that's how the value of A is computed in the ramping period.

}
}

pub fn ramp_amp_coef(
&mut self,
future_amp_coef: u128,
future_time: u64,
) -> Result<(), StablePoolError> {
ensure!(future_amp_coef >= MIN_AMP, StablePoolError::AmpCoefTooLow);
ensure!(future_amp_coef <= MAX_AMP, StablePoolError::AmpCoefTooHigh);
let current_time = ink::env::block_timestamp::<DefaultEnvironment>();
let ramp_duration = future_time.checked_sub(current_time);
ensure!(
ramp_duration.is_some() && ramp_duration.unwrap() >= MIN_RAMP_DURATION,
StablePoolError::AmpCoefRampDurationTooShort
);
let current_amp_coef = self.compute_amp_coef()?;
ensure!(
(future_amp_coef >= current_amp_coef
&& future_amp_coef <= current_amp_coef * MAX_AMP_CHANGE)
|| (future_amp_coef < current_amp_coef
&& future_amp_coef * MAX_AMP_CHANGE >= current_amp_coef),
StablePoolError::AmpCoefChangeTooLarge
);
self.init_amp_coef = current_amp_coef;
self.init_time = current_time;
self.future_amp_coef = future_amp_coef;
self.future_time = future_time;
Ok(())
}

/// Stop ramping A. If ramping is not in progress, it does not influence the A.
pub fn stop_ramp_amp_coef(&mut self) -> Result<(), StablePoolError> {
let current_amp_coef = self.compute_amp_coef()?;
let current_time = ink::env::block_timestamp::<DefaultEnvironment>();
self.init_amp_coef = current_amp_coef;
self.future_amp_coef = current_amp_coef;
self.init_time = current_time;
self.future_time = current_time;
Ok(())
}

/// Returns a tuple of the future amplification coefficient and the ramping end time.
/// Returns `None` if the amplification coefficient is not in ramping period.
pub fn future_amp_coef(&self) -> Option<(u128, u64)> {
let current_time = ink::env::block_timestamp::<DefaultEnvironment>();
if current_time < self.future_time {
Some((self.future_amp_coef, self.future_time))
} else {
None
}
}
}

#[cfg(test)]
mod tests {
use super::*;

fn set_block_timestamp(ts: u64) {
ink::env::test::set_block_timestamp::<ink::env::DefaultEnvironment>(ts);
}

#[test]
fn amp_coef_up() {
let amp_coef = AmpCoef {
init_amp_coef: 100,
future_amp_coef: 1000,
init_time: 100,
future_time: 1600,
};
set_block_timestamp(100);
assert_eq!(amp_coef.compute_amp_coef(), Ok(100));
set_block_timestamp(850);
assert_eq!(amp_coef.compute_amp_coef(), Ok(550));
set_block_timestamp(1600);
assert_eq!(amp_coef.compute_amp_coef(), Ok(1000));
}

#[test]
fn amp_coef_down() {
let amp_coef = AmpCoef {
init_amp_coef: 1000,
future_amp_coef: 100,
init_time: 100,
future_time: 1600,
};
set_block_timestamp(100);
assert_eq!(amp_coef.compute_amp_coef(), Ok(1000));
set_block_timestamp(850);
assert_eq!(amp_coef.compute_amp_coef(), Ok(550));
set_block_timestamp(1600);
assert_eq!(amp_coef.compute_amp_coef(), Ok(100));
}

#[test]
fn amp_coef_change_duration() {
set_block_timestamp(1000);
let mut amp_coef = AmpCoef {
init_amp_coef: 1000,
future_amp_coef: 100,
init_time: 100,
future_time: 1600,
};
assert_eq!(
amp_coef.ramp_amp_coef(1000, 999),
Err(StablePoolError::AmpCoefRampDurationTooShort)
);
assert_eq!(
amp_coef.ramp_amp_coef(1000, 1000 + MIN_RAMP_DURATION - 1),
Err(StablePoolError::AmpCoefRampDurationTooShort)
);
assert_eq!(
amp_coef.ramp_amp_coef(1000, 1000 + MIN_RAMP_DURATION),
Ok(())
);
}

#[test]
fn amp_coef_change_too_large() {
set_block_timestamp(100);
let mut amp_coef = AmpCoef {
init_amp_coef: 100,
future_amp_coef: 100,
init_time: 100,
future_time: 1600,
};
assert_eq!(
amp_coef.ramp_amp_coef(1001, 100 + MIN_RAMP_DURATION),
Err(StablePoolError::AmpCoefChangeTooLarge)
);
assert_eq!(
amp_coef.ramp_amp_coef(1000, 100 + MIN_RAMP_DURATION),
Ok(())
);
}

#[test]
fn amp_coef_stop_ramp() {
set_block_timestamp(100);
let mut amp_coef = AmpCoef {
init_amp_coef: 100,
future_amp_coef: 100,
init_time: 100,
future_time: 1600,
};
assert_eq!(amp_coef.compute_amp_coef(), Ok(100));
assert_eq!(
amp_coef.ramp_amp_coef(1000, 100 + MIN_RAMP_DURATION),
Ok(())
);
set_block_timestamp(100 + MIN_RAMP_DURATION / 2);
assert!(amp_coef.stop_ramp_amp_coef().is_ok());
assert_eq!(amp_coef.compute_amp_coef(), Ok(550));
}

#[test]
fn amp_coef_stop_ramp_no_change() {
set_block_timestamp(100);
let mut amp_coef = AmpCoef {
init_amp_coef: 100,
future_amp_coef: 100,
init_time: 100,
future_time: 1600,
};
assert_eq!(amp_coef.compute_amp_coef(), Ok(100));
assert_eq!(
amp_coef.ramp_amp_coef(1000, 100 + MIN_RAMP_DURATION),
Ok(())
);
set_block_timestamp(100 + MIN_RAMP_DURATION);
assert_eq!(amp_coef.compute_amp_coef(), Ok(1000));
set_block_timestamp(100 + MIN_RAMP_DURATION * 2);
assert!(amp_coef.stop_ramp_amp_coef().is_ok());
assert_eq!(amp_coef.compute_amp_coef(), Ok(1000));
}
}
Loading