diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 27c41c20..63046690 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -23,6 +23,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 94918128..2e3899a1 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -116,7 +116,7 @@ contracts - contracts + contracts contract_core @@ -211,6 +211,9 @@ platform + + contracts + diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 9826c336..d022bba6 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -149,7 +149,11 @@ struct __FunctionOrProcedureBeginEndGuard #define CONTRACT_INDEX QEARN_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QEARN #define CONTRACT_STATE2_TYPE QEARN2 +#ifdef QEARN_UPDATE #include "contracts/Qearn.h" +#else +#include "contracts/Qearn_old.h" +#endif #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE diff --git a/src/contracts/Qearn.h b/src/contracts/Qearn.h index c330fb05..16cc52a6 100644 --- a/src/contracts/Qearn.h +++ b/src/contracts/Qearn.h @@ -253,7 +253,7 @@ struct QEARN : public ContractBase }; - array statsInfo; + Array statsInfo; struct getStateOfRound_locals { uint32 firstEpoch; diff --git a/src/contracts/Qearn_old.h b/src/contracts/Qearn_old.h new file mode 100644 index 00000000..9e2ee051 --- /dev/null +++ b/src/contracts/Qearn_old.h @@ -0,0 +1,911 @@ +using namespace QPI; + +constexpr uint64 QEARN_MINIMUM_LOCKING_AMOUNT = 10000000; +constexpr uint64 QEARN_MAX_LOCKS = 4194304; +constexpr uint64 QEARN_MAX_EPOCHS = 4096; +constexpr uint64 QEARN_MAX_USERS = 131072; +constexpr uint64 QEARN_MAX_LOCK_AMOUNT = 1000000000000ULL; +constexpr uint64 QEARN_MAX_BONUS_AMOUNT = 1000000000000ULL; +constexpr uint64 QEARN_INITIAL_EPOCH = 138; + +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_0_3 = 0; +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_4_7 = 5; +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_8_11 = 5; +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_12_15 = 10; +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_16_19 = 15; +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_20_23 = 20; +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_24_27 = 25; +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_28_31 = 30; +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_32_35 = 35; +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_36_39 = 40; +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_40_43 = 45; +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_44_47 = 50; +constexpr uint64 QEARN_EARLY_UNLOCKING_PERCENT_48_51 = 55; + +constexpr uint64 QEARN_BURN_PERCENT_0_3 = 0; +constexpr uint64 QEARN_BURN_PERCENT_4_7 = 45; +constexpr uint64 QEARN_BURN_PERCENT_8_11 = 45; +constexpr uint64 QEARN_BURN_PERCENT_12_15 = 45; +constexpr uint64 QEARN_BURN_PERCENT_16_19 = 40; +constexpr uint64 QEARN_BURN_PERCENT_20_23 = 40; +constexpr uint64 QEARN_BURN_PERCENT_24_27 = 35; +constexpr uint64 QEARN_BURN_PERCENT_28_31 = 35; +constexpr uint64 QEARN_BURN_PERCENT_32_35 = 35; +constexpr uint64 QEARN_BURN_PERCENT_36_39 = 30; +constexpr uint64 QEARN_BURN_PERCENT_40_43 = 30; +constexpr uint64 QEARN_BURN_PERCENT_44_47 = 30; +constexpr uint64 QEARN_BURN_PERCENT_48_51 = 25; + +constexpr sint32 QEARN_INVALID_INPUT_AMOUNT = 0; +constexpr sint32 QEARN_LOCK_SUCCESS = 1; +constexpr sint32 QEARN_INVALID_INPUT_LOCKED_EPOCH = 2; +constexpr sint32 QEARN_INVALID_INPUT_UNLOCK_AMOUNT = 3; +constexpr sint32 QEARN_EMPTY_LOCKED = 4; +constexpr sint32 QEARN_UNLOCK_SUCCESS = 5; +constexpr sint32 QEARN_OVERFLOW_USER = 6; +constexpr sint32 QEARN_LIMIT_LOCK = 7; + +struct QEARN2 +{ +}; + +struct QEARN : public ContractBase +{ +public: + struct getLockInfoPerEpoch_input { + uint32 Epoch; /* epoch number to get information */ + }; + + struct getLockInfoPerEpoch_output { + uint64 lockedAmount; /* initial total locked amount in epoch */ + uint64 bonusAmount; /* initial bonus amount in epoch*/ + uint64 currentLockedAmount; /* total locked amount in epoch. exactly the amount excluding the amount unlocked early*/ + uint64 currentBonusAmount; /* bonus amount in epoch excluding the early unlocking */ + uint64 yield; /* Yield calculated by 10000000 multiple*/ + }; + + struct getUserLockedInfo_input { + id user; + uint32 epoch; + }; + + struct getUserLockedInfo_output { + uint64 lockedAmount; /* the amount user locked at input.epoch */ + }; + + /* + getStateOfRound FUNCTION + + getStateOfRound function returns following. + + 0 = open epoch,not started yet + 1 = running epoch + 2 = ended epoch(>52weeks) + */ + struct getStateOfRound_input { + uint32 epoch; + }; + + struct getStateOfRound_output { + uint32 state; + }; + + /* + getUserLockStatus FUNCTION + + the status will return the binary status. + 1101010010110101001011010100101101010010110101001001 + + 1 means locked in [index of 1] weeks ago. 0 means unlocked in [index of zero] weeks ago. + The frontend can get the status of locked in 52 epochs. in above binary status, + the frontend can know that user locked 0 week ago, 1 week ago, 3 weeks ago, 5, 8,10,11,13 weeks ago. + */ + struct getUserLockStatus_input { + id user; + }; + + struct getUserLockStatus_output { + uint64 status; + }; + + /* + getEndedStatus FUNCTION + + output.earlyRewardedAmount returns the amount rewarded by unlocking early at current epoch + output.earlyUnlockedAmount returns the amount unlocked by unlocking early at current epoch + output.fullyRewardedAmount returns the amount rewarded by unlocking fully at the end of previous epoch + output.fullyUnlockedAmount returns the amount unlocked by unlocking fully at the end of previous epoch + + let's assume that current epoch is 170, user unlocked the 15B qu totally at this epoch and he got the 30B qu of reward. + in this case, output.earlyUnlockedAmount = 15B qu, output.earlyRewardedAmount = 30B qu + if this user unlocks 3B qu additionally at this epoch and rewarded 6B qu, + in this case, output.earlyUnlockedAmount = 18B qu, output.earlyRewardedAmount = 36B qu + state.earlyUnlocker array would be initialized at the end of every epoch + + let's assume also that current epoch is 170, user got the 15B(locked amount for 52 weeks) + 10B(rewarded amount for 52 weeks) at the end of epoch 169. + in this case, output.fullyRewardedAmount = 10B, output.fullyUnlockedAmount = 15B + state.fullyUnlocker array would be decided with distributions at the end of every epoch + + state.earlyUnlocker, state.fullyUnlocker arrays would be initialized and decided by following expression at the END_EPOCH_WITH_LOCALS function. + state._earlyUnlockedCnt = 0; + state._fullyUnlockedCnt = 0; + */ + + struct getEndedStatus_input { + id user; + }; + + struct getEndedStatus_output { + uint64 fullyUnlockedAmount; + uint64 fullyRewardedAmount; + uint64 earlyUnlockedAmount; + uint64 earlyRewardedAmount; + }; + + struct lock_input { + }; + + struct lock_output { + sint32 returnCode; + }; + + struct unlock_input { + uint64 amount; /* unlocking amount */ + uint32 lockedEpoch; /* locked epoch */ + }; + + struct unlock_output { + sint32 returnCode; + }; + + struct getStatsPerEpoch_input { + uint32 epoch; + }; + + struct getStatsPerEpoch_output { + + uint64 earlyUnlockedAmount; + uint64 earlyUnlockedPercent; + uint64 totalLockedAmount; + uint64 averageAPY; + + }; + +protected: + + struct RoundInfo { + + uint64 _totalLockedAmount; // The initial total locked amount in any epoch. Max Epoch is 65535 + uint64 _epochBonusAmount; // The initial bonus amount per an epoch. Max Epoch is 65535 + + }; + + Array _initialRoundInfo; + Array _currentRoundInfo; + + struct EpochIndexInfo { + + uint32 startIndex; + uint32 endIndex; + }; + + Array _epochIndex; + + struct LockInfo { + + uint64 _lockedAmount; + id ID; + uint32 _lockedEpoch; + + }; + + Array locker; + + struct HistoryInfo { + + uint64 _unlockedAmount; + uint64 _rewardedAmount; + id _unlockedID; + + }; + + Array earlyUnlocker; + Array fullyUnlocker; + + uint32 _earlyUnlockedCnt; + uint32 _fullyUnlockedCnt; + + struct getStateOfRound_locals { + uint32 firstEpoch; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getStateOfRound) + if(input.epoch < QEARN_INITIAL_EPOCH) + { // non staking + output.state = 2; + return ; + } + if(input.epoch > qpi.epoch()) + { + output.state = 0; // opening round, not started yet + } + locals.firstEpoch = qpi.epoch() - 52; + if(input.epoch <= qpi.epoch() && input.epoch >= locals.firstEpoch) + { + output.state = 1; // running round, available unlocking early + } + if(input.epoch < locals.firstEpoch) + { + output.state = 2; // ended round + } + _ + + PUBLIC_FUNCTION(getLockInfoPerEpoch) + + output.bonusAmount = state._initialRoundInfo.get(input.Epoch)._epochBonusAmount; + output.lockedAmount = state._initialRoundInfo.get(input.Epoch)._totalLockedAmount; + output.currentBonusAmount = state._currentRoundInfo.get(input.Epoch)._epochBonusAmount; + output.currentLockedAmount = state._currentRoundInfo.get(input.Epoch)._totalLockedAmount; + if(state._currentRoundInfo.get(input.Epoch)._totalLockedAmount) + { + output.yield = state._currentRoundInfo.get(input.Epoch)._epochBonusAmount * 10000000ULL / state._currentRoundInfo.get(input.Epoch)._totalLockedAmount; + } + else + { + output.yield = 0ULL; + } + _ + + struct getStatsPerEpoch_locals + { + uint32 cnt, _t; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getStatsPerEpoch) + + output.earlyUnlockedAmount = state._initialRoundInfo.get(input.epoch)._totalLockedAmount - state._currentRoundInfo.get(input.epoch)._totalLockedAmount; + output.earlyUnlockedPercent = QPI::div(output.earlyUnlockedAmount * 10000ULL, state._initialRoundInfo.get(input.epoch)._totalLockedAmount); + + output.totalLockedAmount = 0; + output.averageAPY = 0; + locals.cnt = 0; + + for(locals._t = qpi.epoch() - 1U; locals._t >= qpi.epoch() - 52U; locals._t--) + { + if(locals._t < QEARN_INITIAL_EPOCH) + { + break; + } + if(state._currentRoundInfo.get(locals._t)._totalLockedAmount == 0) + { + continue; + } + + locals.cnt++; + output.totalLockedAmount += state._currentRoundInfo.get(locals._t)._totalLockedAmount; + output.averageAPY += QPI::div(state._currentRoundInfo.get(locals._t)._epochBonusAmount * 10000000ULL, state._currentRoundInfo.get(locals._t)._totalLockedAmount); + } + + output.averageAPY = QPI::div(output.averageAPY, locals.cnt * 1ULL); + + _ + + struct getUserLockedInfo_locals { + uint32 _t; + uint32 startIndex; + uint32 endIndex; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getUserLockedInfo) + + locals.startIndex = state._epochIndex.get(input.epoch).startIndex; + locals.endIndex = state._epochIndex.get(input.epoch).endIndex; + + for(locals._t = locals.startIndex; locals._t < locals.endIndex; locals._t++) + { + if(state.locker.get(locals._t).ID == input.user) + { + output.lockedAmount = state.locker.get(locals._t)._lockedAmount; + return; + } + } + _ + + struct getUserLockStatus_locals { + uint64 bn; + uint32 _t; + uint32 _r; + uint32 endIndex; + uint8 lockedWeeks; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getUserLockStatus) + + output.status = 0ULL; + locals.endIndex = state._epochIndex.get(qpi.epoch()).endIndex; + + for(locals._t = 0; locals._t < locals.endIndex; locals._t++) + { + if(state.locker.get(locals._t)._lockedAmount > 0 && state.locker.get(locals._t).ID == input.user) + { + + locals.lockedWeeks = qpi.epoch() - state.locker.get(locals._t)._lockedEpoch; + locals.bn = 1ULL< 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + locals.endIndex = state._epochIndex.get(qpi.epoch()).endIndex; + + for(locals.t = state._epochIndex.get(qpi.epoch()).startIndex ; locals.t < locals.endIndex; locals.t++) + { + + if(state.locker.get(locals.t).ID == qpi.invocator()) + { // the case to be locked several times at one epoch, at that time, this address already located in state.Locker array, the amount will be increased as current locking amount. + if(state.locker.get(locals.t)._lockedAmount + qpi.invocationReward() > QEARN_MAX_LOCK_AMOUNT) + { + output.returnCode = QEARN_LIMIT_LOCK; + if(qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + locals.newLocker._lockedAmount = state.locker.get(locals.t)._lockedAmount + qpi.invocationReward(); + locals.newLocker._lockedEpoch = qpi.epoch(); + locals.newLocker.ID = qpi.invocator(); + + state.locker.set(locals.t, locals.newLocker); + + locals.updatedRoundInfo._totalLockedAmount = state._initialRoundInfo.get(qpi.epoch())._totalLockedAmount + qpi.invocationReward(); + locals.updatedRoundInfo._epochBonusAmount = state._initialRoundInfo.get(qpi.epoch())._epochBonusAmount; + state._initialRoundInfo.set(qpi.epoch(), locals.updatedRoundInfo); + + locals.updatedRoundInfo._totalLockedAmount = state._currentRoundInfo.get(qpi.epoch())._totalLockedAmount + qpi.invocationReward(); + locals.updatedRoundInfo._epochBonusAmount = state._currentRoundInfo.get(qpi.epoch())._epochBonusAmount; + state._currentRoundInfo.set(qpi.epoch(), locals.updatedRoundInfo); + + output.returnCode = QEARN_LOCK_SUCCESS; // additional locking of this epoch is succeed + return ; + } + + } + + if(locals.endIndex == QEARN_MAX_LOCKS - 1) + { + output.returnCode = QEARN_OVERFLOW_USER; + if(qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; // overflow users in Qearn + } + + if(qpi.invocationReward() > QEARN_MAX_LOCK_AMOUNT) + { + output.returnCode = QEARN_LIMIT_LOCK; + if(qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + locals.newLocker.ID = qpi.invocator(); + locals.newLocker._lockedAmount = qpi.invocationReward(); + locals.newLocker._lockedEpoch = qpi.epoch(); + + state.locker.set(locals.endIndex, locals.newLocker); + + locals.tmpIndex.startIndex = state._epochIndex.get(qpi.epoch()).startIndex; + locals.tmpIndex.endIndex = locals.endIndex + 1; + state._epochIndex.set(qpi.epoch(), locals.tmpIndex); + + locals.updatedRoundInfo._totalLockedAmount = state._initialRoundInfo.get(qpi.epoch())._totalLockedAmount + qpi.invocationReward(); + locals.updatedRoundInfo._epochBonusAmount = state._initialRoundInfo.get(qpi.epoch())._epochBonusAmount; + state._initialRoundInfo.set(qpi.epoch(), locals.updatedRoundInfo); + + locals.updatedRoundInfo._totalLockedAmount = state._currentRoundInfo.get(qpi.epoch())._totalLockedAmount + qpi.invocationReward(); + locals.updatedRoundInfo._epochBonusAmount = state._currentRoundInfo.get(qpi.epoch())._epochBonusAmount; + state._currentRoundInfo.set(qpi.epoch(), locals.updatedRoundInfo); + + output.returnCode = QEARN_LOCK_SUCCESS; // new locking of this epoch is succeed + _ + + struct unlock_locals { + + RoundInfo updatedRoundInfo; + LockInfo updatedUserInfo; + HistoryInfo unlockerInfo; + + uint64 amountOfUnlocking; + uint64 amountOfReward; + uint64 amountOfburn; + uint64 rewardPercent; + sint64 divCalcu; + uint32 earlyUnlockingPercent; + uint32 burnPercent; + uint32 indexOfinvocator; + uint32 _t; + uint32 countOfLastVacancy; + uint32 countOfLockedEpochs; + uint32 startIndex; + uint32 endIndex; + + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(unlock) + + if (input.lockedEpoch > QEARN_MAX_EPOCHS || input.lockedEpoch < QEARN_INITIAL_EPOCH) + { + + output.returnCode = QEARN_INVALID_INPUT_LOCKED_EPOCH; // if user try to unlock with wrong locked epoch, it should be failed to unlock. + return ; + + } + + if(input.amount < QEARN_MINIMUM_LOCKING_AMOUNT) + { + + output.returnCode = QEARN_INVALID_INPUT_AMOUNT; + return ; + + } + + locals.indexOfinvocator = QEARN_MAX_LOCKS; + locals.startIndex = state._epochIndex.get(input.lockedEpoch).startIndex; + locals.endIndex = state._epochIndex.get(input.lockedEpoch).endIndex; + + for(locals._t = locals.startIndex ; locals._t < locals.endIndex; locals._t++) + { + + if(state.locker.get(locals._t).ID == qpi.invocator()) + { + if(state.locker.get(locals._t)._lockedAmount < input.amount) + { + + output.returnCode = QEARN_INVALID_INPUT_UNLOCK_AMOUNT; // if the amount to be wanted to unlock is more than locked amount, it should be failed to unlock + return ; + + } + else + { + locals.indexOfinvocator = locals._t; + break; + } + } + + } + + if(locals.indexOfinvocator == QEARN_MAX_LOCKS) + { + + output.returnCode = QEARN_EMPTY_LOCKED; // if there is no any locked info in state.Locker array, it shows this address didn't lock at the epoch (input.Locked_Epoch) + return ; + } + + /* the rest amount after unlocking should be more than MINIMUM_LOCKING_AMOUNT */ + if(state.locker.get(locals.indexOfinvocator)._lockedAmount - input.amount < QEARN_MINIMUM_LOCKING_AMOUNT) + { + locals.amountOfUnlocking = state.locker.get(locals.indexOfinvocator)._lockedAmount; + } + else + { + locals.amountOfUnlocking = input.amount; + } + + locals.countOfLockedEpochs = qpi.epoch() - input.lockedEpoch - 1; + + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_0_3; + locals.burnPercent = QEARN_BURN_PERCENT_0_3; + + if(locals.countOfLockedEpochs >= 4 && locals.countOfLockedEpochs <= 7) + { + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_4_7; + locals.burnPercent = QEARN_BURN_PERCENT_4_7; + } + + else if(locals.countOfLockedEpochs >= 8 && locals.countOfLockedEpochs <= 11) + { + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_8_11; + locals.burnPercent = QEARN_BURN_PERCENT_8_11; + } + + else if(locals.countOfLockedEpochs >= 12 && locals.countOfLockedEpochs <= 15) + { + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_12_15; + locals.burnPercent = QEARN_BURN_PERCENT_12_15; + } + + else if(locals.countOfLockedEpochs >= 16 && locals.countOfLockedEpochs <= 19) + { + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_16_19; + locals.burnPercent = QEARN_BURN_PERCENT_16_19; + } + + else if(locals.countOfLockedEpochs >= 20 && locals.countOfLockedEpochs <= 23) + { + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_20_23; + locals.burnPercent = QEARN_BURN_PERCENT_20_23; + } + + else if(locals.countOfLockedEpochs >= 24 && locals.countOfLockedEpochs <= 27) + { + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_24_27; + locals.burnPercent = QEARN_BURN_PERCENT_24_27; + } + + else if(locals.countOfLockedEpochs >= 28 && locals.countOfLockedEpochs <= 31) + { + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_28_31; + locals.burnPercent = QEARN_BURN_PERCENT_28_31; + } + + else if(locals.countOfLockedEpochs >= 32 && locals.countOfLockedEpochs <= 35) + { + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_32_35; + locals.burnPercent = QEARN_BURN_PERCENT_32_35; + } + + else if(locals.countOfLockedEpochs >= 36 && locals.countOfLockedEpochs <= 39) + { + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_36_39; + locals.burnPercent = QEARN_BURN_PERCENT_36_39; + } + + else if(locals.countOfLockedEpochs >= 40 && locals.countOfLockedEpochs <= 43) + { + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_40_43; + locals.burnPercent = QEARN_BURN_PERCENT_40_43; + } + + else if(locals.countOfLockedEpochs >= 44 && locals.countOfLockedEpochs <= 47) + { + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_44_47; + locals.burnPercent = QEARN_BURN_PERCENT_44_47; + } + + else if(locals.countOfLockedEpochs >= 48 && locals.countOfLockedEpochs <= 51) + { + locals.earlyUnlockingPercent = QEARN_EARLY_UNLOCKING_PERCENT_48_51; + locals.burnPercent = QEARN_BURN_PERCENT_48_51; + } + + locals.rewardPercent = QPI::div(state._currentRoundInfo.get(input.lockedEpoch)._epochBonusAmount * 10000000ULL, state._currentRoundInfo.get(input.lockedEpoch)._totalLockedAmount); + locals.divCalcu = QPI::div(locals.rewardPercent * locals.amountOfUnlocking , 100ULL); + locals.amountOfReward = QPI::div(locals.divCalcu * locals.earlyUnlockingPercent * 1ULL , 10000000ULL); + locals.amountOfburn = QPI::div(locals.divCalcu * locals.burnPercent * 1ULL, 10000000ULL); + + qpi.transfer(qpi.invocator(), locals.amountOfUnlocking + locals.amountOfReward); + qpi.burn(locals.amountOfburn); + + locals.updatedRoundInfo._totalLockedAmount = state._currentRoundInfo.get(input.lockedEpoch)._totalLockedAmount - locals.amountOfUnlocking; + locals.updatedRoundInfo._epochBonusAmount = state._currentRoundInfo.get(input.lockedEpoch)._epochBonusAmount - locals.amountOfReward - locals.amountOfburn; + + state._currentRoundInfo.set(input.lockedEpoch, locals.updatedRoundInfo); + + if(qpi.epoch() == input.lockedEpoch) + { + locals.updatedRoundInfo._totalLockedAmount = state._initialRoundInfo.get(input.lockedEpoch)._totalLockedAmount - locals.amountOfUnlocking; + locals.updatedRoundInfo._epochBonusAmount = state._initialRoundInfo.get(input.lockedEpoch)._epochBonusAmount; + + state._initialRoundInfo.set(input.lockedEpoch, locals.updatedRoundInfo); + } + + if(state.locker.get(locals.indexOfinvocator)._lockedAmount == locals.amountOfUnlocking) + { + locals.updatedUserInfo.ID = NULL_ID; + locals.updatedUserInfo._lockedAmount = 0; + locals.updatedUserInfo._lockedEpoch = 0; + } + else + { + locals.updatedUserInfo.ID = qpi.invocator(); + locals.updatedUserInfo._lockedAmount = state.locker.get(locals.indexOfinvocator)._lockedAmount - locals.amountOfUnlocking; + locals.updatedUserInfo._lockedEpoch = state.locker.get(locals.indexOfinvocator)._lockedEpoch; + } + + state.locker.set(locals.indexOfinvocator, locals.updatedUserInfo); + + if(state._currentRoundInfo.get(input.lockedEpoch)._totalLockedAmount == 0 && input.lockedEpoch != qpi.epoch()) + { + + // If all users have unlocked early, burn bonus + qpi.burn(state._currentRoundInfo.get(input.lockedEpoch)._epochBonusAmount); + + locals.updatedRoundInfo._totalLockedAmount = 0; + locals.updatedRoundInfo._epochBonusAmount = 0; + + state._currentRoundInfo.set(input.lockedEpoch, locals.updatedRoundInfo); + + } + + if(input.lockedEpoch != qpi.epoch()) + { + + locals.unlockerInfo._unlockedID = qpi.invocator(); + + for(locals._t = 0; locals._t < state._earlyUnlockedCnt; locals._t++) + { + if(state.earlyUnlocker.get(locals._t)._unlockedID == qpi.invocator()) + { + + locals.unlockerInfo._rewardedAmount = state.earlyUnlocker.get(locals._t)._rewardedAmount + locals.amountOfReward; + locals.unlockerInfo._unlockedAmount = state.earlyUnlocker.get(locals._t)._unlockedAmount + locals.amountOfUnlocking; + + state.earlyUnlocker.set(locals._t, locals.unlockerInfo); + + break; + } + } + + if(locals._t == state._earlyUnlockedCnt && state._earlyUnlockedCnt < QEARN_MAX_USERS) + { + locals.unlockerInfo._rewardedAmount = locals.amountOfReward; + locals.unlockerInfo._unlockedAmount = locals.amountOfUnlocking; + + state.earlyUnlocker.set(locals._t, locals.unlockerInfo); + state._earlyUnlockedCnt++; + } + + } + + output.returnCode = QEARN_UNLOCK_SUCCESS; // unlock is succeed + _ + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES + + REGISTER_USER_FUNCTION(getLockInfoPerEpoch, 1); + REGISTER_USER_FUNCTION(getUserLockedInfo, 2); + REGISTER_USER_FUNCTION(getStateOfRound, 3); + REGISTER_USER_FUNCTION(getUserLockStatus, 4); + REGISTER_USER_FUNCTION(getEndedStatus, 5); + REGISTER_USER_FUNCTION(getStatsPerEpoch, 6); + + REGISTER_USER_PROCEDURE(lock, 1); + REGISTER_USER_PROCEDURE(unlock, 2); + + _ + + struct BEGIN_EPOCH_locals + { + HistoryInfo INITIALIZE_HISTORY; + LockInfo INITIALIZE_USER; + RoundInfo INITIALIZE_ROUNDINFO; + + uint32 t; + bit status; + uint64 pre_epoch_balance; + uint64 current_balance; + ::Entity entity; + uint32 locked_epoch; + }; + + BEGIN_EPOCH_WITH_LOCALS + + qpi.getEntity(SELF, locals.entity); + locals.current_balance = locals.entity.incomingAmount - locals.entity.outgoingAmount; + + locals.pre_epoch_balance = 0ULL; + locals.locked_epoch = qpi.epoch() - 52; + for(locals.t = qpi.epoch() - 1; locals.t >= locals.locked_epoch; locals.t--) + { + locals.pre_epoch_balance += state._currentRoundInfo.get(locals.t)._epochBonusAmount + state._currentRoundInfo.get(locals.t)._totalLockedAmount; + } + + if(locals.current_balance - locals.pre_epoch_balance > QEARN_MAX_BONUS_AMOUNT) + { + qpi.burn(locals.current_balance - locals.pre_epoch_balance - QEARN_MAX_BONUS_AMOUNT); + locals.INITIALIZE_ROUNDINFO._epochBonusAmount = QEARN_MAX_BONUS_AMOUNT; + } + else + { + locals.INITIALIZE_ROUNDINFO._epochBonusAmount = locals.current_balance - locals.pre_epoch_balance; + } + locals.INITIALIZE_ROUNDINFO._totalLockedAmount = 0; + + state._initialRoundInfo.set(qpi.epoch(), locals.INITIALIZE_ROUNDINFO); + state._currentRoundInfo.set(qpi.epoch(), locals.INITIALIZE_ROUNDINFO); + + /* + the initial total locked amount should exclude the amount that locked on initial epoch but it didn't exclude that amount before. + so now it is updated. + I recorded the initial total locked amount of epoch 139 with 1834842583179. + This updates should be deployed on mainnet to chnage the initial infos of epoch 139. it will be deleted an epoch after deployment on mainnet. + */ + locals.INITIALIZE_ROUNDINFO._epochBonusAmount = state._initialRoundInfo.get(139)._epochBonusAmount; + locals.INITIALIZE_ROUNDINFO._totalLockedAmount = 1834842583179; + + state._initialRoundInfo.set(139, locals.INITIALIZE_ROUNDINFO); + _ + + struct END_EPOCH_locals + { + HistoryInfo INITIALIZE_HISTORY; + LockInfo INITIALIZE_USER; + RoundInfo INITIALIZE_ROUNDINFO; + EpochIndexInfo tmpEpochIndex; + + uint64 _rewardPercent; + uint64 _rewardAmount; + uint64 _burnAmount; + uint32 lockedEpoch; + uint32 startEpoch; + uint32 _t; + sint32 st; + sint32 en; + uint32 endIndex; + + }; + + END_EPOCH_WITH_LOCALS + + state._earlyUnlockedCnt = 0; + state._fullyUnlockedCnt = 0; + locals.lockedEpoch = qpi.epoch() - 52; + locals.endIndex = state._epochIndex.get(locals.lockedEpoch).endIndex; + + locals._burnAmount = state._currentRoundInfo.get(locals.lockedEpoch)._epochBonusAmount; + + locals._rewardPercent = QPI::div(state._currentRoundInfo.get(locals.lockedEpoch)._epochBonusAmount * 10000000ULL, state._currentRoundInfo.get(locals.lockedEpoch)._totalLockedAmount); + + for(locals._t = state._epochIndex.get(locals.lockedEpoch).startIndex; locals._t < locals.endIndex; locals._t++) + { + if(state.locker.get(locals._t)._lockedAmount == 0) + { + continue; + } + + ASSERT(state.locker.get(locals._t)._lockedEpoch == locals.lockedEpoch); + + locals._rewardAmount = QPI::div(state.locker.get(locals._t)._lockedAmount * locals._rewardPercent, 10000000ULL); + qpi.transfer(state.locker.get(locals._t).ID, locals._rewardAmount + state.locker.get(locals._t)._lockedAmount); + + if(state._fullyUnlockedCnt < QEARN_MAX_USERS) + { + + locals.INITIALIZE_HISTORY._unlockedID = state.locker.get(locals._t).ID; + locals.INITIALIZE_HISTORY._unlockedAmount = state.locker.get(locals._t)._lockedAmount; + locals.INITIALIZE_HISTORY._rewardedAmount = locals._rewardAmount; + + state.fullyUnlocker.set(state._fullyUnlockedCnt, locals.INITIALIZE_HISTORY); + + state._fullyUnlockedCnt++; + } + + locals.INITIALIZE_USER.ID = NULL_ID; + locals.INITIALIZE_USER._lockedAmount = 0; + locals.INITIALIZE_USER._lockedEpoch = 0; + + state.locker.set(locals._t, locals.INITIALIZE_USER); + + locals._burnAmount -= locals._rewardAmount; + } + + locals.tmpEpochIndex.startIndex = 0; + locals.tmpEpochIndex.endIndex = 0; + state._epochIndex.set(locals.lockedEpoch, locals.tmpEpochIndex); + + locals.startEpoch = locals.lockedEpoch + 1; + if (locals.startEpoch < QEARN_INITIAL_EPOCH) + locals.startEpoch = QEARN_INITIAL_EPOCH; + + // remove all gaps in Locker array (from beginning) and update epochIndex + locals.tmpEpochIndex.startIndex = 0; + for(locals._t = locals.startEpoch; locals._t <= qpi.epoch(); locals._t++) + { + // This for loop iteration moves all elements of one epoch the to start of its range of the Locker array. + // The startIndex is given by the end of the range of the previous epoch, the new endIndex is found in the + // gap removal process. + locals.st = locals.tmpEpochIndex.startIndex; + locals.en = state._epochIndex.get(locals._t).endIndex; + ASSERT(locals.st <= locals.en); + + while(locals.st < locals.en) + { + // try to set locals.st to first empty slot + while (state.locker.get(locals.st)._lockedAmount && locals.st < locals.en) + { + locals.st++; + } + + // try set locals.en to last non-empty slot in epoch + --locals.en; + while (!state.locker.get(locals.en)._lockedAmount && locals.st < locals.en) + { + locals.en--; + } + + // if st and en meet, there are no gaps to be closed by moving in this epoch range + if (locals.st >= locals.en) + { + // make locals.en point behind last element again + ++locals.en; + break; + } + + // move entry from locals.en to locals.st + state.locker.set(locals.st, state.locker.get(locals.en)); + + // make locals.en slot empty -> locals.en points behind last element again + locals.INITIALIZE_USER.ID = NULL_ID; + locals.INITIALIZE_USER._lockedAmount = 0; + locals.INITIALIZE_USER._lockedEpoch = 0; + state.locker.set(locals.en, locals.INITIALIZE_USER); + } + + // update epoch index + locals.tmpEpochIndex.endIndex = locals.en; + state._epochIndex.set(locals._t, locals.tmpEpochIndex); + + // set start index of next epoch to end index of current epoch + locals.tmpEpochIndex.startIndex = locals.tmpEpochIndex.endIndex; + } + + locals.tmpEpochIndex.endIndex = locals.tmpEpochIndex.startIndex; + state._epochIndex.set(qpi.epoch() + 1, locals.tmpEpochIndex); + + qpi.burn(locals._burnAmount); + _ +}; diff --git a/src/qubic.cpp b/src/qubic.cpp index 346a9b5d..92f209ed 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -1,5 +1,7 @@ #define SINGLE_COMPILE_UNIT +#define QEARN_UPDATE + // contract_def.h needs to be included first to make sure that contracts have minimal access #include "contract_core/contract_def.h" #include "contract_core/contract_exec.h" diff --git a/test/contract_qearn.cpp b/test/contract_qearn.cpp index 0ee1f6d3..e3ca52ea 100644 --- a/test/contract_qearn.cpp +++ b/test/contract_qearn.cpp @@ -1,3 +1,7 @@ +#define QEARN_UPDATE + +#ifdef QEARN_UPDATE + #define NO_UEFI #include @@ -887,3 +891,844 @@ TEST(TestContractQearn, RandomLockAndUnlock) testRandomLockWithUnlock(100, 20000, 10000, 8000); #endif } + +#else + +#define NO_UEFI + +#include +#include + +#include "contract_testing.h" + +#define PRINT_TEST_INFO 0 + +// test config: +// - 0 is fastest +// - 1 to enable more tests with random lock/unlock +// - 2 to enable even more tests with random lock/unlock +// - 3 to also check values more often (expensive functions) +// - 4 to also test out-of-user error +#define LARGE_SCALE_TEST 0 + + +static const id QEARN_CONTRACT_ID(QEARN_CONTRACT_INDEX, 0, 0, 0); + +static std::mt19937_64 rand64; + +static id getUser(unsigned long long i); +static unsigned long long random(unsigned long long maxValue); + +static std::vector fullyUnlockedAmount; +static std::vector fullyUnlockedUser; + + +class QearnChecker : public QEARN +{ +public: + void checkLockerArray(bool beforeEndEpoch, bool printInfo = false) + { + // check that locker array is in consistent state + std::map epochTotalLocked; + uint32 minEpoch = 0xffff; + uint32 maxEpoch = 0; + for (uint64 idx = 0; idx < locker.capacity(); ++idx) + { + const QEARN::LockInfo& lock = locker.get(idx); + if (lock._lockedAmount == 0) + { + EXPECT_TRUE(isZero(lock.ID)); + EXPECT_EQ(lock._lockedEpoch, 0); + } + else + { + EXPECT_GT(lock._lockedAmount, QEARN_MINIMUM_LOCKING_AMOUNT); + EXPECT_LE(lock._lockedAmount, QEARN_MAX_LOCK_AMOUNT); + EXPECT_FALSE(isZero(lock.ID)); + const QEARN::EpochIndexInfo& epochRange = _epochIndex.get(lock._lockedEpoch); + EXPECT_GE(idx, epochRange.startIndex); + EXPECT_LT(idx, epochRange.endIndex); + epochTotalLocked[lock._lockedEpoch] += lock._lockedAmount; + + minEpoch = std::min(minEpoch, lock._lockedEpoch); + maxEpoch = std::max(minEpoch, lock._lockedEpoch); + } + } + + const uint32 beginEpoch = std::max((int)contractDescriptions[QEARN_CONTRACT_INDEX].constructionEpoch, system.epoch - 52); + EXPECT_LE(beginEpoch, minEpoch); + EXPECT_LE(maxEpoch, uint32(system.epoch)); + + if (PRINT_TEST_INFO) + { + const char* beforeAfterStr = (beforeEndEpoch) ? "Before" : "After"; + std::cout << "--- " << beforeAfterStr << " END_EPOCH in epoch " << system.epoch << std::endl; + } + + for (uint32 epoch = beginEpoch; epoch <= system.epoch; ++epoch) + { + const QEARN::RoundInfo& currentRoundInfo = _currentRoundInfo.get(epoch); + //if (!currentRoundInfo._Epoch_Bonus_Amount && !currentRoundInfo._Total_Locked_Amount) + // continue; + unsigned long long totalLocked = epochTotalLocked[epoch]; + if (printInfo) + { + std::cout << "Total locked amount in epoch " << epoch << " = " << totalLocked << ", total bonus " << currentRoundInfo._epochBonusAmount << std::endl; + } + if (beforeEndEpoch || epoch != system.epoch - 52) + EXPECT_EQ(currentRoundInfo._totalLockedAmount, totalLocked); + } + + // check that old epoch indices have been reset + for (uint32 epoch = contractDescriptions[QEARN_CONTRACT_INDEX].constructionEpoch; epoch < beginEpoch; ++epoch) + { + EXPECT_EQ(this->_epochIndex.get(epoch).startIndex, this->_epochIndex.get(epoch).endIndex); + } + } + + void checkGetUnlockedInfo(uint32 epoch) + { + fullyUnlockedAmount.clear(); + fullyUnlockedUser.clear(); + + const QEARN::EpochIndexInfo& epochIndex = _epochIndex.get(epoch); + for (uint64 idx = epochIndex.startIndex; idx < epochIndex.endIndex; ++idx) + { + if (locker.get(idx)._lockedAmount != 0) + { + fullyUnlockedAmount.push_back(locker.get(idx)._lockedAmount); + fullyUnlockedUser.push_back(locker.get(idx).ID); + } + } + } + + void checkFullyUnlockedAmount() + { + for (uint32 idx = 0; idx < _fullyUnlockedCnt; idx++) + { + const QEARN::HistoryInfo& FullyUnlockedInfo = fullyUnlocker.get(idx); + + EXPECT_EQ(fullyUnlockedAmount[idx], FullyUnlockedInfo._unlockedAmount); + EXPECT_EQ(fullyUnlockedUser[idx], FullyUnlockedInfo._unlockedID); + } + } +}; + +class ContractTestingQearn : protected ContractTesting +{ + struct UnlockTableEntry + { + unsigned long long rewardPercent; + unsigned long long burnPercent; + }; + std::vector epochChangesToUnlockParams; + +public: + ContractTestingQearn() + { + INIT_CONTRACT(QEARN); + initEmptySpectrum(); + qLogger::initLogging(); + rand64.seed(42); + + for (unsigned int epChanges = 0; epChanges <= 52; ++epChanges) + { + if (epChanges <= 4) + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 0, 0 }); + else if (epChanges <= 12) + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 5, 45 }); + else if (epChanges <= 16) + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 10, 45 }); + else if (epChanges <= 20) + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 15, 40 }); + else if (epChanges <= 24) + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 20, 40 }); + else if (epChanges <= 28) + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 25, 35 }); + else if (epChanges <= 32) + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 30, 35 }); + else if (epChanges <= 36) + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 35, 35 }); + else if (epChanges <= 40) + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 40, 30 }); + else if (epChanges <= 44) + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 45, 30 }); + else if (epChanges <= 48) + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 50, 30 }); + else if (epChanges <= 52) + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 55, 25 }); + else + epochChangesToUnlockParams.push_back(UnlockTableEntry{ 100, 0 }); + } + } + + ~ContractTestingQearn() + { + qLogger::deinitLogging(); + } + + QearnChecker* getState() + { + return (QearnChecker*)contractStates[QEARN_CONTRACT_INDEX]; + } + + void beginEpoch(bool expectSuccess = true) + { + callSystemProcedure(QEARN_CONTRACT_INDEX, BEGIN_EPOCH, expectSuccess); + + // If there is no entry for this epoch in allEpochData, create one with default init (all 0) + allEpochData[system.epoch]; + } + + void endEpoch(bool expectSuccess = true) + { + callSystemProcedure(QEARN_CONTRACT_INDEX, END_EPOCH, expectSuccess); + } + + QEARN::getLockInfoPerEpoch_output getLockInfoPerEpoch(uint16 epoch) const + { + QEARN::getLockInfoPerEpoch_input input{ epoch }; + QEARN::getLockInfoPerEpoch_output output; + callFunction(QEARN_CONTRACT_INDEX, 1, input, output); + return output; + } + + uint64 getUserLockedInfo(uint16 epoch, const id& user) const + { + QEARN::getUserLockedInfo_input input; + input.epoch = epoch; + input.user = user; + QEARN::getUserLockedInfo_output output; + callFunction(QEARN_CONTRACT_INDEX, 2, input, output); + return output.lockedAmount; + } + + uint32 getStateOfRound(uint16 epoch) const + { + QEARN::getStateOfRound_input input{ epoch }; + QEARN::getStateOfRound_output output; + callFunction(QEARN_CONTRACT_INDEX, 3, input, output); + return output.state; + } + + uint64 getUserLockStatus(const id& user) const + { + QEARN::getUserLockStatus_input input{ user }; + QEARN::getUserLockStatus_output output; + callFunction(QEARN_CONTRACT_INDEX, 4, input, output); + return output.status; + } + + QEARN::getEndedStatus_output getEndedStatus(const id& user) const + { + QEARN::getEndedStatus_input input{ user }; + QEARN::getEndedStatus_output output; + callFunction(QEARN_CONTRACT_INDEX, 5, input, output); + return output; + } + + QEARN::getStatsPerEpoch_output getStatsPerEpoch(uint16 epoch) const + { + QEARN::getStatsPerEpoch_input input{ epoch }; + QEARN::getStatsPerEpoch_output output; + callFunction(QEARN_CONTRACT_INDEX, 6, input, output); + return output; + } + + sint32 lock(const id& user, long long amount, bool expectSuccess = true) + { + QEARN::lock_input input; + QEARN::lock_output output; + EXPECT_EQ(invokeUserProcedure(QEARN_CONTRACT_INDEX, 1, input, output, user, amount), expectSuccess); + return output.returnCode; + } + + sint32 unlock(const id& user, long long amount, uint16 lockedEpoch, bool expectSuccess = true) + { + QEARN::unlock_input input; + input.amount = amount; + input.lockedEpoch = lockedEpoch; + QEARN::unlock_output output; + EXPECT_EQ(invokeUserProcedure(QEARN_CONTRACT_INDEX, 2, input, output, user, 0), expectSuccess); + return output.returnCode; + } + + struct UserData + { + std::map locked; + }; + + std::map allUserData; + + struct EpochData + { + unsigned long long initialBonusAmount; + unsigned long long initialTotalLockedAmount; + unsigned long long bonusAmount; + unsigned long long amountCurrentlyLocked; + }; + + std::map allEpochData; + + std::map amountUnlockPerUser; + + void simulateDonation(const unsigned long long donationAmount) + { + increaseEnergy(QEARN_CONTRACT_ID, donationAmount); + + unsigned long long& totalBonusAmount = allEpochData[system.epoch + 1].bonusAmount; + totalBonusAmount += donationAmount; + if (totalBonusAmount > QEARN_MAX_BONUS_AMOUNT) + totalBonusAmount = QEARN_MAX_BONUS_AMOUNT; + } + + bool lockAndCheck(const id& user, uint64 amountLock, bool expectSuccess = true) + { + // check consistency of epoch info expected vs returned by contract + checkEpochInfo(system.epoch); + + // get amount and balances before action +#if LARGE_SCALE_TEST >= 3 + uint64 amountBefore = getUserLockedInfo(system.epoch, user); + EXPECT_EQ(allUserData[user].locked[system.epoch], amountBefore); +#else + uint64 amountBefore = allUserData[user].locked[system.epoch]; +#endif + sint64 userBalanceBefore = getBalance(user); + sint64 contractBalanceBefore = getBalance(QEARN_CONTRACT_ID); + + // call lock prcoedure + uint32 retCode = lock(user, amountLock, expectSuccess); + + // check new amount and balances + uint64 amountAfter = getUserLockedInfo(system.epoch, user); + sint64 userBalanceAfter = getBalance(user); + sint64 contractBalanceAfter = getBalance(QEARN_CONTRACT_ID); + if (retCode == QEARN_LOCK_SUCCESS && expectSuccess) + { + EXPECT_EQ(amountAfter, amountBefore + amountLock); + EXPECT_EQ(userBalanceAfter, userBalanceBefore - amountLock); + EXPECT_EQ(contractBalanceAfter, contractBalanceBefore + amountLock); + + allUserData[user].locked[system.epoch] += amountLock; + allEpochData[system.epoch].amountCurrentlyLocked += amountLock; + allEpochData[system.epoch].initialTotalLockedAmount += amountLock; + } + else + { + EXPECT_EQ(amountAfter, amountBefore); + EXPECT_EQ(userBalanceAfter, userBalanceBefore); + EXPECT_EQ(contractBalanceAfter, contractBalanceBefore); + } + + if (!expectSuccess) + return false; + + // check return code + if (retCode != QEARN_OVERFLOW_USER) + { + if (amountLock < QEARN_MINIMUM_LOCKING_AMOUNT || system.epoch < QEARN_INITIAL_EPOCH) + { + EXPECT_EQ(retCode, QEARN_INVALID_INPUT_AMOUNT); + } + else if (amountBefore + amountLock > QEARN_MAX_LOCK_AMOUNT) + { + EXPECT_EQ(retCode, QEARN_LIMIT_LOCK); + } + } + + return retCode == QEARN_LOCK_SUCCESS; + } + + unsigned long long getAndCheckRewardFactorTenmillionth(uint16 epoch) const + { + auto edIt = allEpochData.find(epoch); + EXPECT_NE(edIt, allEpochData.end()); + const EpochData& ed = edIt->second; + const unsigned long long rewardFactorTenmillionth = QPI::div(ed.bonusAmount * 10000000ULL, ed.amountCurrentlyLocked); + if (rewardFactorTenmillionth) + { + // detect overflow in computation of rewardFactorTenmillionth + const double rewardFactorTenmillionthDouble = ed.bonusAmount * 10000000.0 / ed.amountCurrentlyLocked; + double arthmeticError = double(rewardFactorTenmillionth) - rewardFactorTenmillionthDouble; + EXPECT_LT(fabs(arthmeticError), 1e5); + } + + return rewardFactorTenmillionth; + } + + void checkEpochInfo(uint16 epoch) const + { + const auto scEpochInfo = getLockInfoPerEpoch(epoch); + EXPECT_LE(scEpochInfo.currentBonusAmount, QEARN_MAX_BONUS_AMOUNT); + if (epoch < QEARN_INITIAL_EPOCH) + return; + auto edIt = allEpochData.find(epoch); + EXPECT_NE(edIt, allEpochData.end()); + const EpochData& ed = edIt->second; + EXPECT_EQ(getAndCheckRewardFactorTenmillionth(epoch), scEpochInfo.yield); + EXPECT_EQ(ed.bonusAmount, scEpochInfo.currentBonusAmount); + EXPECT_EQ(ed.amountCurrentlyLocked, scEpochInfo.currentLockedAmount); + + const auto scStatsInfo = getStatsPerEpoch(epoch); + /* + we can't test at epoch 139 because the value of state in QEarn SC was assigned by hardcoding. + To test the epoch 139, please remove the line 776~779 in QEarn SC. + */ + if (epoch != 139) + { + EXPECT_EQ(scStatsInfo.earlyUnlockedAmount, ed.initialTotalLockedAmount - ed.amountCurrentlyLocked); + EXPECT_EQ(scStatsInfo.earlyUnlockedPercent, QPI::div((ed.initialTotalLockedAmount - ed.amountCurrentlyLocked) * 10000, ed.initialTotalLockedAmount)); + } + uint64 totalLockedInSC = 0; + uint64 averageAPY = 0; + uint32 cnt = 0; + for (uint16 t = system.epoch; t >= system.epoch - 52; t--) + { + auto preEdIt = allEpochData.find(t); + const EpochData& preED = preEdIt->second; + if (t < QEARN_INITIAL_EPOCH) + { + break; + } + if (preED.amountCurrentlyLocked == 0) + { + continue; + } + + cnt++; + totalLockedInSC += preED.amountCurrentlyLocked; + EXPECT_EQ(getLockInfoPerEpoch(t).currentLockedAmount, preED.amountCurrentlyLocked); + averageAPY += QPI::div(preED.bonusAmount * 10000000ULL, preED.amountCurrentlyLocked); + } + EXPECT_EQ(scStatsInfo.totalLockedAmount, totalLockedInSC); + EXPECT_EQ(scStatsInfo.averageAPY, QPI::div(averageAPY, cnt * 1ULL)); + } + + bool unlockAndCheck(const id& user, uint16 lockingEpoch, uint64 amountUnlock, bool expectSuccess = true) + { + // make sure that user exists in spectrum + increaseEnergy(user, 1); + + // get old locked amount +#if LARGE_SCALE_TEST >= 3 + uint64 amountBefore = getUserLockedInfo(lockingEpoch, user); + EXPECT_EQ(allUserData[user].locked[lockingEpoch], amountBefore); +#else + uint64 amountBefore = allUserData[user].locked[lockingEpoch]; +#endif + sint64 userBalanceBefore = getBalance(user); + sint64 contractBalanceBefore = getBalance(QEARN_CONTRACT_ID); + + // call unlock prcoedure + uint32 retCode = unlock(user, amountUnlock, lockingEpoch); + + // check new locked amount and balances + uint64 amountAfter = getUserLockedInfo(lockingEpoch, user); + sint64 userBalanceAfter = getBalance(user); + sint64 contractBalanceAfter = getBalance(QEARN_CONTRACT_ID); + if (retCode == QEARN_UNLOCK_SUCCESS && expectSuccess) + { + EXPECT_GE(amountBefore, amountUnlock); + uint64 expectedAmountAfter = amountBefore - amountUnlock; + if (expectedAmountAfter < QEARN_MINIMUM_LOCKING_AMOUNT) + { + expectedAmountAfter = 0; + } + EXPECT_EQ(amountAfter, expectedAmountAfter); + uint64 amountUnlocked = amountBefore - amountAfter; + + uint16 epochTransitions = system.epoch - lockingEpoch; + unsigned long long rewardFactorTenmillionth = getAndCheckRewardFactorTenmillionth(lockingEpoch); + unsigned long long commonFactor = QPI::div(rewardFactorTenmillionth * amountUnlocked, 100ULL); + unsigned long long amountReward = QPI::div(commonFactor * epochChangesToUnlockParams[epochTransitions].rewardPercent, 10000000ULL); + unsigned long long amountBurn = QPI::div(commonFactor * epochChangesToUnlockParams[epochTransitions].burnPercent, 10000000ULL); + { + // Check for overflows + double commonFactorError = fabs((double(rewardFactorTenmillionth) * double(amountUnlocked) / 100.0) - commonFactor); + EXPECT_LT(commonFactorError, 1e3); + double amountRewardError = fabs((double(commonFactor) * double(epochChangesToUnlockParams[epochTransitions].rewardPercent) / 10000000.0) - amountReward); + EXPECT_LE(amountRewardError, 1); + double amountBurnError = fabs((double(commonFactor) * double(epochChangesToUnlockParams[epochTransitions].burnPercent) / 10000000.0) - amountBurn); + EXPECT_LE(amountBurnError, 1); + } + + allUserData[user].locked[lockingEpoch] -= amountUnlocked; + if (system.epoch == lockingEpoch) + { + allEpochData[lockingEpoch].initialTotalLockedAmount -= amountUnlocked; + } + allEpochData[lockingEpoch].amountCurrentlyLocked -= amountUnlocked; + allEpochData[lockingEpoch].bonusAmount -= amountReward + amountBurn; + + // Edge case of unlocking of all locked funds in previous epoch -> bonus added to next round + if (lockingEpoch != system.epoch && !allEpochData[lockingEpoch].amountCurrentlyLocked) + { + amountBurn += allEpochData[lockingEpoch].bonusAmount; + allEpochData[lockingEpoch].bonusAmount = 0; + } + + EXPECT_EQ(userBalanceAfter, userBalanceBefore + amountUnlocked + amountReward); + EXPECT_EQ(contractBalanceAfter, contractBalanceBefore - amountUnlocked - amountReward - amountBurn); + + // Check consistency of epoch info expected vs returned by contract + checkEpochInfo(lockingEpoch); + + // getEndedStatus() only included Early_Unlocked_Amount if unlocked after locking epoch + if (lockingEpoch != system.epoch) + { + amountUnlockPerUser[user] += amountUnlocked; + } + } + else + { + EXPECT_EQ(amountAfter, amountBefore); + EXPECT_EQ(userBalanceAfter, userBalanceBefore); + EXPECT_EQ(contractBalanceAfter, contractBalanceBefore); + } + + return retCode == QEARN_UNLOCK_SUCCESS; + } + + void endEpochAndCheck() + { + // check getStateOfRound + uint16 payoutEpoch = system.epoch - 52; + EXPECT_EQ(getStateOfRound(QEARN_INITIAL_EPOCH - 1), 2); + EXPECT_EQ(getStateOfRound(payoutEpoch - 1), 2); + EXPECT_EQ(getStateOfRound(payoutEpoch), (payoutEpoch >= QEARN_INITIAL_EPOCH) ? 1 : 2); + EXPECT_EQ(getStateOfRound(system.epoch - 1), (system.epoch - 1 >= QEARN_INITIAL_EPOCH) ? 1 : 2); + EXPECT_EQ(getStateOfRound(system.epoch), (system.epoch >= QEARN_INITIAL_EPOCH) ? 1 : 2); + EXPECT_EQ(getStateOfRound(system.epoch + 1), (system.epoch + 1 >= QEARN_INITIAL_EPOCH) ? 0 : 2); + + // test getUserLockStatus() + { + id user = getUser(random(10)); + uint64 lockStatus = getUserLockStatus(user); + const auto userDataIt = allUserData.find(user); + if (userDataIt == allUserData.end()) + { + EXPECT_EQ(lockStatus, 0); + } + else + { + auto& userData = userDataIt->second; + for (int i = 0; i <= 52; ++i) + { + if (lockStatus & 1) + { + EXPECT_GT(userData.locked[system.epoch - i], 0ll); + } + else + { + EXPECT_EQ(userData.locked[system.epoch - i], 0ll); + } + lockStatus >>= 1; + } + } + } + + // check unlocked amounts returned by getEndedStatus() + for (const auto& userAmountPairs : amountUnlockPerUser) + { + QEARN::getEndedStatus_output endedStatus = getEndedStatus(userAmountPairs.first); + EXPECT_EQ(userAmountPairs.second, endedStatus.earlyUnlockedAmount); + } + + checkEpochInfo(system.epoch); + + bool beforeEndEpoch = true; + getState()->checkLockerArray(beforeEndEpoch, PRINT_TEST_INFO); + getState()->checkGetUnlockedInfo(payoutEpoch); + + // get entity balances to check payout in END_EPOCH + std::map oldUserBalance; + long long oldContractBalance = getBalance(QEARN_CONTRACT_ID); + for (const auto& userIdDataPair : allUserData) + { + const id& user = userIdDataPair.first; + oldUserBalance[user] = getBalance(user); + } + checkEpochInfo(payoutEpoch); + + amountUnlockPerUser.clear(); + endEpoch(); + + // check payout after END_EPOCH + bool expectPayout = (allEpochData.find(payoutEpoch) != allEpochData.end()); + checkEpochInfo(payoutEpoch); + if (expectPayout) + { + // compute and check expected payouts + unsigned long long rewardFactorTenmillionth = getAndCheckRewardFactorTenmillionth(payoutEpoch); + unsigned long long totalRewardsPaid = 0; + const EpochData& ed = allEpochData[payoutEpoch]; + + for (const auto& userIdBalancePair : oldUserBalance) + { + const id& user = userIdBalancePair.first; + const long long oldUserBalance = userIdBalancePair.second; + const UserData& userData = allUserData[user]; + + auto userLockedAmountIter = userData.locked.find(payoutEpoch); + if (userLockedAmountIter == userData.locked.end() || userLockedAmountIter->second == 0) + continue; + const long long userLockedAmount = userLockedAmountIter->second; + const unsigned long long userReward = userLockedAmount * rewardFactorTenmillionth / 10000000ULL; + if (rewardFactorTenmillionth) + EXPECT_EQ((userLockedAmount * rewardFactorTenmillionth) / rewardFactorTenmillionth, userLockedAmount); + totalRewardsPaid += userReward; + + EXPECT_EQ(oldUserBalance + userLockedAmount + userReward, getBalance(user)); + } + EXPECT_EQ(oldContractBalance - ed.bonusAmount - ed.amountCurrentlyLocked, getBalance(QEARN_CONTRACT_ID)); + + + // all the bonus that has not been paid is burned (remainder due to inaccurate arithmetic and full bonus if nothing is locked until the end) + EXPECT_LE(totalRewardsPaid, ed.bonusAmount); + if (ed.amountCurrentlyLocked && ed.bonusAmount) + EXPECT_GE(QPI::div(totalRewardsPaid * 1000, ed.bonusAmount), 998); // only small part of bonus should be burned + else + EXPECT_EQ(totalRewardsPaid, 0ull); + } + else + { + // no payout expected + for (const auto& userIdBalancePair : oldUserBalance) + { + const id& user = userIdBalancePair.first; + const long long oldUserBalance = userIdBalancePair.second; + const long long currentUserBalance = getBalance(user); + EXPECT_EQ(oldUserBalance, currentUserBalance); + } + EXPECT_EQ(oldContractBalance, getBalance(QEARN_CONTRACT_ID)); + } + + beforeEndEpoch = false; + getState()->checkLockerArray(beforeEndEpoch, PRINT_TEST_INFO); + getState()->checkFullyUnlockedAmount(); + } +}; + + +static id getUser(unsigned long long i) +{ + return id(i, i / 2 + 4, i + 10, i * 3 + 8); +} + +static unsigned long long random(unsigned long long maxValue) +{ + return rand64() % (maxValue + 1); +} + +static std::vector getRandomUsers(unsigned int totalUsers, unsigned int maxNum) +{ + unsigned long long userCount = random(maxNum); + std::vector users; + users.reserve(userCount); + for (unsigned int i = 0; i < userCount; ++i) + { + unsigned long long userIdx = random(totalUsers - 1); + users.push_back(getUser(userIdx)); + } + return users; +} + + +TEST(TestContractQearn, ErrorChecking) +{ + ContractTestingQearn qearn; + id user(1, 2, 3, 4); + + system.epoch = QEARN_INITIAL_EPOCH - 1; + + qearn.beginEpoch(); + + // special test case: trying to lock/unlock before QEARN_INITIAL_EPOCH must fail + { + id user2(98765, 43, 2, 1); + increaseEnergy(user2, QEARN_MAX_LOCK_AMOUNT); + EXPECT_FALSE(qearn.lockAndCheck(user2, QEARN_MAX_LOCK_AMOUNT)); + EXPECT_EQ(qearn.unlock(user2, QEARN_MAX_LOCK_AMOUNT, system.epoch), QEARN_INVALID_INPUT_LOCKED_EPOCH); + } + + qearn.endEpoch(); + + system.epoch = QEARN_INITIAL_EPOCH; + + qearn.beginEpoch(); + + // test cases, for which procedures is not executed: + { + // 1. non-existing entities = invalid ID) + EXPECT_FALSE(qearn.lockAndCheck(id::zero(), QEARN_MAX_LOCK_AMOUNT, false)); + EXPECT_FALSE(qearn.lockAndCheck(user, QEARN_MAX_LOCK_AMOUNT, false)); + + // 2. valid ID but negative amount / insufficient balance + increaseEnergy(user, 1); + EXPECT_FALSE(qearn.lockAndCheck(user, -10, false)); + EXPECT_FALSE(qearn.lockAndCheck(user, QEARN_MINIMUM_LOCKING_AMOUNT, false)); + } + + // test cases, for which procedure is executed (valid ID, enough balance) + increaseEnergy(user, QEARN_MAX_LOCK_AMOUNT * 1000); + { + EXPECT_FALSE(qearn.lockAndCheck(user, 0)); + EXPECT_FALSE(qearn.lockAndCheck(user, QEARN_MINIMUM_LOCKING_AMOUNT / 2)); + EXPECT_FALSE(qearn.lockAndCheck(user, QEARN_MINIMUM_LOCKING_AMOUNT - 1)); + + EXPECT_FALSE(qearn.lockAndCheck(user, QEARN_MAX_LOCK_AMOUNT + 1)); + EXPECT_FALSE(qearn.lockAndCheck(user, QEARN_MAX_LOCK_AMOUNT * 2)); + } + + // in order trigger out-of-lock-slots error, lock with many users +#if LARGE_SCALE_TEST >= 4 + // notes: - disabled by default because it takes long + // - seems like the last locker slot is never used in QEARN (FIXME) + for (uint64 i = 0; i < QEARN_MAX_LOCKS - 1; ++i) + { + id otherUser(i, 42, 1234, 642); + long long amount = QEARN_MINIMUM_LOCKING_AMOUNT + (7 * i) % (QEARN_MAX_LOCK_AMOUNT - QEARN_MINIMUM_LOCKING_AMOUNT); + increaseEnergy(otherUser, amount); + EXPECT_TRUE(qearn.lockAndCheck(otherUser, amount)); + } + EXPECT_FALSE(qearn.lockAndCheck(user, QEARN_MINIMUM_LOCKING_AMOUNT)); +#endif + + // note: lock implements no checking of system.epoch + + // for unlock, successfully lock some funds + id otherUser(1, 42, 1234, 642); + long long amount = QEARN_MINIMUM_LOCKING_AMOUNT; + increaseEnergy(otherUser, amount); + EXPECT_TRUE(qearn.lockAndCheck(otherUser, amount)); + + // unlock with too high amount + EXPECT_EQ(qearn.unlock(otherUser, QEARN_MAX_LOCK_AMOUNT + 1, system.epoch), QEARN_INVALID_INPUT_UNLOCK_AMOUNT); + + // unlock with too low amount + EXPECT_EQ(qearn.unlock(otherUser, QEARN_MINIMUM_LOCKING_AMOUNT - 1, system.epoch), QEARN_INVALID_INPUT_AMOUNT); + + // unlock with wrong user + EXPECT_EQ(qearn.unlock(user, QEARN_MINIMUM_LOCKING_AMOUNT, system.epoch), QEARN_EMPTY_LOCKED); + + // unlock with wrong epoch + EXPECT_EQ(qearn.unlock(otherUser, QEARN_MINIMUM_LOCKING_AMOUNT, 1), QEARN_INVALID_INPUT_LOCKED_EPOCH); + EXPECT_EQ(qearn.unlock(otherUser, QEARN_MINIMUM_LOCKING_AMOUNT, QEARN_MAX_EPOCHS + 1), QEARN_INVALID_INPUT_LOCKED_EPOCH); + + // finally, test success case + EXPECT_EQ(qearn.unlock(otherUser, QEARN_MINIMUM_LOCKING_AMOUNT, system.epoch), QEARN_UNLOCK_SUCCESS); +} + +void testRandomLockWithoutUnlock(const uint16 numEpochs, const unsigned int totalUsers, const unsigned int maxUserLocking) +{ + std::cout << "random test without early unlock for " << numEpochs << " epochs with " << totalUsers << " total users and up to " << maxUserLocking << " lock calls per epoch" << std::endl; + ContractTestingQearn qearn; + + const uint16 firstEpoch = contractDescriptions[QEARN_CONTRACT_INDEX].constructionEpoch; + const uint16 lastEpoch = firstEpoch + numEpochs; + + // first epoch is without donation/bonus + for (system.epoch = firstEpoch; system.epoch <= lastEpoch; ++system.epoch) + { + // invoke BEGIN_EPOCH + qearn.beginEpoch(); + + // simulate a random additional donation during the epoch + qearn.simulateDonation(random(ISSUANCE_RATE / 2)); + + // locking + auto lockUsers = getRandomUsers(totalUsers, maxUserLocking); + for (const auto& user : lockUsers) + { + // get random amount for locking and make sure that user has enough qus (may be invalid amount for locking) + uint64 amountLock = random(QEARN_MAX_LOCK_AMOUNT * 4 / 3); + increaseEnergy(user, amountLock); + + qearn.lockAndCheck(user, amountLock); + } + + // invoke END_EPOCH and check correct payouts + qearn.endEpochAndCheck(); + + // send revenue donation to qearn contract (happens after END_EPOCH but before system.epoch is incremented and before BEGIN_EPOCH + qearn.simulateDonation(random(ISSUANCE_RATE)); + } +} + +TEST(TestContractQearn, RandomLockWithoutUnlock) +{ + // params: epochs, total number of users, max users locking in epoch + testRandomLockWithoutUnlock(100, 40, 10); + testRandomLockWithoutUnlock(100, 20, 20); +#if LARGE_SCALE_TEST >= 1 + testRandomLockWithoutUnlock(300, 1000, 1000); +#endif +#if LARGE_SCALE_TEST >= 2 + testRandomLockWithoutUnlock(100, 20000, 10000); +#endif +} + +void testRandomLockWithUnlock(const uint16 numEpochs, const unsigned int totalUsers, const unsigned int maxUserLocking, const unsigned int maxUserUnlocking) +{ + std::cout << "random test with early unlock for " << numEpochs << " epochs with " << totalUsers << " total users, up to " << maxUserLocking << " lock calls (per epoch), and up to " << maxUserUnlocking << " unlock calls (per running round)" << std::endl; + ContractTestingQearn qearn; + + const uint16 firstEpoch = contractDescriptions[QEARN_CONTRACT_INDEX].constructionEpoch; + const uint16 lastEpoch = firstEpoch + numEpochs; + + for (system.epoch = firstEpoch; system.epoch <= lastEpoch; ++system.epoch) + { + // invoke BEGIN_EPOCH + qearn.beginEpoch(); + + // simulate a random additional donation during the epoch + qearn.simulateDonation(random(ISSUANCE_RATE / 2)); + + // locking + auto lockUsers = getRandomUsers(totalUsers, maxUserLocking); + for (const auto& user : lockUsers) + { + // get random amount for locking and make sure that user has enough qus (may be invalid amount for locking) + uint64 amountLock = random(QEARN_MAX_LOCK_AMOUNT * 4 / 3); + increaseEnergy(user, amountLock); + + qearn.lockAndCheck(user, amountLock); + } + + // unlocking + auto unlockUsers = getRandomUsers(totalUsers, maxUserUnlocking); + for (const auto& user : unlockUsers) + { + for (sint32 lockedEpoch = system.epoch; lockedEpoch >= system.epoch - 52; lockedEpoch--) + { + uint64 amountUnlock = random(qearn.allUserData[user].locked[lockedEpoch] * 11 / 10); + qearn.unlockAndCheck(user, lockedEpoch, amountUnlock); + } + } + + // invoke END_EPOCH and check correct payouts + qearn.endEpochAndCheck(); + + // send revenue donation to qearn contract (happens after END_EPOCH but before system.epoch is incremented and before BEGIN_EPOCH + qearn.simulateDonation(random(ISSUANCE_RATE)); + } +} + +TEST(TestContractQearn, RandomLockAndUnlock) +{ + // params: epochs, total number of users, max users locking in epoch, maxUserUnlocking + testRandomLockWithUnlock(100, 40, 10, 10); + testRandomLockWithUnlock(100, 40, 10, 8); // less early unlocking + testRandomLockWithUnlock(100, 40, 20, 19); // more user activity +#if LARGE_SCALE_TEST >= 1 + testRandomLockWithUnlock(300, 1000, 1000, 1000); + testRandomLockWithUnlock(300, 1000, 1000, 800); +#endif +#if LARGE_SCALE_TEST >= 2 + testRandomLockWithUnlock(400, 2000, 1500, 1200); + testRandomLockWithUnlock(100, 20000, 10000, 8000); +#endif +} + +#endif