Skip to content

Commit

Permalink
Ensure economic security on validator sets (#459)
Browse files Browse the repository at this point in the history
* add price oracle

* tidy up

* add todo

* bug fixes

* fix pr comments

* Use spot price, tweak some formulas

Also cleans nits.

---------

Co-authored-by: Luke Parker <[email protected]>
  • Loading branch information
akildemir and kayabaNerve authored Dec 5, 2023
1 parent 746bf5c commit 4ebfae0
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 6 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

19 changes: 19 additions & 0 deletions substrate/coins/pallet/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
#![cfg_attr(not(feature = "std"), no_std)]

use serai_primitives::{Coin, SubstrateAmount, Balance};

pub trait AllowMint {
fn is_allowed(balance: &Balance) -> bool;
}

impl AllowMint for () {
fn is_allowed(_: &Balance) -> bool {
true
}
}

#[frame_support::pallet]
pub mod pallet {
use super::*;
use sp_std::{vec::Vec, any::TypeId};
use sp_core::sr25519::Public;
use sp_runtime::{
Expand All @@ -23,6 +36,7 @@ pub mod pallet {
#[pallet::config]
pub trait Config<I: 'static = ()>: frame_system::Config<AccountId = Public> {
type RuntimeEvent: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type AllowMint: AllowMint;
}

#[pallet::genesis_config]
Expand All @@ -43,6 +57,7 @@ pub mod pallet {
AmountOverflowed,
NotEnoughCoins,
BurnWithInstructionNotAllowed,
MintNotAllowed,
}

#[pallet::event]
Expand Down Expand Up @@ -142,6 +157,10 @@ pub mod pallet {
///
/// Errors if any amount overflows.
pub fn mint(to: Public, balance: Balance) -> Result<(), Error<T, I>> {
if !T::AllowMint::is_allowed(&balance) {
Err(Error::<T, I>::MintNotAllowed)?;
}

// update the balance
Self::increase_balance_internal(to, balance)?;

Expand Down
110 changes: 106 additions & 4 deletions substrate/dex/pallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ pub use pallet::*;

use sp_runtime::{traits::TrailingZeroInput, DispatchError};

use serai_primitives::{Coin, SubstrateAmount};
use serai_primitives::{NetworkId, Coin, SubstrateAmount};

use sp_std::prelude::*;
pub use types::*;
Expand Down Expand Up @@ -154,11 +154,42 @@ pub mod pallet {
#[pallet::storage]
pub type Pools<T: Config> = StorageMap<_, Blake2_128Concat, PoolId, PoolInfo<Coin>, OptionQuery>;

#[pallet::storage]
#[pallet::getter(fn spot_price_for_block)]
pub type SpotPriceForBlock<T: Config> =
StorageDoubleMap<_, Identity, BlockNumberFor<T>, Identity, Coin, [u8; 8], ValueQuery>;

/// Moving window of oracle prices.
///
/// The second [u8; 8] key is the amount's big endian bytes, and u16 is the amount of inclusions
/// in this multi-set.
#[pallet::storage]
#[pallet::getter(fn oracle_prices)]
pub type OraclePrices<T: Config> =
StorageDoubleMap<_, Identity, Coin, Identity, [u8; 8], u16, OptionQuery>;
impl<T: Config> Pallet<T> {
// TODO: consider an algorithm which removes outliers? This algorithm might work a good bit
// better if we remove the bottom n values (so some value sustained over 90% of blocks instead
// of all blocks in the window).
/// Get the highest sustained value for this window.
/// This is actually the lowest price observed during the windows, as it's the price
/// all prices are greater than or equal to.
pub fn highest_sustained_price(coin: &Coin) -> Option<Amount> {
let mut iter = OraclePrices::<T>::iter_key_prefix(coin);
// the first key will be the lowest price due to the keys being lexicographically ordered.
iter.next().map(|amount| Amount(u64::from_be_bytes(amount)))
}
}

#[pallet::storage]
#[pallet::getter(fn oracle_value)]
pub type OracleValue<T: Config> = StorageMap<_, Identity, Coin, Amount, OptionQuery>;

// Pallet's events.
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A successful call of the `CretaPool` extrinsic will create this event.
/// A successful call of the `CreatePool` extrinsic will create this event.
PoolCreated {
/// The pool id associated with the pool. Note that the order of the coins may not be
/// the same as the order specified in the create pool extrinsic.
Expand Down Expand Up @@ -240,6 +271,13 @@ pub mod pallet {
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
// assert that oracle windows size can fit into u16. Otherwise number of observants
// for a price in the `OraclePrices` map can overflow
// We don't want to make this const directly a u16 because it is used the block number
// calculations (which are done as u32s)
u16::try_from(ORACLE_WINDOW_SIZE).unwrap();

// create the pools
for coin in &self.pools {
Pallet::<T>::create_pool(*coin).unwrap();
}
Expand Down Expand Up @@ -311,8 +349,64 @@ pub mod pallet {

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn integrity_test() {
assert!(T::MaxSwapPathLength::get() > 1, "the `MaxSwapPathLength` should be greater than 1",);
fn on_finalize(n: BlockNumberFor<T>) {
// we run this on on_finalize because we want to use the last price of the block for a coin.
// This prevents the exploit where a malicious block proposer spikes the price in either
// direction, then includes a swap in the other direction (ensuring they don't get arbitraged
// against)
// Since they'll have to leave the spike present at the end of the block, making the next
// block the one to include any arbitrage transactions (which there's no guarantee they'll
// produce), this cannot be done in a way without significant risk
for coin in Pools::<T>::iter_keys() {
// insert the new price to our oracle window
// The spot price for 1 coin, in atomic units, to SRI is used
let pool_id = Self::get_pool_id(coin, Coin::native()).ok().unwrap();
let pool_account = Self::get_pool_account(pool_id);
let sri_balance = Self::get_balance(&pool_account, Coin::native());
let coin_balance = Self::get_balance(&pool_account, coin);
// We use 1 coin to handle rounding errors which may occur with atomic units
// If we used atomic units, any coin whose atomic unit is worth less than SRI's atomic unit
// would cause a 'price' of 0
// If the decimals aren't large enough to provide sufficient buffer, use 10,000
let coin_decimals = coin.decimals().max(5);
let accuracy_increase =
HigherPrecisionBalance::from(SubstrateAmount::pow(10, coin_decimals));
let sri_per_coin = u64::try_from(
accuracy_increase * HigherPrecisionBalance::from(sri_balance) /
HigherPrecisionBalance::from(coin_balance),
)
.unwrap_or(u64::MAX);
let sri_per_coin = sri_per_coin.to_be_bytes();

SpotPriceForBlock::<T>::set(n, coin, sri_per_coin);

// Include this spot price into the multiset
{
let observed = OraclePrices::<T>::get(coin, sri_per_coin).unwrap_or(0);
OraclePrices::<T>::set(coin, sri_per_coin, Some(observed + 1));
}

// pop the earliest key from the window once we reach its full size.
if n >= ORACLE_WINDOW_SIZE.into() {
let start_of_window = n - ORACLE_WINDOW_SIZE.into();
let start_spot_price = Self::spot_price_for_block(start_of_window, coin);
SpotPriceForBlock::<T>::remove(start_of_window, coin);
// Remove this price from the multiset
OraclePrices::<T>::mutate_exists(coin, start_spot_price, |v| {
*v = Some(v.unwrap() - 1);
if *v == Some(0) {
*v = None;
}
});
}

// update the oracle value
let highest_sustained = Self::highest_sustained_price(&coin).unwrap_or(Amount(0));
let oracle_value = Self::oracle_value(coin).unwrap_or(Amount(0));
if highest_sustained > oracle_value {
OracleValue::<T>::set(coin, Some(highest_sustained));
}
}
}
}

Expand All @@ -338,6 +432,14 @@ pub mod pallet {

Ok(())
}

/// A hook to be called whenever a network's session is rotated.
pub fn on_new_session(network: NetworkId) {
// reset the oracle value
for coin in network.coins() {
OracleValue::<T>::set(*coin, Self::highest_sustained_price(coin));
}
}
}

/// Pallet's callable functions.
Expand Down
2 changes: 2 additions & 0 deletions substrate/dex/pallet/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,12 @@ impl frame_system::Config for Test {

impl coins::Config for Test {
type RuntimeEvent = RuntimeEvent;
type AllowMint = ();
}

impl coins::Config<coins::Instance1> for Test {
type RuntimeEvent = RuntimeEvent;
type AllowMint = ();
}

impl Config for Test {
Expand Down
4 changes: 4 additions & 0 deletions substrate/dex/pallet/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ use super::*;
use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;

/// This needs to be long enough for arbitrage to occur and make holding
/// any fake price up sufficiently unprofitable.
pub const ORACLE_WINDOW_SIZE: u32 = 1000;

/// Stores the lp_token coin id a particular pool has been assigned.
#[derive(Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)]
pub struct PoolInfo<PoolCoinId> {
Expand Down
10 changes: 10 additions & 0 deletions substrate/primitives/src/networks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ pub enum NetworkId {
Ethereum,
Monero,
}
impl NetworkId {
pub fn coins(&self) -> &'static [Coin] {
match self {
Self::Serai => &[Coin::Serai],
Self::Bitcoin => &[Coin::Bitcoin],
Self::Ethereum => &[Coin::Ether, Coin::Dai],
Self::Monero => &[Coin::Monero],
}
}
}

pub const NETWORKS: [NetworkId; 4] =
[NetworkId::Serai, NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero];
Expand Down
2 changes: 2 additions & 0 deletions substrate/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,12 @@ impl transaction_payment::Config for Runtime {

impl coins::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type AllowMint = ValidatorSets;
}

impl coins::Config<coins::Instance1> for Runtime {
type RuntimeEvent = RuntimeEvent;
type AllowMint = ();
}

impl dex::Config for Runtime {
Expand Down
2 changes: 2 additions & 0 deletions substrate/validator-sets/pallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ serai-primitives = { path = "../../primitives", default-features = false }
validator-sets-primitives = { package = "serai-validator-sets-primitives", path = "../primitives", default-features = false }

coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false }
dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false }

[features]
std = [
Expand All @@ -56,6 +57,7 @@ std = [
"validator-sets-primitives/std",

"coins-pallet/std",
"dex-pallet/std",
]

runtime-benchmarks = [
Expand Down
Loading

0 comments on commit 4ebfae0

Please sign in to comment.