diff --git a/README.md b/README.md
index 218dbd0..35ad055 100644
--- a/README.md
+++ b/README.md
@@ -78,4 +78,16 @@ This is default Tact blueprint project with default commands:
Invalid transfer amount.Occurs when you try to send, burn or mint 0 tokens.
+
+ 6906 |
+
+ Minting already disabled.Occurs when you attempt to enable minting after it has been permanently disabled.
+ |
+
+
+ 6907 |
+
+ Minting is disabled.Occurs when you try to mint tokens while the minting functionality is disabled.
+ |
+
diff --git a/contracts/errors.tact b/contracts/errors.tact
index e30b390..8694d81 100644
--- a/contracts/errors.tact
+++ b/contracts/errors.tact
@@ -9,4 +9,8 @@ const ERROR_JETTON_INITIALIZED: Int = 6903;
// Jetton max supply exceeded
const ERROR_MAX_SUPPLY_EXCEEDED: Int = 6904;
// Invalid transfer amount (e.g., zero tokens)
-const ERROR_CODE_INVALID_AMOUNT: Int = 6905;
\ No newline at end of file
+const ERROR_CODE_INVALID_AMOUNT: Int = 6905;
+// Minting already disabled
+const ERROR_CODE_MINTING_ALREADY_DISABLED: Int = 6906;
+// Minting is disabled
+const ERROR_CODE_MINTING_DISABLED: Int = 6907;
\ No newline at end of file
diff --git a/contracts/jetton/master.tact b/contracts/jetton/master.tact
index 78ada6d..ce5318e 100644
--- a/contracts/jetton/master.tact
+++ b/contracts/jetton/master.tact
@@ -12,6 +12,8 @@ contract JettonMaster with TEP74JettonMaster, TEP89JettonDiscoverable, Deployabl
max_supply: Int = 0;
// Current tokens minted
current_supply: Int = 0;
+ // Is token mintable
+ mintable: Bool = true;
// Administrator of token. Who can mint new tokens
owner: Address;
// Initial code of jetton wallet
@@ -59,12 +61,19 @@ contract JettonMaster with TEP74JettonMaster, TEP89JettonDiscoverable, Deployabl
self.max_supply = new_max_supply;
return;
+ }else if(msg.key == "mintable"){
+ // Once mintable is set to false, it cannot be changed back to true
+ nativeThrowIf(ERROR_CODE_MINTING_ALREADY_DISABLED, !self.mintable);
+
+ self.mintable = msg.value.loadBool();
+ return;
}
self.metadata.set(msg.key, msg.value); // Update metadata for other keys
}
receive(msg: JettonMint){
+ nativeThrowIf(ERROR_CODE_MINTING_DISABLED, !self.mintable); // Reject mint if minting is disabled
nativeThrowUnless(ERROR_CODE_INVALID_AMOUNT, msg.amount > 0); // Reject mint with amount <= 0
self.requireOwner();
diff --git a/tests/Mintable.spec.ts b/tests/Mintable.spec.ts
new file mode 100644
index 0000000..48e0e8b
--- /dev/null
+++ b/tests/Mintable.spec.ts
@@ -0,0 +1,345 @@
+import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox';
+import { beginCell, Builder, Cell, Dictionary, toNano } from '@ton/core';
+import { JettonWallet } from '../build/Jetton/tact_JettonWallet';
+import { JettonMaster } from '../build/Jetton/tact_JettonMaster';
+import { OP_CODES } from './constants/opCodes';
+
+import '@ton/test-utils';
+
+const SYSTEM_CELL = Cell.fromBase64('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');
+
+const JETTON_NAME = "Test jetton";
+const JETTON_DESCRIPTION = "Test jetton description. Test jetton description. Test jetton description";
+const JETTON_SYMBOL = "TSTJTN";
+const JETTON_MAX_SUPPLY = toNano("0");
+
+describe('Mintable', () => {
+ let blockchain: Blockchain;
+ let deployer: SandboxContract;
+ let other: SandboxContract;
+ let jettonMaster: SandboxContract;
+ let jettonWallet: SandboxContract;
+ let otherJettonWallet: SandboxContract;
+
+ beforeEach(async () => {
+ blockchain = await Blockchain.create();
+
+ deployer = await blockchain.treasury('deployer');
+ other = await blockchain.treasury("other");
+
+ jettonMaster = blockchain.openContract(await JettonMaster.fromInit(deployer.address));
+ jettonWallet = blockchain.openContract(await JettonWallet.fromInit(jettonMaster.address, deployer.address));
+ otherJettonWallet = blockchain.openContract(await JettonWallet.fromInit(jettonMaster.address, other.address));
+
+ const deployResult = await jettonMaster.send(
+ deployer.getSender(),
+ {
+ value: toNano("0.05"),
+ },
+ {
+ $$type: 'JettonInit',
+ query_id: 0n,
+ jetton_name: beginCell().storeStringTail(JETTON_NAME).asSlice(),
+ jetton_description: beginCell().storeStringTail(JETTON_DESCRIPTION).asSlice(),
+ jetton_symbol: beginCell().storeStringTail(JETTON_SYMBOL).asSlice(),
+ max_supply: JETTON_MAX_SUPPLY,
+ }
+ );
+ expect(deployResult.transactions).toHaveTransaction({
+ from: deployer.address,
+ to: jettonMaster.address,
+ success: true,
+ deploy: true,
+ op: OP_CODES.JettonInit,
+ });
+ expect(deployResult.transactions).toHaveTransaction({
+ from: jettonMaster.address,
+ to: deployer.address,
+ success: true,
+ deploy: false,
+ op: OP_CODES.JettonInitOk,
+ });
+ });
+
+
+
+ it('should correct build wallet address', async () => {
+ let walletAddressData = await jettonMaster.getGetWalletAddress(deployer.address);
+ expect(walletAddressData.toString()).toEqual(jettonWallet.address.toString());
+
+ let otherWalletAddressData = await jettonMaster.getGetWalletAddress(other.address);
+ expect(otherWalletAddressData.toString()).toEqual(otherJettonWallet.address.toString());
+ });
+
+
+ it('should not double init', async () => {
+ const deployResult = await jettonMaster.send(
+ deployer.getSender(),
+ {
+ value: toNano("0.05"),
+ },
+ {
+ $$type: 'JettonInit',
+ query_id: 0n,
+ jetton_name: beginCell().storeStringTail(JETTON_NAME).asSlice(),
+ jetton_description: beginCell().storeStringTail(JETTON_DESCRIPTION).asSlice(),
+ jetton_symbol: beginCell().storeStringTail(JETTON_SYMBOL).asSlice(),
+ max_supply: JETTON_MAX_SUPPLY,
+ }
+ );
+
+ expect(deployResult.transactions).toHaveTransaction({
+ from: deployer.address,
+ to: jettonMaster.address,
+ success: false,
+ deploy: false,
+ op: OP_CODES.JettonInit,
+ exitCode: 6903,
+ });
+ });
+
+ it('should mint tokens', async () => {
+
+ // mint tokens with unlimited supply
+ const mintResult = await jettonMaster.send(
+ deployer.getSender(),
+ {
+ value: toNano("0.05"),
+ },
+ {
+ $$type: 'JettonMint',
+ query_id: 0n,
+ amount: toNano("1337"),
+ destination: deployer.address,
+ }
+ );
+ expect(mintResult.transactions).toHaveTransaction({
+ from: deployer.address,
+ to: jettonMaster.address,
+ success: true,
+ deploy: false,
+ op: OP_CODES.JettonMint,
+ });
+ expect(mintResult.transactions).toHaveTransaction({
+ from: jettonMaster.address,
+ to: jettonWallet.address,
+ success: true,
+ deploy: true,
+ op: 0x178d4519,
+ });
+
+ let jettonMasterMetadata = await jettonMaster.getGetJettonData();
+ expect(jettonMasterMetadata.total_supply).toEqual(toNano("1337"));
+
+ let jettonWalletData = await jettonWallet.getGetWalletData();
+ expect(jettonWalletData.balance).toEqual(toNano("1337"));
+ });
+
+ it('should not mint tokens not owner', async () => {
+ const mintResult = await jettonMaster.send(
+ other.getSender(),
+ {
+ value: toNano("0.05"),
+ },
+ {
+ $$type: 'JettonMint',
+ query_id: 0n,
+ amount: toNano("1337"),
+ destination: other.address,
+ }
+ );
+ expect(mintResult.transactions).toHaveTransaction({
+ from: other.address,
+ to: jettonMaster.address,
+ success: false,
+ deploy: false,
+ op: 0x133704,
+ exitCode: 132,
+ });
+ });
+
+
+ it('should try to set mintable true when already true', async () => {
+ //enable minting - when in an already enabled state
+ const mintableUpdateResult = await jettonMaster.send(
+ deployer.getSender(),
+ {
+ value: toNano("0.05"),
+ },
+ {
+ $$type: 'JettonSetParameter',
+ key: "mintable",
+ value: beginCell().storeBit(true).asSlice()
+ }
+ );
+
+ expect(mintableUpdateResult.transactions).toHaveTransaction({
+ from: deployer.address,
+ to: jettonMaster.address,
+ success: true,
+ deploy: false,
+ op: OP_CODES.JettonSetParameter,
+ });
+
+ //try to mint 100 tokens
+ const mintResult = await jettonMaster.send(
+ deployer.getSender(),
+ {
+ value: toNano("0.05"),
+ },
+ {
+ $$type: 'JettonMint',
+ query_id: 0n,
+ amount: toNano("100"),
+ destination: deployer.address,
+ }
+ );
+ expect(mintResult.transactions).toHaveTransaction({
+ from: deployer.address,
+ to: jettonMaster.address,
+ success: true,
+ deploy: false,
+ op: OP_CODES.JettonMint
+ });
+ });
+
+ it('should disable minting', async () => {
+ //disable minting
+ const mintableUpdateResult = await jettonMaster.send(
+ deployer.getSender(),
+ {
+ value: toNano("0.05"),
+ },
+ {
+ $$type: 'JettonSetParameter',
+ key: "mintable",
+ value: beginCell().storeBit(false).asSlice()
+ }
+ );
+
+ expect(mintableUpdateResult.transactions).toHaveTransaction({
+ from: deployer.address,
+ to: jettonMaster.address,
+ success: true,
+ deploy: false,
+ op: OP_CODES.JettonSetParameter,
+ });
+
+ //try to mint 100 tokens
+ const mintResult = await jettonMaster.send(
+ deployer.getSender(),
+ {
+ value: toNano("0.05"),
+ },
+ {
+ $$type: 'JettonMint',
+ query_id: 0n,
+ amount: toNano("100"),
+ destination: deployer.address,
+ }
+ );
+ expect(mintResult.transactions).toHaveTransaction({
+ from: deployer.address,
+ to: jettonMaster.address,
+ success: false,
+ deploy: false,
+ op: OP_CODES.JettonMint,
+ exitCode: 6907,
+ });
+ });
+
+ it('should fail to enable minting back', async () => {
+ //disable minting
+ const mintableUpdateResult = await jettonMaster.send(
+ deployer.getSender(),
+ {
+ value: toNano("0.05"),
+ },
+ {
+ $$type: 'JettonSetParameter',
+ key: "mintable",
+ value: beginCell().storeBit(false).asSlice()
+ }
+ );
+
+ expect(mintableUpdateResult.transactions).toHaveTransaction({
+ from: deployer.address,
+ to: jettonMaster.address,
+ success: true,
+ deploy: false,
+ op: OP_CODES.JettonSetParameter,
+ });
+
+ //try to mint 100 tokens
+ const mintResult = await jettonMaster.send(
+ deployer.getSender(),
+ {
+ value: toNano("0.05"),
+ },
+ {
+ $$type: 'JettonMint',
+ query_id: 0n,
+ amount: toNano("100"),
+ destination: deployer.address,
+ }
+ );
+ expect(mintResult.transactions).toHaveTransaction({
+ from: deployer.address,
+ to: jettonMaster.address,
+ success: false,
+ deploy: false,
+ op: OP_CODES.JettonMint,
+ exitCode: 6907,
+ });
+
+ //try to enable minting back
+ const mintableUpdateResult2 = await jettonMaster.send(
+ deployer.getSender(),
+ {
+ value: toNano("0.05"),
+ },
+ {
+ $$type: 'JettonSetParameter',
+ key: "mintable",
+ value: beginCell().storeBit(true).asSlice()
+ }
+ );
+
+ expect(mintableUpdateResult2.transactions).toHaveTransaction({
+ from: deployer.address,
+ to: jettonMaster.address,
+ success: false,
+ deploy: false,
+ op: OP_CODES.JettonSetParameter,
+ exitCode: 6906,
+ });
+
+ //try to mint 100 tokens
+ const mintResult2 = await jettonMaster.send(
+ deployer.getSender(),
+ {
+ value: toNano("0.05"),
+ },
+ {
+ $$type: 'JettonMint',
+ query_id: 0n,
+ amount: toNano("100"),
+ destination: deployer.address,
+ }
+ );
+ expect(mintResult2.transactions).toHaveTransaction({
+ from: deployer.address,
+ to: jettonMaster.address,
+ success: false,
+ deploy: false,
+ op: OP_CODES.JettonMint,
+ exitCode: 6907,
+ });
+ });
+
+ it('should return system cell', async () => {
+ let systemCell = await jettonMaster.getTactSystemCell();
+ expect(systemCell).toEqualCell(SYSTEM_CELL);
+ });
+
+});