Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
18 changes: 18 additions & 0 deletions contracts/Acton.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,24 @@ src = "contracts/ccip/merkle_root/contract.tolk"
domain = "ccip"
depends = []

[contracts.TokenPool]
display-name = "link.chain.ton.ccip.TokenPool"
src = "contracts/ccip/pools/token_pool_contract.tolk"
domain = "ccip/pools"
depends = []

[contracts.LockReleaseTokenPool]
display-name = "link.chain.ton.ccip.LockReleaseTokenPool"
src = "contracts/ccip/pools/lock_release_token_pool/contract.tolk"
domain = "ccip/pools"
depends = []

[contracts.BurnMintTokenPool]
display-name = "link.chain.ton.ccip.BurnMintTokenPool"
src = "contracts/ccip/pools/burn_mint_token_pool/contract.tolk"
domain = "ccip/pools"
depends = []

[contracts.Timelock]
display-name = "link.chain.ton.mcms.Timelock"
src = "contracts/mcms/rbac_timelock.tolk"
Expand Down
208 changes: 208 additions & 0 deletions contracts/contracts/ccip/cct/JettonMinter.tolk
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import "@stdlib/strings"
import "../../lib/jetton/errors"
import "../../lib/jetton/jetton-utils"
import "../../lib/jetton/storage"
import "../../lib/jetton/messages"
import "fees-management"

contract JettonMinter {
author: "The Tolk Team"
incomingMessages: AllowedMessageToMinter
storage: MinterStorage
}

type AllowedMessageToMinter =
| MintNewJettons
| BurnNotificationForMinter
| RequestWalletAddress
| ChangeMinterAdmin
| ClaimMinterAdmin
| DropMinterAdmin
| ChangeMinterMetadataUri
| UpgradeMinterCode
| TopUpTons

fun onBouncedMessage(in: InMessageBounced) {
in.bouncedBody.skipBouncedPrefix();
// process only mint bounces; on other messages, an exception will be thrown, it's okay
val msg = lazy InternalTransferStep.fromSlice(in.bouncedBody);

var storage = lazy MinterStorage.load();
storage.totalSupply -= msg.jettonAmount;
storage.save();
}

fun assertSenderIsAdmin(senderAddress: address, adminAddress: address?) {
// theoretically, minter's admin can be dropped and be `null`, so being precise, we should check:
// ```
// assert (adminAddress != null) throw ERROR_NOT_OWNER;
// ```
// but in practice, the above assertion is reduntant, we just bypass the nullability check:
assert (senderAddress == adminAddress!) throw ERROR_NOT_OWNER;
// then, if admin is `null`, error code 7 will be thrown (while executing operator `==`),
// it's suitable for current implementation
}

fun onInternalMessage(in: InMessage) {
val msg = lazy AllowedMessageToMinter.fromSlice(in.body);

match (msg) {
BurnNotificationForMinter => {
var storage = lazy MinterStorage.load();
assert (in.senderAddress == calcAddressOfJettonWallet(msg.burnInitiator, contract.getAddress(), storage.jettonWalletCode)) throw ERROR_NOT_VALID_WALLET;
storage.totalSupply -= msg.jettonAmount;
storage.save();

if (msg.sendExcessesTo == null) {
return;
}

val excessesMsg = createMessage({
bounce: BounceMode.NoBounce,
dest: msg.sendExcessesTo,
value: 0,
body: ReturnExcessesBack {
queryId: msg.queryId
}
});
excessesMsg.send(SEND_MODE_IGNORE_ERRORS | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
}

RequestWalletAddress => {
var ownerAddress: Cell<address>? = msg.includeOwnerAddress
? msg.ownerAddress.toCell()
: null;

var walletAddress: address? = null;
if (msg.ownerAddress.getWorkchain() == MY_WORKCHAIN) {
val storage = lazy MinterStorage.load();
walletAddress = calcAddressOfJettonWallet(msg.ownerAddress, contract.getAddress(), storage.jettonWalletCode);
}

val respondMsg = createMessage({
bounce: BounceMode.NoBounce,
dest: in.senderAddress,
value: 0,
body: ResponseWalletAddress {
queryId: msg.queryId,
jettonWalletAddress: walletAddress,
ownerAddress: ownerAddress,
}
});
respondMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL);
}

MintNewJettons => {
var storage = lazy MinterStorage.load();
assertSenderIsAdmin(in.senderAddress, storage.adminAddress);
assert (msg.mintRecipient.getWorkchain() == MY_WORKCHAIN) throw ERROR_WRONG_WORKCHAIN;

val internalTransferMsg = lazy msg.internalTransferMsg.load({
throwIfOpcodeDoesNotMatch: ERROR_INVALID_OP
});
var forwardTonAmount = internalTransferMsg.forwardTonAmount;
internalTransferMsg.forwardPayload.checkIsCorrectTLBEither();

// a little more than needed, it’s ok since it’s sent by the admin and excesses will return back
checkAmountIsEnoughToTransfer(msg.tonAmount, forwardTonAmount, in.originalForwardFee);

storage.totalSupply += internalTransferMsg.jettonAmount;
storage.save();

reserveToncoinsOnBalance(ton("0.01"), RESERVE_MODE_EXACT_AMOUNT); // reserve for storage fees

val deployMsg = createMessage({
bounce: BounceMode.Only256BitsOfBody,
dest: calcDeployedJettonWallet(msg.mintRecipient, contract.getAddress(), storage.jettonWalletCode),
value: msg.tonAmount,
body: msg.internalTransferMsg,
});
deployMsg.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_BOUNCE_ON_ACTION_FAIL);
}

ChangeMinterAdmin => {
var storage = lazy MinterStorage.load();
assertSenderIsAdmin(in.senderAddress, storage.adminAddress);
storage.nextAdminAddress = msg.newAdminAddress;
storage.save();
}

ClaimMinterAdmin => {
var storage = lazy MinterStorage.load();
assertSenderIsAdmin(in.senderAddress, storage.nextAdminAddress);
storage.adminAddress = storage.nextAdminAddress;
storage.nextAdminAddress = null;
storage.save();
}

DropMinterAdmin => {
var storage = lazy MinterStorage.load();
assertSenderIsAdmin(in.senderAddress, storage.adminAddress);
storage.adminAddress = null;
storage.nextAdminAddress = null;
storage.save();
}

ChangeMinterMetadataUri => {
var storage = lazy MinterStorage.load();
assertSenderIsAdmin(in.senderAddress, storage.adminAddress);
// convert "inlined snake string" to a normal (ref) string, bypassing the type system
storage.metadataUri = msg.newMetadataUri.toCell() as unknown as string;
storage.save();
}

UpgradeMinterCode => {
var storage = lazy MinterStorage.load();
assertSenderIsAdmin(in.senderAddress, storage.adminAddress);
contract.setData(msg.newData);
contract.setCodePostponed(msg.newCode);
}

TopUpTons => {
// just accept tons
}

else => throw 0xFFFF
}
}



struct JettonDataReply {
totalSupply: int
mintable: bool
adminAddress: address?
jettonContent: Cell<OnchainMetadataReply>
jettonWalletCode: cell
}

struct (0x00) OnchainMetadataReply {
contentDict: map<uint256, string_prefixed0x>
}

get fun get_jetton_data(): JettonDataReply {
val storage = lazy MinterStorage.load();
var metadata: OnchainMetadataReply = {
contentDict: []
};
metadata.contentDict.set("uri".sha256(), storage.metadataUri.prefixWith00());
metadata.contentDict.set("decimals".sha256(), "9".prefixWith00());

return {
totalSupply: storage.totalSupply,
mintable: true,
adminAddress: storage.adminAddress,
jettonContent: metadata.toCell(),
jettonWalletCode: storage.jettonWalletCode,
}
}

get fun get_wallet_address(ownerAddress: address): address {
val storage = lazy MinterStorage.load();
return calcAddressOfJettonWallet(ownerAddress, contract.getAddress(), storage.jettonWalletCode);
}

get fun get_next_admin_address(): address? {
val storage = lazy MinterStorage.load();
return storage.nextAdminAddress;
}
163 changes: 163 additions & 0 deletions contracts/contracts/ccip/cct/JettonWallet.tolk
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import "@stdlib/gas-payments"
import "../../lib/jetton/errors"
import "../../lib/jetton/jetton-utils"
import "../../lib/jetton/storage"
import "../../lib/jetton/messages"
import "fees-management"

contract JettonWallet {
author: "SmartContract Chainlink Limited SEZC"
version: "0.0.1"
description: "link.chain.ton.ccip.cct.JettonWallet"

storage: WalletStorage
incomingMessages: AllowedMessageToWallet
}

type AllowedMessageToWallet =
| AskToTransfer
| AskToBurn
| InternalTransferStep
| TopUpTons

type BounceOpToHandle = InternalTransferStep | BurnNotificationForMinter

fun onBouncedMessage(in: InMessageBounced) {
in.bouncedBody.skipBouncedPrefix();

val msg = lazy BounceOpToHandle.fromSlice(in.bouncedBody);
val restoreAmount = match (msg) {
InternalTransferStep => msg.jettonAmount, // safe to fetch jettonAmount, because
BurnNotificationForMinter => msg.jettonAmount, // it's in the beginning of a message
};

var storage = lazy WalletStorage.load();
storage.jettonBalance += restoreAmount;
storage.save();
}

fun onInternalMessage(in: InMessage) {
val msg = lazy AllowedMessageToWallet.fromSlice(in.body);

match (msg) {
InternalTransferStep => {
var storage = lazy WalletStorage.load();
if (in.senderAddress != storage.minterAddress) {
assert (in.senderAddress == calcAddressOfJettonWallet(msg.transferInitiator!, storage.minterAddress, contract.getCode())) throw ERROR_NOT_VALID_WALLET;
}
storage.jettonBalance += msg.jettonAmount;
storage.save();

if (msg.forwardTonAmount != 0) {
val notifyOwnerMsg = createMessage({
bounce: BounceMode.NoBounce,
dest: storage.ownerAddress,
value: msg.forwardTonAmount,
body: TransferNotificationForRecipient {
queryId: msg.queryId,
jettonAmount: msg.jettonAmount,
transferInitiator: msg.transferInitiator,
forwardPayload: msg.forwardPayload,
}
});
notifyOwnerMsg.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_BOUNCE_ON_ACTION_FAIL);
}

if (msg.sendExcessesTo != null) {
var toLeaveOnBalance = contract.getOriginalBalance() - in.valueCoins + contract.getStorageDuePayment();
reserveToncoinsOnBalance(max(toLeaveOnBalance, calculateJettonWalletMinStorageFee()), RESERVE_MODE_AT_MOST);

val excessesMsg = createMessage({
bounce: BounceMode.NoBounce,
dest: msg.sendExcessesTo,
value: 0,
body: ReturnExcessesBack {
queryId: msg.queryId
}
});
excessesMsg.send(SEND_MODE_CARRY_ALL_BALANCE | SEND_MODE_IGNORE_ERRORS);
}
}

AskToTransfer => {
msg.forwardPayload.checkIsCorrectTLBEither();
assert (msg.transferRecipient.getWorkchain() == MY_WORKCHAIN) throw ERROR_WRONG_WORKCHAIN;
checkAmountIsEnoughToTransfer(in.valueCoins, msg.forwardTonAmount, in.originalForwardFee);

var storage = lazy WalletStorage.load();
assert (in.senderAddress == storage.ownerAddress) throw ERROR_NOT_OWNER;
assert (storage.jettonBalance >= msg.jettonAmount) throw ERROR_BALANCE_ERROR;
storage.jettonBalance -= msg.jettonAmount;
storage.save();

val deployMsg = createMessage({
bounce: BounceMode.Only256BitsOfBody,
dest: calcDeployedJettonWallet(msg.transferRecipient, storage.minterAddress, contract.getCode()),
value: 0,
body: InternalTransferStep {
queryId: msg.queryId,
jettonAmount: msg.jettonAmount,
transferInitiator: storage.ownerAddress,
sendExcessesTo: msg.sendExcessesTo,
forwardTonAmount: msg.forwardTonAmount,
forwardPayload: msg.forwardPayload,
}
});
deployMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL);
}

AskToBurn => {
checkAmountIsEnoughToBurn(in.valueCoins);

var storage = lazy WalletStorage.load();
assert (in.senderAddress == storage.ownerAddress) throw ERROR_NOT_OWNER;
assert (storage.jettonBalance >= msg.jettonAmount) throw ERROR_BALANCE_ERROR;
storage.jettonBalance -= msg.jettonAmount;
storage.save();

val notifyMinterMsg = createMessage({
bounce: BounceMode.Only256BitsOfBody,
dest: storage.minterAddress,
value: 0,
body: BurnNotificationForMinter {
queryId: msg.queryId,
jettonAmount: msg.jettonAmount,
burnInitiator: storage.ownerAddress,
sendExcessesTo: msg.sendExcessesTo,
}
});
notifyMinterMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL);
}

TopUpTons => {
// just accept tons
}

else => throw 0xFFFF
}
}



struct JettonWalletDataReply {
jettonBalance: coins
ownerAddress: address
minterAddress: address
jettonWalletCode: cell
}

get fun get_wallet_data(): JettonWalletDataReply {
var storage = lazy WalletStorage.load();

return {
jettonBalance: storage.jettonBalance,
ownerAddress: storage.ownerAddress,
minterAddress: storage.minterAddress,
jettonWalletCode: contract.getCode(),
}
}

get fun get_status(): int {
var storage = lazy WalletStorage.load();
return storage.status;
}
Loading
Loading