Skip to content

Commit

Permalink
fix: JMC-1 Rewards for Users not Present in JettonMasterChef
Browse files Browse the repository at this point in the history
cannot be Extracted
  • Loading branch information
ipromise2324 committed Apr 30, 2024
1 parent 8df2d0d commit 1644f41
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 84 deletions.
32 changes: 22 additions & 10 deletions contracts/jetton_master_chef.tact
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ contract JettonMasterChef with Deployable, MasterChef {
startTime: Int as uint64 = 0;
metaData: Cell;
getRedundant: Bool = false; // The flag to get the redundant data
totalLpSupply: Int = 0; // Caculated total LP supply(In each pool)
reduntantReward: Int = 0; // The redundant reward for the owner
lastWithdrawTime: Int = 0; // The last withdraw time

// For Reward Jetton
mcRewardJettonWallet: Address;
Expand Down Expand Up @@ -83,24 +86,21 @@ contract JettonMasterChef with Deployable, MasterChef {

return;
}

// check if pool exists or exceed the deadline
if(self.pools.get(ctx.sender) == null || now() > self.deadline || now() < self.startTime || ctx.value < ton("0.04")){
if(self.pools.get(ctx.sender) == null || now() > self.deadline || now() < self.startTime || ctx.value < ton("0.065")){
// Send the jetton back to the sender if pool does not exist
self.sendJetton(ctx.sender, 0, msg.amount, msg.sender, msg.sender, SendRemainingValue);
return;
}
// Deposit the LP tokens for the user
self.userDeposit(msg, ctx);

// Send the redundant jetton back to the owner
if(self.getRedundant == false) {
if(myBalance() >= ton("0.05")) {
let rewardBack: Int = (now() - self.startTime) * self.rewardPerSecond;
self.sendJetton(self.mcRewardJettonWallet, ton("0.05"), rewardBack, self.owner, self.owner, 0);
}
self.getRedundant = true;
if(self.totalLpSupply == 0) {
self.reduntantReward = self.reduntantReward + (now() - self.lastWithdrawTime) * self.rewardPerSecond;
}

// Deposit the LP tokens for the user
self.userDeposit(msg, ctx);

}

// MiniChef will send this message to ask MasterChef to send reward
Expand All @@ -123,12 +123,24 @@ contract JettonMasterChef with Deployable, MasterChef {
// Withdraw lpToken to msg.beneficiary
let pool: PoolInfo = self.pools.get(msg.lpTokenAddress)!!;
pool.lpSupply = pool.lpSupply - msg.withdrawAmount;
self.totalLpSupply = self.totalLpSupply - msg.withdrawAmount;
self.lastWithdrawTime = now();
self.pools.set(ctx.sender, pool);
self.sendJetton(pool.lpTokenAddress, ctx.value / 2, msg.withdrawAmount, msg.beneficiary, msg.beneficiary, 0);

}
}

receive("Redeem") {
if(self.totalLpSupply == 0) {
let time: Int = min(now(), self.deadline);
self.reduntantReward = self.reduntantReward + (time - self.lastWithdrawTime) * self.rewardPerSecond;
self.lastWithdrawTime = now();
}
self.sendJetton(self.mcRewardJettonWallet, 0, self.reduntantReward, self.owner, self.owner, SendRemainingValue);
self.reduntantReward = 0;
}

// Get Methods //

// Get JettonMasterChef Data
Expand Down
5 changes: 5 additions & 0 deletions contracts/messages.tact
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ struct MasterChefData {
totalReward: Int as coins; // Total reward
}

struct RedeemData {
reduntantReward: Int as coins; // Redundant reward,
totalLpSupply: Int as coins;
lastWithdrawTime: Int;
}

// Constants
const THUNDER_FEE: Int = ton("0.01"); // User have to pay the fee to ThunderMint
Expand Down
30 changes: 21 additions & 9 deletions contracts/ton_master_chef.tact
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ contract TonMasterChef with Deployable, MasterChef {
startTime: Int as uint64 = 0;
metaData: Cell;
getRedundant: Bool = false; // The flag to get the redundant data
totalLpSupply: Int = 0; // Caculated total LP supply(In each pool)
reduntantReward: Int = 0; // The redundant reward for the owner
lastWithdrawTime: Int = 0; // The last withdraw time

// For Reward TON
rewardPerSecond: Int as coins = 0;
Expand Down Expand Up @@ -90,17 +93,13 @@ contract TonMasterChef with Deployable, MasterChef {
return;
}

// Send the redundant ton back to the owner
if(self.totalLpSupply == 0) {
self.reduntantReward = self.reduntantReward + (now() - self.lastWithdrawTime) * self.rewardPerSecond;
}

// Deposit the LP tokens for the user
self.userDeposit(msg, ctx);

// Send the redundant jetton back to the owner
if(self.getRedundant == false) {
let rewardBack: Int = (now() - self.startTime) * self.rewardPerSecond;
if(rewardBack >= ton("0.01")) {
self.sendTon(self.owner, rewardBack, 0);
}
self.getRedundant = true;
}
}

// MiniChef will send this message to ask MasterChef to send reward
Expand All @@ -125,11 +124,23 @@ contract TonMasterChef with Deployable, MasterChef {
// Withdraw to msg.beneficiary
let pool: PoolInfo = self.pools.get(msg.lpTokenAddress)!!;
pool.lpSupply = pool.lpSupply - msg.withdrawAmount;
self.totalLpSupply = self.totalLpSupply - msg.withdrawAmount;
self.lastWithdrawTime = now();
self.pools.set(ctx.sender, pool);
self.sendJetton(pool.lpTokenAddress, sendTon - THUNDER_FEE, msg.withdrawAmount, msg.beneficiary, msg.beneficiary, SendIgnoreErrors);
}
}

receive("Redeem") {
if(self.totalLpSupply == 0) {
let time: Int = min(now(), self.deadline);
self.reduntantReward = self.reduntantReward + (time - self.lastWithdrawTime) * self.rewardPerSecond;
self.lastWithdrawTime = now();
}
self.sendTon(self.owner, self.reduntantReward, SendRemainingValue);
self.reduntantReward = 0;
}

// Get Methods //

// Get TonMasterChef Data
Expand All @@ -149,4 +160,5 @@ contract TonMasterChef with Deployable, MasterChef {
};
}


}
20 changes: 18 additions & 2 deletions contracts/trait_master_chef.tact
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ trait MasterChef {
metaData: Cell; // The meta data of the contract, such as the name, image url, description, etc.
startTime: Int = 0; // The time when the reward distribution starts (initialize in basic setup)
getRedundant: Bool = false; // The flag to get the redundant data
totalLpSupply: Int = 0; // Caculated total LP supply(In each pool)
reduntantReward: Int = 0; // The redundant reward for the owner
lastWithdrawTime: Int = 0; // The last withdraw time

// For Reward jetton
rewardPerSecond: Int = 0; // The reward per second (coins), so called RPS.
Expand Down Expand Up @@ -86,6 +89,8 @@ trait MasterChef {
if(msg.amount > 0) {
let pool: PoolInfo = self.pools.get(msg.lpTokenAddress)!!;
pool.lpSupply = pool.lpSupply - msg.amount;
self.totalLpSupply = self.totalLpSupply - msg.amount;
self.lastWithdrawTime = now();
self.pools.set(msg.lpTokenAddress, pool);
self.sendJetton(pool.lpTokenAddress, 0, msg.amount, msg.beneficiary, msg.beneficiary, SendRemainingValue);
}
Expand Down Expand Up @@ -183,6 +188,7 @@ trait MasterChef {
self.totalReward = _totalReward;
self.deadline = _deadline;
self.startTime = _startTime;
self.lastWithdrawTime = _startTime;
self.rewardPerSecond = self.totalReward / (self.deadline - self.startTime);
}

Expand All @@ -193,8 +199,7 @@ trait MasterChef {
// check if pool exists
require(self.pools.get(lpTokenAddress) != null , "pool not exists");

require(_value >= ton("0.075"), "value not enough");

require(_value >= ton("0.065"), "value not enough");

// should update accRewardPerShare first and get the pool info
self.updatePool(lpTokenAddress);
Expand All @@ -218,6 +223,7 @@ trait MasterChef {
self.updatePool(ctx.sender);
let pool: PoolInfo = self.pools.get(ctx.sender)!!;
pool.lpSupply = pool.lpSupply + msg.amount;
self.totalLpSupply = self.totalLpSupply + msg.amount;
self.pools.set(ctx.sender, pool);
// Get the MiniChef init code for the user
let initCode: StateInit = self._calculateMiniChefInit(msg.sender);
Expand Down Expand Up @@ -334,4 +340,14 @@ trait MasterChef {
}
return 0;
}


// get RedeemData
get fun getRedeemData(): RedeemData {
return RedeemData{
reduntantReward: self.reduntantReward,
totalLpSupply: self.totalLpSupply,
lastWithdrawTime: self.lastWithdrawTime
};
}
}
139 changes: 97 additions & 42 deletions tests/JettonMasterChef.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('Jetton MasterChef Tests', () => {
let rewardPerSecond: bigint;
let seed: bigint;
let deadline: bigint;
let startTime: bigint;
let totalReward: bigint;
let masterChefJettonWalletAddress: Address;
const gasFile = 'JettonMasterChefCosts.txt';
Expand Down Expand Up @@ -332,6 +333,7 @@ describe('Jetton MasterChef Tests', () => {
); // MasterChef USDT JettonWallet

deadline = BigInt(blockchain.now!! + 2000);
startTime = BigInt(blockchain.now!!) - 10n;
totalReward = 1000n * 10n ** 5n;
// Build the MasterChef contract from kitchen
const masterChefResult = await kitchen.send(
Expand Down Expand Up @@ -927,48 +929,6 @@ describe('Jetton MasterChef Tests', () => {
expect(userUSDTBalanceAfterHarvest).toEqual(userUSDTBalanceBeforeHarvest + benefit);
});

// it('Should deposit later and owner can get the redundent jetton', async () => {
// const userDepositAmount = 1n * TOKEN_DECIMALS;
// const userWithdrawAmount = 5n * 10n ** 5n;
// const periodTime = 10;
// const userJettonWallet = blockchain.openContract(await JettonWalletUSDT.fromInit(user.address, usdt.address));
// const deployerJettonWallet = blockchain.openContract(
// await JettonWalletUSDT.fromInit(deployer.address, usdt.address),
// );
// const deployerBalanceBefore = (await deployerJettonWallet.getGetWalletData()).balance;
// // deposit first
// const r = await deposit(masterChef, user, masterChefJettonWallet, usdt, userDepositAmount);
// printTransactionFees(r.transactions);
// const deployerBalanceAfter = (await deployerJettonWallet.getGetWalletData()).balance;

// // Deployer Should get redundent jetton
// let rewardPerSecond = (await masterChef.getGetJettonMasterChefData()).rewardPerSecond;
// // After 10s, first user deposit, so that the deployer should get the redundent jetton (10s * rewardPerSecond)
// expect(deployerBalanceAfter.toString()).toEqual((deployerBalanceBefore + rewardPerSecond * BigInt(periodTime)).toString());
// // get the balance of usdt before withdraw
// const userUSDTBalanceBeforeWithdraw = (await userJettonWallet.getGetWalletData()).balance;

// // Update time to periodTime, so that we can withdraw
// blockchain.now!! += periodTime;
// // withdraw
// await withdraw(masterChef, user, masterChefJettonWallet, userWithdrawAmount);
// // console.log("-----------------")
// const userUSDTBalanceBeforeHarvest = (await userJettonWallet.getGetWalletData()).balance;

// // Update time to periodTime, so that we can harvest
// blockchain.now!! += periodTime;
// // User send Harvest to MasterChef
// await harvest(masterChef, user, masterChefJettonWallet);
// const userUSDTBalanceAfterHarvest = (await userJettonWallet.getGetWalletData()).balance;

// // check the differnce between userUSDTBalanceBeforeWithdraw and userUSDTBalanceAfterHarvest is equal to userWithdrawAmount
// expect(userUSDTBalanceBeforeHarvest).toEqual(userUSDTBalanceBeforeWithdraw + userWithdrawAmount);
// // check the differnce between userUSDTBalanceBeforeWithdraw and userUSDTBalanceAfterHarvest is equal to userWithdrawAmount
// const remainDeposit = userDepositAmount - userWithdrawAmount;
// const benefit = ((userDepositAmount + remainDeposit) * BigInt(periodTime) * rewardPerSecond) / TOKEN_DECIMALS;
// expect(userUSDTBalanceAfterHarvest).toEqual(userUSDTBalanceBeforeHarvest + benefit);
// });

it('Should withdraw and harvest in one step', async () => {
const userDepositAmount = 1n * TOKEN_DECIMALS;
const userWithdrawAmount = 5n * 10n ** 5n;
Expand Down Expand Up @@ -2106,4 +2066,99 @@ describe('Jetton MasterChef Tests', () => {
const userUSDTBalanceAfterWH = (await userJettonWallet.getGetWalletData()).balance;
expect(userUSDTBalanceAfterWH - userUSDTBalanceAfter).toEqual(userWithdrawAmount);
});

// Owner can redeem the redundant reward token
it('Should owner redeem the redundant reward token when LpSupply is 0 and someoen stake', async () => {
const userDepositAmount = 1n * TOKEN_DECIMALS;
const userWithdrawAmount = userDepositAmount;
const periodTime = 10;
// deposit first
await deposit(masterChef, user, masterChefJettonWallet, usdt, userDepositAmount);
// get the balance of usdt before withdraw

// Update time to periodTime, so that we can withdraw
blockchain.now!! += periodTime;
const poolData = await masterChef.getGetPoolInfo(masterChefJettonWallet.address);
const lpSupplyBefore = poolData.lpSupply;
// withdraw
await withdraw(masterChef, user, masterChefJettonWallet, userWithdrawAmount);
const poolDataAfter = await masterChef.getGetPoolInfo(masterChefJettonWallet.address);
const lpSupplyAfter = poolDataAfter.lpSupply;
expect(lpSupplyAfter).toEqual(lpSupplyBefore - userWithdrawAmount);
expect(lpSupplyAfter).toEqual(0n);

blockchain.now!! += periodTime;
await deposit(masterChef, user, masterChefJettonWallet, usdt, userDepositAmount);
// Owner call redeem to masterChef
const ownerUSDTBalanceBefore = (await deployerJettonWallet.getGetWalletData()).balance;
await masterChef.send(deployer.getSender(), { value: toNano('2') }, 'Redeem');
const ownerUSDTBalanceAfter = (await deployerJettonWallet.getGetWalletData()).balance;
// 10n: the first user deposit time - start time which this period is redundant reward
// The other 10n is time of someone unstake to make LpSupply to 0 and stake again
const redundantReward = 10n * 2n * rewardPerSecond;
expect(ownerUSDTBalanceAfter - ownerUSDTBalanceBefore).toEqual(redundantReward);
});
it('Should owner redeem the redundant reward token when no one stake before deadline', async () => {
const userDepositAmount = 1n * TOKEN_DECIMALS;
const userWithdrawAmount = userDepositAmount;
const periodTime = 10;
// deposit first
await deposit(masterChef, user, masterChefJettonWallet, usdt, userDepositAmount);
// get the balance of usdt before withdraw

// Update time to periodTime, so that we can withdraw
blockchain.now!! += periodTime;
const poolData = await masterChef.getGetPoolInfo(masterChefJettonWallet.address);
const lpSupplyBefore = poolData.lpSupply;
// withdraw
await withdraw(masterChef, user, masterChefJettonWallet, userWithdrawAmount);
let lastWithdrawTime = BigInt(blockchain.now!!);
const poolDataAfter = await masterChef.getGetPoolInfo(masterChefJettonWallet.address);
const lpSupplyAfter = poolDataAfter.lpSupply;
expect(lpSupplyAfter).toEqual(lpSupplyBefore - userWithdrawAmount);
expect(lpSupplyAfter).toEqual(0n);

// Expect lastWithdrawTime == blockchain.now
const redeemData = await masterChef.getGetRedeemData();
expect(redeemData.lastWithdrawTime).toEqual(BigInt(blockchain.now!!));

// Updtate time to deadline
blockchain.now!! += 2500;

// Owner call redeem to masterChef
const ownerUSDTBalanceBefore = (await deployerJettonWallet.getGetWalletData()).balance;
await masterChef.send(deployer.getSender(), { value: toNano('2') }, 'Redeem');
const ownerUSDTBalanceAfter = (await deployerJettonWallet.getGetWalletData()).balance;
// 10n: the first user deposit time - start time which this period is redundant reward
const redundantReward = (deadline - lastWithdrawTime + 10n) * rewardPerSecond;
expect(ownerUSDTBalanceAfter - ownerUSDTBalanceBefore).toEqual(redundantReward);
});
it('Should not withdraw when sending ton is not enough', async () => {
const userDepositAmount = 1n * TOKEN_DECIMALS;
const userWithdrawAmount = 5n * 10n ** 5n;
const periodTime = 100;
// deposit first
await deposit(masterChef, user, masterChefJettonWallet, usdt, userDepositAmount);

// Update time to periodTime, so that we can withdraw
blockchain.now!! += periodTime;
// withdraw
const withdrawResult = await masterChef.send(
user.getSender(),
{ value: toNano('0.06') },
{
$$type: 'Withdraw',
queryId: 0n,
lpTokenAddress: masterChefJettonWallet.address,
amount: userWithdrawAmount,
beneficiary: user.address,
},
);
expect(withdrawResult.transactions).toHaveTransaction({
from: user.address,
to: masterChef.address,
success: false,
exitCode: 28952,
});
});
});
Loading

0 comments on commit 1644f41

Please sign in to comment.