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); + }); + +});