Skip to content

Commit efb6aa7

Browse files
authored
Merge pull request #1032 from multiversx/timestamp-based-exchange-scs
Timestamp based exchange SCs
2 parents 7576adb + 75dd4cb commit efb6aa7

File tree

57 files changed

+1857
-717
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1857
-717
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ The owner of all the Pair SCs is the Router SC. It is used for deploying, upgrad
3838

3939
### Farm Contract
4040

41-
In order to gain users trust, the liquidity inside the DEX must be somewhat stable. To achieve that, Farm contracts come in place to incentivise liquidity providers to lock their LP tokens in exchange for MEX rewards. Rewards are generated per block and their rate is configurable in sync with the MEX tokenomics.
41+
In order to gain users trust, the liquidity inside the DEX must be somewhat stable. To achieve that, Farm contracts come in place to incentivise liquidity providers to lock their LP tokens in exchange for MEX rewards. Rewards are generated based on timestamps and their rate is configurable in sync with the MEX tokenomics.
4242

4343
### Farm with Lock Contract
4444

45-
Works the same as the regular Farm with the exception that it does not generate MEX as rewards. Instead, it generates Locked MEX, with the help of the Factory SC. The reason one would choose to go with the locked rewards (LKMEX) instead of the regular rewards (MEX) is that the reward emission rate (reward per block rate) is bigger, meaning the APR is higher.
45+
It works the same as a regular Farm SC, with the exception that it does not generate MEX as rewards. Instead, it generates Locked MEX (XMEX), with the help of the Energy Factory smart contract. Users may choose to receive locked rewards instead of regular MEX because XMEX also provides Energy, which can be used across the entire xExchange protocol.
4646

4747
### Price discovery
4848

common/common_structs/src/alias_types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub type Nonce = u64;
66
pub type Epoch = u64;
77
pub type Week = usize;
88
pub type Percent = u64;
9+
pub type Timestamp = u64;
910
pub type PaymentsVec<M> = ManagedVec<M, EsdtTokenPayment<M>>;
1011
pub type UnlockPeriod<M> = UnlockSchedule<M>;
1112
pub type OldLockedTokenAttributes<M> = LockedAssetTokenAttributesEx<M>;

common/modules/farm/config/src/config.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
multiversx_sc::imports!();
44
multiversx_sc::derive_imports!();
55

6-
use common_structs::Nonce;
6+
use common_structs::{Nonce, Timestamp};
77
use pausable::State;
88

99
pub const DEFAULT_NFT_DEPOSIT_MAX_LEN: usize = 10;
@@ -52,16 +52,16 @@ pub trait ConfigModule: pausable::PausableModule + permissions_module::Permissio
5252
#[storage_mapper("reward_token_id")]
5353
fn reward_token_id(&self) -> SingleValueMapper<TokenIdentifier>;
5454

55-
#[view(getPerBlockRewardAmount)]
56-
#[storage_mapper("per_block_reward_amount")]
57-
fn per_block_reward_amount(&self) -> SingleValueMapper<BigUint>;
58-
5955
#[storage_mapper("produce_rewards_enabled")]
6056
fn produce_rewards_enabled(&self) -> SingleValueMapper<bool>;
6157

62-
#[view(getLastRewardBlockNonce)]
63-
#[storage_mapper("last_reward_block_nonce")]
64-
fn last_reward_block_nonce(&self) -> SingleValueMapper<Nonce>;
58+
#[view(getPerSecondRewardAmount)]
59+
#[storage_mapper("per_second_reward_amount")]
60+
fn per_second_reward_amount(&self) -> SingleValueMapper<BigUint>;
61+
62+
#[view(getLastRewardTimestamp)]
63+
#[storage_mapper("last_reward_timestamp")]
64+
fn last_reward_timestamp(&self) -> SingleValueMapper<Timestamp>;
6565

6666
#[view(getDivisionSafetyConstant)]
6767
#[storage_mapper("division_safety_constant")]

common/modules/farm/farm_base_impl/src/base_traits_impl.rs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
multiversx_sc::imports!();
22

3-
use common_structs::{FarmToken, FarmTokenAttributes, Nonce};
3+
use common_structs::{FarmToken, FarmTokenAttributes, Timestamp};
44
use config::ConfigModule;
55
use contexts::storage_cache::StorageCache;
66
use core::marker::PhantomData;
@@ -54,35 +54,35 @@ pub trait FarmContract {
5454
sc.send().esdt_local_mint(token_id, 0, amount);
5555
}
5656

57-
fn calculate_per_block_rewards(
57+
fn calculate_per_second_rewards(
5858
sc: &Self::FarmSc,
59-
current_block_nonce: Nonce,
60-
last_reward_block_nonce: Nonce,
59+
current_timestamp: Timestamp,
60+
last_reward_timestamp: Timestamp,
6161
) -> BigUint<<Self::FarmSc as ContractBase>::Api> {
62-
if current_block_nonce <= last_reward_block_nonce || !sc.produces_per_block_rewards() {
62+
if current_timestamp <= last_reward_timestamp || !sc.produces_per_second_rewards() {
6363
return BigUint::zero();
6464
}
6565

66-
let per_block_reward = sc.per_block_reward_amount().get();
67-
let block_nonce_diff = current_block_nonce - last_reward_block_nonce;
66+
let per_second_reward = sc.per_second_reward_amount().get();
67+
let timestamp_diff = current_timestamp - last_reward_timestamp;
6868

69-
per_block_reward * block_nonce_diff
69+
per_second_reward * timestamp_diff
7070
}
7171

72-
fn mint_per_block_rewards(
72+
fn mint_per_second_rewards(
7373
sc: &Self::FarmSc,
7474
token_id: &TokenIdentifier<<Self::FarmSc as ContractBase>::Api>,
7575
) -> BigUint<<Self::FarmSc as ContractBase>::Api> {
76-
let current_block_nonce = sc.blockchain().get_block_nonce();
77-
let last_reward_nonce = sc.last_reward_block_nonce().get();
78-
if current_block_nonce > last_reward_nonce {
76+
let current_timestamp = sc.blockchain().get_block_timestamp();
77+
let last_reward_timestamp = sc.last_reward_timestamp().get();
78+
if current_timestamp > last_reward_timestamp {
7979
let to_mint =
80-
Self::calculate_per_block_rewards(sc, current_block_nonce, last_reward_nonce);
80+
Self::calculate_per_second_rewards(sc, current_timestamp, last_reward_timestamp);
8181
if to_mint != 0 {
8282
Self::mint_rewards(sc, token_id, &to_mint);
8383
}
8484

85-
sc.last_reward_block_nonce().set(current_block_nonce);
85+
sc.last_reward_timestamp().set(current_timestamp);
8686

8787
to_mint
8888
} else {
@@ -94,7 +94,7 @@ pub trait FarmContract {
9494
sc: &Self::FarmSc,
9595
storage_cache: &mut StorageCache<Self::FarmSc>,
9696
) {
97-
let total_reward = Self::mint_per_block_rewards(sc, &storage_cache.reward_token_id);
97+
let total_reward = Self::mint_per_second_rewards(sc, &storage_cache.reward_token_id);
9898
if total_reward > 0u64 {
9999
storage_cache.reward_reserve += &total_reward;
100100

common/modules/farm/rewards/src/rewards.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@ pub trait RewardsModule:
88
{
99
fn start_produce_rewards(&self) {
1010
require!(
11-
self.per_block_reward_amount().get() != 0u64,
11+
self.per_second_reward_amount().get() != 0u64,
1212
"Cannot produce zero reward amount"
1313
);
1414
require!(
1515
!self.produce_rewards_enabled().get(),
1616
"Producing rewards is already enabled"
1717
);
18-
let current_nonce = self.blockchain().get_block_nonce();
18+
let current_timestamp = self.blockchain().get_block_timestamp();
1919
self.produce_rewards_enabled().set(true);
20-
self.last_reward_block_nonce().set(current_nonce);
20+
self.last_reward_timestamp().set(current_timestamp);
2121
}
2222

2323
#[inline]
24-
fn produces_per_block_rewards(&self) -> bool {
24+
fn produces_per_second_rewards(&self) -> bool {
2525
self.produce_rewards_enabled().get()
2626
}
2727

dex/farm-with-locked-rewards/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ This endpoint will give back to the caller a Farm position as a result. The Farm
6464

6565
This endpoint receives one payment, and that is the Farm Position. Based on an internal counter that the contract keeps track of, which is the __rps__ - meaning reward per share, the contract can calculate the reward that it needs to return to the caller for those specific tokens that he has sent. The output will consist of two payments: the LP tokens initially added and the accumulated rewards.
6666

67-
This contract simulates per-block-reward-generation by keeping track of the last block that generated mex and keeps updating on every endpoint execution. Everytime an execution happens, the contract will generate the rewards for previous blocks. This is the case for the first successful TX inside a block, so only once per block this check has to be made and the action to be taken.
67+
This contract simulates timestamp-based reward generation by keeping track of the last timestamp that generated rewards and keeps updating on every endpoint execution. Everytime an execution happens, the contract will generate the rewards for the previous time period. This is the case for the first successful transaction within a timestamp interval, so only once per interval this check has to be made and the action to be taken.
6868

6969
### claimRewards
7070

dex/farm-with-locked-rewards/src/lib.rs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use common_structs::FarmTokenAttributes;
99
use contexts::storage_cache::StorageCache;
1010
use core::marker::PhantomData;
1111
use fixed_supply_token::FixedSupplyToken;
12+
use multiversx_sc::storage::StorageKey;
1213

1314
use farm::{
1415
base_functions::{BaseFunctionsModule, ClaimRewardsResultType, DoubleMultiPayment, Wrapper},
@@ -88,12 +89,50 @@ pub trait Farm:
8889

8990
#[upgrade]
9091
fn upgrade(&self) {
91-
let current_epoch = self.blockchain().get_block_epoch();
92-
self.first_week_start_epoch().set_if_empty(current_epoch);
92+
// Aggregate rewards before migration
93+
let mut storage_cache = StorageCache::new(self);
94+
let current_block_nonce = self.blockchain().get_block_nonce();
9395

94-
// Farm position migration code
95-
let farm_token_mapper = self.farm_token();
96-
self.try_set_farm_position_migration_nonce(farm_token_mapper);
96+
let last_reward_block_nonce_mapper =
97+
SingleValueMapper::<Self::Api, u64>::new(StorageKey::new(b"last_reward_block_nonce"));
98+
let per_block_reward_amount_mapper = SingleValueMapper::<Self::Api, BigUint>::new(
99+
StorageKey::new(b"per_block_reward_amount"),
100+
);
101+
102+
let per_block_reward_amount: BigUint<Self::Api> = per_block_reward_amount_mapper.take();
103+
let last_reward_nonce = last_reward_block_nonce_mapper.take();
104+
105+
if self.produces_per_second_rewards() {
106+
let total_reward = if current_block_nonce > last_reward_nonce {
107+
let block_nonce_diff = current_block_nonce - last_reward_nonce;
108+
&per_block_reward_amount * block_nonce_diff
109+
} else {
110+
BigUint::zero()
111+
};
112+
113+
// No minting, even if total_reward is not zero (NoMintWrapper)
114+
if total_reward > 0u64 {
115+
storage_cache.reward_reserve += &total_reward;
116+
let split_rewards = self.take_reward_slice(total_reward);
117+
118+
if storage_cache.farm_token_supply != 0u64 {
119+
let increase = (&split_rewards.base_farm
120+
* &storage_cache.division_safety_constant)
121+
/ &storage_cache.farm_token_supply;
122+
storage_cache.reward_per_share += &increase;
123+
}
124+
}
125+
126+
// Set farm supply for current week to ensure boosted rewards can be claimed correctly
127+
self.set_farm_supply_for_current_week(&storage_cache.farm_token_supply);
128+
}
129+
130+
// Migrate storage
131+
let per_second_reward_amount = per_block_reward_amount / 6u64; // 6 seconds per block
132+
self.per_second_reward_amount()
133+
.set(&per_second_reward_amount);
134+
self.last_reward_timestamp()
135+
.set(self.blockchain().get_block_timestamp());
97136
}
98137

99138
#[payable("*")]
@@ -265,10 +304,10 @@ pub trait Farm:
265304
self.end_produce_rewards::<NoMintWrapper<Self>>();
266305
}
267306

268-
#[endpoint(setPerBlockRewardAmount)]
269-
fn set_per_block_rewards_endpoint(&self, per_block_amount: BigUint) {
307+
#[endpoint(setPerSecondRewardAmount)]
308+
fn set_per_second_rewards_endpoint(&self, per_second_amount: BigUint) {
270309
self.require_caller_has_admin_permissions();
271-
self.set_per_block_rewards::<NoMintWrapper<Self>>(per_block_amount);
310+
self.set_per_second_rewards::<NoMintWrapper<Self>>(per_second_amount);
272311
}
273312

274313
#[endpoint(setBoostedYieldsRewardsPercentage)]
@@ -326,7 +365,7 @@ where
326365
sc: &Self::FarmSc,
327366
storage_cache: &mut StorageCache<Self::FarmSc>,
328367
) {
329-
let total_reward = Self::mint_per_block_rewards(sc, &storage_cache.reward_token_id);
368+
let total_reward = Self::mint_per_second_rewards(sc, &storage_cache.reward_token_id);
330369
if total_reward > 0u64 {
331370
storage_cache.reward_reserve += &total_reward;
332371
let split_rewards = sc.take_reward_slice(total_reward);

dex/farm-with-locked-rewards/tests/farm_with_locked_rewards_setup/mod.rs

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ use common_structs::FarmTokenAttributes;
44
use config::ConfigModule;
55
use multiversx_sc::{
66
codec::multi_types::OptionalValue,
7+
imports::{SingleValueMapper, StorageMapper},
78
storage::mappers::StorageTokenWrapper,
8-
types::{Address, BigInt, EsdtLocalRole, MultiValueEncoded},
9+
types::{Address, BigInt, BigUint, EsdtLocalRole, MultiValueEncoded},
910
};
1011
use multiversx_sc_scenario::{
1112
imports::TxTokenTransfer,
@@ -37,8 +38,8 @@ pub static LOCKED_REWARD_TOKEN_ID: &[u8] = b"LOCKED-123456";
3738
pub static LEGACY_LOCKED_TOKEN_ID: &[u8] = b"LEGACY-123456";
3839
pub static FARMING_TOKEN_ID: &[u8] = b"LPTOK-123456";
3940
pub static FARM_TOKEN_ID: &[u8] = b"FARM-123456";
40-
const DIV_SAFETY: u64 = 1_000_000_000_000;
41-
pub const PER_BLOCK_REWARD_AMOUNT: u64 = 1_000;
41+
pub const DIV_SAFETY: u64 = 1_000_000_000_000;
42+
pub const PER_SECOND_REWARD_AMOUNT: u64 = 1_000;
4243
pub const FARMING_TOKEN_BALANCE: u64 = 100_000_000;
4344
pub const MAX_PERCENTAGE: u64 = 10_000; // 100%
4445
pub const BOOSTED_YIELDS_PERCENTAGE: u64 = 2_500; // 25%
@@ -180,8 +181,8 @@ where
180181
sc.add_sc_address_to_whitelist(managed_address!(&second_user));
181182
sc.add_sc_address_to_whitelist(managed_address!(&third_user));
182183

183-
sc.per_block_reward_amount()
184-
.set(&managed_biguint!(PER_BLOCK_REWARD_AMOUNT));
184+
sc.per_second_reward_amount()
185+
.set(&managed_biguint!(PER_SECOND_REWARD_AMOUNT));
185186

186187
sc.state().set(State::Active);
187188
sc.produce_rewards_enabled().set(true);
@@ -595,4 +596,90 @@ where
595596
})
596597
.assert_ok();
597598
}
599+
600+
pub fn simulate_per_block_migration_storage(
601+
&mut self,
602+
per_block_reward_amount: u64,
603+
last_reward_block_nonce: u64,
604+
) {
605+
self.b_mock
606+
.execute_tx(&self.owner, &self.farm_wrapper, &rust_biguint!(0), |sc| {
607+
use multiversx_sc::storage::StorageKey;
608+
609+
// Set old storage values
610+
let last_reward_block_nonce_mapper = SingleValueMapper::<DebugApi, u64>::new(
611+
StorageKey::new(b"last_reward_block_nonce"),
612+
);
613+
let per_block_reward_amount_mapper =
614+
SingleValueMapper::<DebugApi, BigUint<DebugApi>>::new(StorageKey::new(
615+
b"per_block_reward_amount",
616+
));
617+
618+
last_reward_block_nonce_mapper.set(last_reward_block_nonce);
619+
per_block_reward_amount_mapper.set(managed_biguint!(per_block_reward_amount));
620+
621+
// Clear the new storage to simulate pre-upgrade state
622+
sc.per_second_reward_amount().clear();
623+
sc.last_reward_timestamp().clear();
624+
625+
sc.produce_rewards_enabled().set(true);
626+
})
627+
.assert_ok();
628+
}
629+
630+
pub fn verify_old_storage_cleared(&mut self) {
631+
self.b_mock
632+
.execute_query(&self.farm_wrapper, |_sc| {
633+
use multiversx_sc::storage::StorageKey;
634+
635+
let last_reward_block_nonce_mapper = SingleValueMapper::<DebugApi, u64>::new(
636+
StorageKey::new(b"last_reward_block_nonce"),
637+
);
638+
let per_block_reward_amount_mapper =
639+
SingleValueMapper::<DebugApi, BigUint<DebugApi>>::new(StorageKey::new(
640+
b"per_block_reward_amount",
641+
));
642+
643+
let old_block_nonce: u64 = last_reward_block_nonce_mapper.get();
644+
let old_per_block_amount: BigUint<DebugApi> = per_block_reward_amount_mapper.get();
645+
646+
assert_eq!(
647+
old_block_nonce, 0u64,
648+
"Old block nonce storage should be cleared"
649+
);
650+
assert_eq!(
651+
old_per_block_amount,
652+
managed_biguint!(0),
653+
"Old per block amount storage should be cleared"
654+
);
655+
})
656+
.assert_ok();
657+
}
658+
659+
pub fn get_reward_per_share(&mut self) -> u64 {
660+
let mut reward_per_share = 0u64;
661+
self.b_mock
662+
.execute_query(&self.farm_wrapper, |sc| {
663+
reward_per_share = sc.reward_per_share().get().to_u64().unwrap();
664+
})
665+
.assert_ok();
666+
667+
reward_per_share
668+
}
669+
670+
pub fn advance_time(&mut self, seconds: u64) {
671+
let mut current_timestamp = 0u64;
672+
let mut current_block = 0u64;
673+
self.b_mock
674+
.execute_query(&self.farm_wrapper, |sc| {
675+
use multiversx_sc::contract_base::ContractBase;
676+
677+
current_timestamp = sc.blockchain().get_block_timestamp();
678+
current_block = sc.blockchain().get_block_nonce();
679+
})
680+
.assert_ok();
681+
682+
self.b_mock.set_block_timestamp(current_timestamp + seconds);
683+
self.b_mock.set_block_nonce(current_block + seconds / 6); // 6 seconds per block
684+
}
598685
}

0 commit comments

Comments
 (0)