Skip to content

Commit

Permalink
fix: balance/totalSupply handling for bounced messages (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shvandre committed Jan 12, 2025
1 parent bfff733 commit 4f80df5
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 20 deletions.
161 changes: 142 additions & 19 deletions sources/contract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,20 @@ import {
SandboxContract,
TreasuryContract,
internal,
printTransactionFees, BlockchainTransaction
} from "@ton/sandbox";
} from '@ton/sandbox';


import {
ChangeOwner,
JettonMinter,
Mint,
TokenUpdateContent,
Deploy,
TokenBurn, ProvideWalletAddress
} from './output/Jetton_JettonMinter';
TokenBurn, ProvideWalletAddress, storeTokenTransfer, storeTokenBurn, storeMint
} from "./output/Jetton_JettonMinter";
import { JettonWallet, TokenTransfer } from "./output/Jetton_JettonWallet";

import "@ton/test-utils";
import { randomAddress } from './utils/utils';
import { fromNano } from "@ton/ton";
import { getRandomInt, randomAddress } from "./utils/utils";

function jettonContentToCell(content: {type: 0|1, uri:string}) {
return beginCell()
Expand Down Expand Up @@ -260,11 +257,13 @@ describe("JettonMinter", () => {
to: deployerJettonWallet.address,
deploy: true,
});
//TODO Why to send excesses to jetton Minter? does this make sense?
// expect(mintResult.transactions).toHaveTransaction({ // excesses
// from: deployerJettonWallet.address,
// to: jettonMinter.address
// });
//Here was the check, that excesses are send to JettonMinter.
//This is an implementation-defined behavior
//In my implementation, excesses are sent to the deployer
expect(mintResult.transactions).toHaveTransaction({ // excesses
from: deployerJettonWallet.address,
to: deployer.address
});


expect(await deployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance);
Expand Down Expand Up @@ -414,7 +413,7 @@ describe("JettonMinter", () => {
let sentAmount = toNano('0.5');
let forwardAmount = toNano('0.05');
let forwardPayload = beginCell().storeUint(0x1234567890abcdefn, 128).endCell();
//TODO This block checks forward_payload in place (Either bit equals 0)
//This block checks forward_payload in place (Either bit equals 0)
const sendResult = await deployerJettonWallet.sendTransfer(deployer.getSender(), toNano('0.1'), //tons
sentAmount, notDeployer.address,
deployer.address, null, forwardAmount, forwardPayload);
Expand Down Expand Up @@ -444,15 +443,15 @@ describe("JettonMinter", () => {
});


//TODO There was no such test in official implementation
//There was no such test in official implementation
it('correctly sends forward_payload in ref', async () => {
const deployerJettonWallet = await userWallet(deployer.address);
let initialJettonBalance = await deployerJettonWallet.getJettonBalance();
const notDeployerJettonWallet = await userWallet(notDeployer.address);
let initialJettonBalance2 = await notDeployerJettonWallet.getJettonBalance();
let sentAmount = toNano('0.5');
let forwardAmount = toNano('0.05');
//TODO This block checks forward_payload in separate ref (Either bit equals 1)
//This block checks forward_payload in separate ref (Either bit equals 1)
let forwardPayload = beginCell().storeUint(1, 1).storeRef(beginCell().storeUint(0x1234567890abcdefn, 128).endCell()).endCell();

const sendResult = await deployerJettonWallet.sendTransfer(deployer.getSender(), toNano('0.1'), //tons
Expand Down Expand Up @@ -533,6 +532,129 @@ describe("JettonMinter", () => {

expect(await deployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance);
});
describe('Bounces', () => {
// This code is borrowed from the stablecoin implementation.
// The behavior is implementation-defined.
// I'm still not sure if the code handling these bounces is really necessary,
// but I could be wrong. Refer to this issue for details: https://github.com/tact-lang/jetton/issues/10
it('minter should restore supply on internal_transfer bounce', async () => {
const deployerJettonWallet = await userWallet(deployer.address);
const mintAmount = BigInt(getRandomInt(1000, 2000));
const mintMsg = beginCell().store(storeMint({$$type: "Mint", amount: mintAmount, receiver: deployer.address})).endCell();

const supplyBefore = await jettonMinter.getTotalSupply();
const minterSmc = await blockchain.getContract(jettonMinter.address);

// Sending message but only processing first step of tx chain
let res = await minterSmc.receiveMessage(internal({
from: deployer.address,
to: jettonMinter.address,
body: mintMsg,
value: toNano('1')
}));

expect(res.outMessagesCount).toEqual(1);
const outMsgSc = res.outMessages.get(0)!.body.beginParse();
expect(outMsgSc.preloadUint(32)).toEqual(Op.internal_transfer);

expect(await jettonMinter.getTotalSupply()).toEqual(supplyBefore + mintAmount);

await minterSmc.receiveMessage(internal({
from: deployerJettonWallet.address,
to: jettonMinter.address,
bounced: true,
body: beginCell().storeUint(0xFFFFFFFF, 32).storeSlice(outMsgSc).endCell(),
value: toNano('0.95')
}));

// Supply should change back
expect(await jettonMinter.getTotalSupply()).toEqual(supplyBefore);
});
it('wallet should restore balance on internal_transfer bounce', async () => {
const initRes = await jettonMinter.sendMint(deployer.getSender(), deployer.address, 201n, 0n, toNano(1));
const deployerJettonWallet = await userWallet(deployer.address);
expect(initRes.transactions).toHaveTransaction({
from: jettonMinter.address,
to: deployerJettonWallet.address,
success: true
})

const notDeployerJettonWallet = await userWallet(notDeployer.address);
const balanceBefore = await deployerJettonWallet.getJettonBalance();
const txAmount = BigInt(getRandomInt(100, 200));
const transferMsg = beginCell().store(storeTokenTransfer({$$type: "TokenTransfer",
query_id: 0n,
amount: txAmount,
response_destination: deployer.address,
destination: notDeployer.address,
custom_payload: null,
forward_ton_amount: 0n,
forward_payload: beginCell().endCell().beginParse()
})).endCell()

const walletSmc = await blockchain.getContract(deployerJettonWallet.address);

const res = await walletSmc.receiveMessage(internal({
from: deployer.address,
to: deployerJettonWallet.address,
body: transferMsg,
value: toNano('1')
}));
expect(res.outMessagesCount).toEqual(1);

const outMsgSc = res.outMessages.get(0)!.body.beginParse();
expect(outMsgSc.preloadUint(32)).toEqual(Op.internal_transfer);

expect(await deployerJettonWallet.getJettonBalance()).toEqual(balanceBefore - txAmount);

await walletSmc.receiveMessage(internal({
from: notDeployerJettonWallet.address,
to: walletSmc.address,
bounced: true,
body: beginCell().storeUint(0xFFFFFFFF, 32).storeSlice(outMsgSc).endCell(),
value: toNano('0.95')
}));

// Balance should roll back
expect(await deployerJettonWallet.getJettonBalance()).toEqual(balanceBefore);
});
it('wallet should restore balance on burn_notification bounce', async () => {
// Mint some jettons
await jettonMinter.sendMint(deployer.getSender(), deployer.address, 201n, 0n, toNano(1));
const deployerJettonWallet = await userWallet(deployer.address);
const balanceBefore = await deployerJettonWallet.getJettonBalance();
const burnAmount = BigInt(getRandomInt(100, 200));

const burnMsg = beginCell().store(storeTokenBurn({amount: burnAmount, $$type: "TokenBurn", query_id: 0n, response_destination: deployer.address, custom_payload: null})).endCell()

const walletSmc = await blockchain.getContract(deployerJettonWallet.address);

const res = await walletSmc.receiveMessage(internal({
from: deployer.address,
to: deployerJettonWallet.address,
body: burnMsg,
value: toNano('1')
}));

expect(res.outMessagesCount).toEqual(1);

const outMsgSc = res.outMessages.get(0)!.body.beginParse();
expect(outMsgSc.preloadUint(32)).toEqual(Op.burn_notification);

expect(await deployerJettonWallet.getJettonBalance()).toEqual(balanceBefore - burnAmount);

await walletSmc.receiveMessage(internal({
from: jettonMinter.address,
to: walletSmc.address,
bounced: true,
body: beginCell().storeUint(0xFFFFFFFF, 32).storeSlice(outMsgSc).endCell(),
value: toNano('0.95')
}));

// Balance should roll back
expect(await deployerJettonWallet.getJettonBalance()).toEqual(balanceBefore);
});
});
// implementation detail
it('works with minimal ton amount', async () => {
const deployerJettonWallet = await userWallet(deployer.address);
Expand Down Expand Up @@ -689,7 +811,8 @@ describe("JettonMinter", () => {
//let minimalFee = toNano("0.006");
let L = toNano(0.00000001);
let R = toNano(0.1);
//TODO change false to true if you want to find minimal fee
//change false to true if you want to find minimal fee
//However, before doing it, remove gas-checks from the smart-contract code
//implementing binary search
while(R - L > 1 && false) {
let minimalFee = (L + R) / 2n;
Expand All @@ -714,8 +837,8 @@ describe("JettonMinter", () => {

const sendLow = await deployerJettonWallet.sendBurn(deployer.getSender(), minimalFee, // ton amount
burnAmount, deployer.address, null); // amount, response address, custom payload
//TODO Here was tests, that checks that there is enough ton to jetton wallet to send a message.
//However I check that it is enough ton to process a message from jetton wallet to jetton minter
//Here was tests, that checks that there is enough ton to jetton wallet to send a message.
//However, I check that it is enough ton to process a message from jetton wallet to jetton minter
expect(sendLow.transactions).not.toHaveTransaction({
from: deployerJettonWallet.address,
to: jettonMinter.address,
Expand Down Expand Up @@ -823,7 +946,7 @@ describe("JettonMinter", () => {
//const fwdFee = 1464012n;
//const minimalFee = fwdFee + 10000000n; // toNano('0.0061');

//TODO Added binary search to find minimal fee
//Added binary search to find minimal fee
let L = toNano(0.00000001);
let R = toNano(0.1);
//Binary search here does not affect on anything except time of test
Expand Down
3 changes: 3 additions & 0 deletions sources/jetton_minter.tact
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ contract JettonMinter with OwnableTransferable {
});
}

bounced(msg: bounced<TokenTransferInternal>){
self.totalSupply -= msg.amount;
}
inline fun takeWalletBody(targetJettonWallet: Address, includedAddress: Address?, query_id: Int): Cell {
let body: Builder = beginCell()
.storeUint(0xd1735400, 32)
Expand Down
2 changes: 1 addition & 1 deletion sources/jetton_wallet.tact
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ contract JettonWallet with Ownable {
}

bounced(msg: bounced<TokenTransferInternal>){
self.balance += self.balance + msg.amount;
self.balance += msg.amount;
}

bounced(msg: bounced<TokenBurnNotification>){
Expand Down

0 comments on commit 4f80df5

Please sign in to comment.