Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored ExitCodes to match reference implementation #25

Merged
merged 7 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2

- name: Install Soufflé on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: |
sudo wget https://souffle-lang.github.io/ppa/souffle-key.public -O /usr/share/keyrings/souffle-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/souffle-archive-keyring.gpg] https://souffle-lang.github.io/ppa/ubuntu/ stable main" | sudo tee /etc/apt/sources.list.d/souffle.list
sudo apt update
sudo apt install souffle

- name: Setup Node.js
uses: actions/setup-node@v3
with:
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"dependencies": {
"@aws-crypto/sha256-js": "^5.2.0",
"@nowarp/misti": "~0.5.0",
"@nowarp/misti": "^0.6.2",
"@tact-lang/compiler": "^1.5.0",
"@tact-lang/deployer": "^0.2.0",
"@tact-lang/ton-abi": "^0.0.3",
Expand All @@ -26,7 +26,6 @@
"dotenv": "^16.4.5",
"enquirer": "^2.3.6",
"jest": "^29.3.1",
"misti": "^0.0.21",
"open": "^8.4.0",
"prando": "^6.0.1",
"prettier": "^2.5.1",
Expand Down
63 changes: 45 additions & 18 deletions sources/contract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
JettonMinter,
Mint,
TokenUpdateContent,
TokenBurn, ProvideWalletAddress, storeTokenTransfer, storeTokenBurn, storeMint
TokenBurn, ProvideWalletAddress, storeTokenTransfer, storeTokenBurn, storeMint, CustomChangeOwner
} from "./output/Jetton_JettonMinter";
import { JettonWallet, TokenTransfer } from "./output/Jetton_JettonWallet";

Expand Down Expand Up @@ -57,6 +57,7 @@ JettonMinter.prototype.sendMint = async function (
}
const msg: Mint = {
$$type: "Mint",
query_id: 0n,
amount: jetton_amount,
receiver: to,
};
Expand All @@ -69,8 +70,8 @@ JettonMinter.prototype.sendChangeAdmin = async function (
via: Sender,
newOwner: Address
) {
const msg: ChangeOwner = {
$$type: "ChangeOwner",
const msg: CustomChangeOwner = {
$$type: "CustomChangeOwner",
queryId: 0n,
newOwner: newOwner,
};
Expand Down Expand Up @@ -123,6 +124,21 @@ const Op = {
mint: 0xfc708bd2,
}

const Errors = {
invalid_op: 709,
not_admin: 73,
unouthorized_burn: 74,
discovery_fee_not_matched: 75,
wrong_op: 0xffff,
not_owner: 705,
not_enough_ton: 709,
not_enough_gas: 707,
not_valid_wallet: 707,
wrong_workchain: 333,
balance_error: 706,
}



describe("JettonMinter", () => {
let blockchain: Blockchain;
Expand Down Expand Up @@ -294,7 +310,7 @@ describe("JettonMinter", () => {
from: notDeployer.address,
to: jettonMinter.address,
aborted: true,
success: false,
exitCode: Errors.not_admin,
});
expect(await deployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance);
expect(await jettonMinter.getTotalSupply()).toEqual(initialTotalSupply);
Expand Down Expand Up @@ -325,7 +341,7 @@ describe("JettonMinter", () => {
from: notDeployer.address,
on: jettonMinter.address,
aborted: true,
success: false,
exitCode: Errors.not_admin,
});
});

Expand All @@ -337,6 +353,17 @@ describe("JettonMinter", () => {
await jettonMinter.sendChangeContent(deployer.getSender(), defaultContent);
expect((await jettonMinter.getContent()).equals(defaultContent)).toBe(true);
});
it('not a minter admin can not change content', async () => {
let newContent = beginCell().storeUint(1,1).endCell();
let changeContent = await jettonMinter.sendChangeContent(notDeployer.getSender(), newContent);
expect((await jettonMinter.getContent()).equals(defaultContent)).toBe(true);
expect(changeContent.transactions).toHaveTransaction({
from: notDeployer.address,
to: jettonMinter.address,
aborted: true,
exitCode: Errors.not_admin, // error::unauthorized_change_content_request
});
});
it('wallet owner should be able to send jettons', async () => {
const deployerJettonWallet = await userWallet(deployer.address);
let initialJettonBalance = await deployerJettonWallet.getJettonBalance();
Expand Down Expand Up @@ -378,7 +405,7 @@ describe("JettonMinter", () => {
from: notDeployer.address,
to: deployerJettonWallet.address,
aborted: true,
success: false,
exitCode: Errors.not_owner,
});
expect(await deployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance);
expect(await notDeployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance2);
Expand All @@ -399,7 +426,7 @@ describe("JettonMinter", () => {
from: deployer.address,
to: deployerJettonWallet.address,
aborted: true,
success: false,
exitCode: Errors.balance_error,
});
expect(await deployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance);
expect(await notDeployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance2);
Expand Down Expand Up @@ -520,7 +547,7 @@ describe("JettonMinter", () => {
from: deployer.address,
on: deployerJettonWallet.address,
aborted: true,
success: false,
exitCode: Errors.not_enough_ton,
});
// Make sure value bounced
expect(sendResult.transactions).toHaveTransaction({
Expand All @@ -540,7 +567,7 @@ describe("JettonMinter", () => {
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 mintMsg = beginCell().store(storeMint({$$type: "Mint", query_id: 0n, amount: mintAmount, receiver: deployer.address})).endCell();

const supplyBefore = await jettonMinter.getTotalSupply();
const minterSmc = await blockchain.getContract(jettonMinter.address);
Expand Down Expand Up @@ -683,7 +710,7 @@ describe("JettonMinter", () => {
from: deployer.address,
to: deployerJettonWallet.address,
aborted: true,
success: false,
exitCode: Errors.not_enough_ton,
});
sentAmount += 1n; // now enough
sendResult = await deployerJettonWallet.sendTransfer(deployer.getSender(), sentAmount,
Expand Down Expand Up @@ -743,7 +770,7 @@ describe("JettonMinter", () => {
from: notDeployer.address,
to: deployerJettonWallet.address,
aborted: true,
success: false,
exitCode: Errors.not_valid_wallet,
});
expect(await deployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance);
});
Expand Down Expand Up @@ -779,7 +806,7 @@ describe("JettonMinter", () => {
from: notDeployer.address,
to: deployerJettonWallet.address,
aborted: true,
success: false,
exitCode: Errors.not_owner,
});
expect(await deployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance);
expect(await jettonMinter.getTotalSupply()).toEqual(initialTotalSupply);
Expand All @@ -796,7 +823,7 @@ describe("JettonMinter", () => {
from: deployer.address,
to: deployerJettonWallet.address,
aborted: true,
success: false,
exitCode: Errors.balance_error,
});
expect(await deployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance);
expect(await jettonMinter.getTotalSupply()).toEqual(initialTotalSupply);
Expand Down Expand Up @@ -832,7 +859,7 @@ describe("JettonMinter", () => {
}
}
console.log(L);
let minimalFee = 11217199n;
let minimalFee = 11408799n;
//It is the number you can get in console.log(L) if setting "false" to "true" in while loop above

const sendLow = await deployerJettonWallet.sendBurn(deployer.getSender(), minimalFee, // ton amount
Expand Down Expand Up @@ -881,7 +908,7 @@ describe("JettonMinter", () => {
from: deployerJettonWallet.address,
to: jettonMinter.address,
aborted: true,
success: false,
exitCode: Errors.unouthorized_burn,
});

res = await blockchain.sendMessage(internal({
Expand Down Expand Up @@ -976,7 +1003,7 @@ describe("JettonMinter", () => {
from: deployer.address,
to: jettonMinter.address,
aborted: true,
success: false,
exitCode: Errors.discovery_fee_not_matched,
});
/*
* Might be helpfull to have logical OR in expect lookup
Expand Down Expand Up @@ -1094,14 +1121,14 @@ describe("JettonMinter", () => {
const deployerJettonWallet = await userWallet(deployer.address);
let sentAmount = toNano('0.5');
let forwardAmount = toNano('0.05');
const sendResult = await deployerJettonWallet.sendTransfer(deployer.getSender(), toNano('0.1'), //tons
const sendResult = await deployerJettonWallet.sendTransfer(deployer.getSender(), toNano('0.2'), //tons
sentAmount, Address.parse("Ef8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAU"),
deployer.address, null, forwardAmount, null);
expect(sendResult.transactions).toHaveTransaction({ //excesses
from: deployer.address,
to: deployerJettonWallet.address,
aborted: true,
success: false,
exitCode: Errors.wrong_workchain,
});
});

Expand Down
14 changes: 14 additions & 0 deletions sources/exit_codes.tact
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
trait MinterExitcodes {
const IncorrectSender: Int = 73;
const UnauthorizedBurn: Int = 74;
const InsufficientGasForDiscovery: Int = 75;
}

trait WalletExitcodes {
const IncorrectSender: Int = 705;
const IncorrectBalanceAfrerSend: Int = 706;
const IncorrectSenderInternal: Int = 707;
const UnsufficientAmountOfTonForBurn: Int = 707;
Shvandre marked this conversation as resolved.
Show resolved Hide resolved
const UnsufficientAmountOfTonAttached: Int = 709;
const InvalidDestinationWorkchain: Int = 333;
}
36 changes: 27 additions & 9 deletions sources/jetton_minter.tact
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "@stdlib/ownable";
import "@stdlib/deploy";
import "./jetton_wallet";
import "./exit_codes";
import "./messages";

asm fun emptyAddress(): Address { b{00} PUSHSLICE }
Expand All @@ -13,7 +14,10 @@ struct JettonMasterState {
jettonWalletCode: Cell;
}

contract JettonMinter with OwnableTransferable {
//Actually this contract has OwnableTransferable functionality
//but this logic is implemented without OwnableTransferable trait
//to match refference implementation in terms of exit codes.
contract JettonMinter with MinterExitcodes {
totalSupply: Int as coins;
mintable: Bool;
owner: Address;
Expand All @@ -30,7 +34,7 @@ contract JettonMinter with OwnableTransferable {

receive(msg: TokenBurnNotification) {
//Check that the message is from msg.sender's jetton_wallet
require(sender() == self.getJettonWalletByOwner(msg.sender), "Not wallet owner");
nativeThrowUnless(self.UnauthorizedBurn, sender() == self.getJettonWalletByOwner(msg.sender));

self.totalSupply -= msg.amount;
send(SendParameters{
Expand All @@ -43,15 +47,23 @@ contract JettonMinter with OwnableTransferable {
}.toCell()
});
}
receive(msg: CustomChangeOwner) {
// Check if the sender is the owner
nativeThrowUnless(self.IncorrectSender, sender() == self.owner);

receive(msg: TokenUpdateContent){
self.requireOwner(); // Allow changing content only by owner
// Update owner
self.owner = msg.newOwner;
}

receive(msg: TokenUpdateContent) {
//Only owner may update content.
nativeThrowUnless(self.IncorrectSender, sender() == self.owner);
self.jettonContent = msg.content; // Update content
}

// https://github.com/ton-blockchain/TEPs/blob/master/text/0089-jetton-wallet-discovery.md
receive(msg: ProvideWalletAddress) {
require(context().value >= ton("0.006625199"), "Insufficient gas");
nativeThrowUnless(self.InsufficientGasForDiscovery, context().value >= ton("0.0067056"));
let includedAddress: Address? = null;
let workchain: Int = parseStdAddress(msg.owner_address.asSlice()).workchain;
//Note, that emptyAddress != null, it is different values.
Expand All @@ -74,11 +86,14 @@ contract JettonMinter with OwnableTransferable {
});
}
receive(msg: Mint) {
self.requireOwner(); // Allow minting only by owner
nativeThrowUnless(self.IncorrectSender, sender() == self.owner); // Allow minting only by owner

//We don't use nativeThrowUnless here as 'mintable' flag is implementation-defined
//And not present in token-contract https://github.com/ton-blockchain/token-contract/tree/main/ft
require(self.mintable, "Not mintable");

//Maybe we should check that msg.value is enough to cover the gas fees
//Or, maybe we should do self.totalSupply -= msg.amount if bounced.
//But there is no any check in Howard's code and in official funC code,
//But there is no such check in token-contract,
self.totalSupply += msg.amount; // Update total supply

let winit: StateInit = self.getJettonWalletInit(msg.receiver);
Expand All @@ -89,7 +104,7 @@ contract JettonMinter with OwnableTransferable {
bounce: true,
mode: SendRemainingValue,
body: TokenTransferInternal{
query_id: 0,
query_id: msg.query_id,
amount: msg.amount,
from: myAddress(),
response_destination: self.owner, // Owner is minting, so send excess to owner
Expand Down Expand Up @@ -140,4 +155,7 @@ contract JettonMinter with OwnableTransferable {
get fun get_wallet_address(ownerAddress: Address): Address {
return self.getJettonWalletByOwner(ownerAddress);
}
get fun owner(): Address {
return self.owner;
}
}
Loading
Loading