diff --git a/farm/contracts/lib.rs b/farm/contracts/lib.rs index b9a2176a..7ae30e9f 100644 --- a/farm/contracts/lib.rs +++ b/farm/contracts/lib.rs @@ -5,22 +5,9 @@ mod reentrancy_guard; // TODO: // Add upper bound on farm length. +// Tests. // ? Refactor to make staking logic reusable in different contracts. -// Tests: -// Deposit: -// - deposit with 0 balance fails -// - deposit with 0 amount fails -// - deposit with non-zero balance succeeds -// - deposit as first farmer takes all shares -// - deposit triggers claim -// - deposit as second farmer splits shares and updates reward counter properly -// - multiple, repeated deposits by the same farmer update reward counter properly -// -// Withdraw: -// Stop: -// Create & Start farm: - #[ink::contract] mod farm { use crate::{ @@ -80,6 +67,8 @@ mod farm { const REENTRANCY_GUARD_LOCKED: u8 = 1u8; const REENTRANCY_GUARD_FREE: u8 = 0u8; + const SCALING_FACTOR: u128 = 10_u128.pow(18); + impl Farm { #[ink(constructor)] pub fn new(pair_address: AccountId) -> Self { @@ -152,8 +141,6 @@ mod farm { // Double-check we have enough to cover the whole farm. if duration * rate < reward_amount { - ink::env::debug_println!("duration * rate <= reward_amount"); - ink::env::debug_println!("{:?} * {:?} <= {:?}", duration, rate, reward_amount); return Err(FarmStartError::InsufficientRewardAmount) } @@ -461,9 +448,10 @@ mod farm { if total_supply == 0 { return Some(reward_per_token_stored) } + reward_rate .checked_mul(last_time_reward_applicable - last_update_time) - .and_then(|r| r.checked_mul(1_u128.pow(18))) + .and_then(|r| r.checked_mul(SCALING_FACTOR)) .and_then(|r| r.checked_div(total_supply)) .and_then(|r| r.checked_add(reward_per_token_stored)) } @@ -477,7 +465,7 @@ mod farm { rewards_per_token .checked_sub(paid_reward_per_token) .and_then(|r| r.checked_mul(shares)) - .and_then(|r| r.checked_div(1_u128.pow(18))) + .and_then(|r| r.checked_div(SCALING_FACTOR)) } use openbrush::modifier_definition; @@ -529,27 +517,420 @@ mod farm { use super::*; - #[ink::test] - fn new_creates_stopped_farm() { - let pool_id = AccountId::from([0x01; 32]); - let farm = Farm::new(pool_id); - assert_eq!(farm.is_stopped, true); + fn set_sender(sender: AccountId) { + ink::env::test::set_caller::(sender); + } + + fn default_accounts() -> ink::env::test::DefaultAccounts { + ink::env::test::default_accounts::() + } + + fn alice() -> AccountId { + default_accounts().alice + } + + fn bob() -> AccountId { + default_accounts().bob } - #[ink::test] - fn start_farm() { - let pool_id = AccountId::from([0x01; 32]); - let mut farm = Farm::new(pool_id); - ink::env::test::set_block_timestamp::(1); - let reward_tokens = vec![AccountId::from([0x02; 32])]; - let r = farm.start(5, vec![300], reward_tokens); - println!("{:?}", r); + #[cfg(test)] + mod reward_calculation { + use crate::farm::SCALING_FACTOR; + + // 1 reward tokens for every t=1. + const REWARD_RATE: u128 = 100; + + fn reward_per_token( + reward_per_token_stored: u128, + reward_rate: u128, + total_supply: u128, + last_update_time: u128, + last_time_reward_applicable: u128, + ) -> u128 { + super::reward_per_token( + reward_per_token_stored, + reward_rate, + total_supply, + last_update_time, + last_time_reward_applicable, + ) + .expect("to calculate reward per token") + } + + fn rewards_earned( + shares: u128, + rewards_per_token: u128, + paid_reward_per_token: u128, + ) -> u128 { + super::rewards_earned(shares, rewards_per_token, paid_reward_per_token) + .expect("to calculate rewards earned") + } + + /// Case when there's a single farmer, + /// staking 100 tokens, from t=3 until t=5. + /// shares: + // ▲ + // │ + // 100 │ ┌─────┐ + // └────┴─────┴──► + // 3 5 t + #[test] + fn single_farmer_simple() { + let shares = 100; + let total_supply = shares; + + let rewards_per_token = reward_per_token(0, REWARD_RATE, total_supply, 3, 5); + // = r_j0 + R/T(t_j - t_j0) + // = 0 + 100/100 * 2 + // = 2 + assert_eq!(rewards_per_token, 2 * super::SCALING_FACTOR); + let reward_earned = rewards_earned(shares, rewards_per_token, 0); + assert_eq!(reward_earned, 200); + } + + /// Case when there's a single farmer, + /// staking 100 tokens, from t=3 until t=5, + /// then topping up with 200 tokens more, + /// and exiting at t=8. + /// For t=3 until t=5, the farmer should get 200 tokens. + /// For t=5 until t=8, the farmer should get 300 tokens. + /// Total: 500 tokens. + /// + /// ▲ + // │ + // │ + // 300 │ ┌─────┐ + // │ │ │ + // 100 │ ┌─────┘ │ + // └────┴───────────┴───► + // 3 5 8 t + #[test] + fn single_farmer_top_up() { + let shares = 100; + let total_supply = shares; + let rewards_per_token_from0_till3 = 0; + + let rewards_per_token_from0_till5 = reward_per_token( + rewards_per_token_from0_till3, + REWARD_RATE, + total_supply, + 3, + 5, + ); + // expected value is + // = r_j0 + R/T(t_j-t_j0) + // = 0 + REWARD_RATE / TOTAL_SUPPLY * (5 - 3) + // = 0 + 100/100 * 2 + // = 2 + assert_eq!(rewards_per_token_from0_till5, 2 * SCALING_FACTOR); + let reward_earned = rewards_earned(shares, rewards_per_token_from0_till5, 0); + assert_eq!(reward_earned, 200); + + let shares: u128 = 300; + let total_supply = shares; + let rewards_per_token_from0_till8 = reward_per_token( + rewards_per_token_from0_till5, + REWARD_RATE, + total_supply, + 5, + 8, + ); + // Reminder: expected value is: + // = r_j0 + R/T(t_j-t_j0) + // = r_j0 + REWARD_RATE / TOTAL_SUPPLY * (8 - 5) + // = r_j0 + 100/300 * 3 + // = r_j0 + 1 + let expected_second = rewards_per_token_from0_till5 + 1 * super::SCALING_FACTOR; + assert_eq!(rewards_per_token_from0_till8, expected_second); + let reward_earned = rewards_earned( + shares, + rewards_per_token_from0_till8, + rewards_per_token_from0_till5, + ); + assert_eq!(reward_earned, 300); + } + + // ▲ + // │ + // │ + // 300 │ ┌─────┐ + // │ │ │ + // 100 │ │ └─────┐ + // └────┴───────────┴───► + // 3 5 8 t + #[test] + fn single_farmer_withdraw_partial() { + let shares = 300; + let total_supply = shares; + let rewards_per_token_from0_till3 = 0; + + let rewards_per_token_from0_till5 = reward_per_token( + rewards_per_token_from0_till3, + REWARD_RATE, + total_supply, + 3, + 5, + ); + // expected value is + // = r_j0 + R/T(t_j-t_j0) + // = 0 + (1 reward_rate * 2s)/300 * SCALING_FACTOR + // = 2/300 * SCALING_FACTOR + let expected = 666666666666666666u128; + assert_eq!(rewards_per_token_from0_till5, expected); + let reward_earned = rewards_earned(shares, rewards_per_token_from0_till5, 0); + println!("reward_earned = {:?}", reward_earned); + // expected value is 200: + // = reward_per_token * shares / SCALING_FACTOR + // = (2/300 * SCALING_FACTOR) * 300 / SCALING_FACTOR + // = 2 + // but due to roundings, we get 199. + // We could choose to add one to account for the error, + // but we prefer to have rounding errors benefit the farm, + // to avoid liquidity drip slowly out. + assert_eq!(reward_earned, 199); + + let shares: u128 = 100; + let total_supply = shares; + let rewards_per_token = reward_per_token( + rewards_per_token_from0_till5, + REWARD_RATE, + total_supply, + 5, + 8, + ); + // Expected value is: + // = r_j0 + R/T(t_j-t_j0) + // = 200/300 + 100/100 * 3 + // = (3 + 2/3) + // modulo SCALING_FACTOR + let expected = 3666666666666666666u128; + assert_eq!(rewards_per_token, expected); + let reward_earned = + rewards_earned(shares, rewards_per_token, rewards_per_token_from0_till5); + assert_eq!(reward_earned, 300); + } + + // ▲ + // 300 │ ┌────┐ + // │ │ └─────┐ 200 + // │ │ BOB │ + // 100 │ ┌──┴────┐ │ + // │ │ ALICE │ │ + // └───┴───────┴─────┴──────► + // 3 5 7 10 t + #[test] + fn two_farmers_overlap() { + let alice = 100; + let bob = 200; + let reward_per_token_from0_till3 = 0; + + // Alice deposits 100 at t=3; + // = r_j0 + R/T(t_j-t_j0) + // = 100/100 * 2 + // = 2 + let rewards_per_token_from0_till5 = + reward_per_token(reward_per_token_from0_till3, REWARD_RATE, alice, 3, 5); + assert_eq!(rewards_per_token_from0_till5, 2 * SCALING_FACTOR); + + // Bob deposits 200 at t=5; + let reward_per_token_from0_till7 = reward_per_token( + rewards_per_token_from0_till5, + REWARD_RATE, + alice + bob, + 5, + 7, + ); + // = r_j0 + R/T(t_j-t_j0) + // = 2 + 100/300 * 2 + // = 2 + 2/3 + assert_eq!(reward_per_token_from0_till7, 2666666666666666666u128); + + // Alice withdraws 100 at t=7; + let alice_reward_earned = rewards_earned(alice, reward_per_token_from0_till7, 0); + + // Expected value is: + // 2 full rewards for 2 units of time when she's the only farmer. + // 1/3 * 2 worth of reward for 2 units of time when she has 1/3 of shares. + // Scaled with SCALING_FACTOR for fixed point arithmetic. + // reward_rate(ALICE) = 8/3R = 2 2/3 R, where R=reward_rate + // rewards_earned(ALICE) = reward_rate(ALICE) * shares(ALICE) / SCALING_FACTOR + let alice_expected = 2666666666666666666u128 * alice / SCALING_FACTOR; + assert_eq!(alice_expected, alice_reward_earned); + + // Bob withdraws 200 at t=10; + // = r_j0 + R/T(t_j-t_j0) + // = r_j0 + REWARD_RATE/200 * 3 + // = r_j0 + 100*3/200 + // = r_j0 + 3/2 + // = 2 + 2/3 + 3/2 + // = 4 + 1/6 + let reward_per_token_from0_till10 = + reward_per_token(reward_per_token_from0_till7, REWARD_RATE, bob, 7, 10); + let expected_rate = 4 * SCALING_FACTOR + SCALING_FACTOR / 6; + assert_eq!(reward_per_token_from0_till10, expected_rate); + let bob_rewards_earned = rewards_earned(bob, reward_per_token_from0_till10, 0); + // 2/3 * 2 worth of reward for 3 units of time when he has 2/3 of shares. + // 3 full rewards for 3 units of time when he's the only farmer. + // rewards_earned(BOB) = reward_rate(BOB) * shares(BOB) / SCALING_FACTOR + let bob_expected = expected_rate * bob / SCALING_FACTOR; + assert_eq!(bob_expected, bob_rewards_earned); + } + + // ▲ + // 400│ ┌──────┐ ┌─┐ + // │ │CAROL │ │ │ + // 300│ ┌─┴──────┼──┘ │ + // │ │ │ C │ + // 200│ │ └────┼───┐ + // │ │ BOB │ │ + // │ ┌─────┴────────┐ │ C │ + // 100│ │ ALICE │ │ │ + // └────┴──────────────┴────┴───┴──────► + // 3 5 6 9 10 11 13 t + // + // t=3: Alice deposits 100 + // t=5: Bob deposits 200 + // t=6: Carol deposits 100 + // t=9: Alice withdraws 100 + // t=10: Carol deposits 100 + // t=11: Bob withdraws 200 + // t=13: Carol withdraws 200 + // + #[test] + fn three_farmers_overlap_topup() { + let alice = 100; + let bob = 200; + let carol = 100; + + let reward_per_token_from0_till5 = reward_per_token(0, REWARD_RATE, alice, 3, 5); + + let reward_per_token_from0_till6 = + reward_per_token(reward_per_token_from0_till5, REWARD_RATE, alice + bob, 5, 6); + + let reward_per_token_from0_till9 = reward_per_token( + reward_per_token_from0_till6, + REWARD_RATE, + alice + bob + carol, + 6, + 9, + ); + + let alice_reward_earned = rewards_earned(alice, reward_per_token_from0_till9, 0); + assert_eq!(alice_reward_earned, 37 * alice / 12); + + let reward_rate_from0_till10 = reward_per_token( + reward_per_token_from0_till9, + REWARD_RATE, + bob + carol, + 9, + 10, + ); + let expected_reward_rate = 3 * SCALING_FACTOR + (5 * SCALING_FACTOR) / 12; + assert_eq!(reward_rate_from0_till10, expected_reward_rate); + let new_carol = carol + 100; + let reward_rate_from0_till11 = reward_per_token( + reward_rate_from0_till10, + REWARD_RATE, + bob + new_carol, + 10, + 11, + ); + let expected_reward_rate = expected_reward_rate + (SCALING_FACTOR / 4); + assert_eq!(reward_rate_from0_till11, expected_reward_rate,); + let bob_earned = rewards_earned(bob, reward_rate_from0_till11, 0); + assert_eq!(bob_earned, bob * expected_reward_rate / SCALING_FACTOR); + } + } + + #[cfg(test)] + + mod farm_start { + use super::*; + + use ink::env::test::set_block_timestamp; + + fn pool_id() -> AccountId { + AccountId::from([0x01; 32]) + } + + fn farm() -> Farm { + Farm::new(pool_id()) + } + + fn single_reward_token() -> Vec { + vec![AccountId::from([0x02; 32])] + } + + #[ink::test] + fn new_creates_stopped_farm() { + let farm = farm(); + assert_eq!(farm.is_stopped, true); + } + + #[ink::test] + fn non_owner_cannot_start_farm() { + set_sender(alice()); + + let mut farm = farm(); + set_block_timestamp::(1); + let reward_tokens = single_reward_token(); + set_sender(bob()); + assert_eq!( + farm.start(5, vec![300], reward_tokens), + Err(FarmStartError::CallerNotOwner) + ); + } + + #[ink::test] + fn farm_end_before_start() { + let mut farm = farm(); + set_block_timestamp::(5); + let reward_tokens = single_reward_token(); + let reward_amounts = vec![300]; + assert_eq!( + farm.start(2, reward_amounts, reward_tokens), + Err(FarmStartError::FarmEndBeforeStart) + ); + } + + #[ink::test] + fn reward_amounts_and_tokens_mismatch() { + let mut farm = farm(); + let reward_tokens = single_reward_token(); + let reward_amounts = vec![300, 400]; + assert_eq!( + farm.start(10, reward_amounts, reward_tokens), + Err(FarmStartError::RewardAmountsAndTokenLengthDiffer) + ); + } + + #[ink::test] + fn fail_on_zero_reward_amount() { + let mut farm = farm(); + let reward_tokens = single_reward_token(); + let reward_amounts = vec![0]; + assert_eq!( + farm.start(10, reward_amounts, reward_tokens), + Err(FarmStartError::ZeroRewardAmount) + ); + } + + #[ink::test] + fn fail_on_insufficient_rewards() { + let mut farm = farm(); + let reward_tokens = single_reward_token(); + let reward_amounts = vec![10]; + // reward_rate = reward / duration + // rr = 10 / 100 == 0; + assert_eq!( + farm.start(100, reward_amounts, reward_tokens), + Err(FarmStartError::ZeroRewardRate) + ); + } } // Tests: // Deposit: - // - deposit with 0 balance fails - // - deposit with 0 amount fails // - deposit with non-zero balance succeeds // - deposit as first farmer takes all shares // - deposit triggers claim