# Tokenizing contracts
## Primitives
**Single-use token:** created once, then it moves back to the origin and consumed. SBT (soul-bound token) conforms to this.
Possible names in stdlib: `OneshotToken`, `OnetimeToken`, `OnceToken`.
**Non-fungible token:** created once, can be transferred and destroyed by the owner.
Possible names in stdlib: `UniqueToken`, `NonfungibleToken`.
**Fungible token:** can be transferred fractionally.
Possible names: `Token`, `FungibleToken`.
Preferred names:
```
OnceToken // one-shot receipt
UniqueToken // NFT
FungibleToken // Jetton
```
```
contract Pair<A: Token, B: Token>: UniqueToken {
let first: Address = A;
let second: Address = B;
let first_amount: uint64 = 0;
let second_amount: uint64 = 0;
action receive_token#<jettonnotify>(token: FungibleToken) {
if (token.minter == self.first) {
self.first_amount += token.amount;
}
else if (token.minter == self.second) {
self.second_amount += token.amount;
}
}
}
contract DEX {
let total: (uint64, uint64);
action add_liquidity#123456(pair: Pair) {
let pool = Pool {
first: pair.first,
second: pair.second,
}
pool.add(pair.first_amount, pair.second_amount, depositor: pair.owner)
}
}
contract Pool {
let owner: Address[DEX];
let first: Address;
let second: Address;
let first_amount: uint64;
let second_amount: uint64;
action add(first: uint64, second: uint64, depositor: Address) {
if (first/second > first_amount/second_amount) {
...
send depositor.transfer_token(first_excess, ...);
} else {
...
send depositor.transfer_token(second_excess, ...);
} else {
// no op
}
}
}
// usage:
let pair = Pair {
first: EUR, // minter address
second: USD
};
send pair.fund(token);
```
v.0.1
```
contract Pair: UniqueToken {
let first: Address;
let second: Address;
let first_amount: uint64 = 0;
let second_amount: uint64 = 0;
action receive_token#<jettonnotify>(token: FungibleToken) {
if (token.minter == self.first) {
self.first_amount += token.amount;
}
else if (token.minter == self.second) {
self.second_amount += token.amount;
}
}
}
contract DEX {
let total: (uint64, uint64);
action add_liquidity#123456(pair: Pair) {
let pool = Pool {
first: pair.first,
second: pair.second,
}
pool.add(pair.first_amount, pair.second_amount, depositor: pair.owner)
}
}
contract Pool {
let owner: Address[DEX];
let first: Address;
let second: Address;
let first_amount: uint64;
let second_amount: uint64;
action add(first: uint64, second: uint64, depositor: Address) {
if (first/second > first_amount/second_amount) {
...
send depositor.transfer_token(first_excess, ...);
} else {
...
send depositor.transfer_token(second_excess, ...);
} else {
// no op
}
}
}
// usage:
let pair = Pair {
first: EUR, // minter address
second: USD
};
pair.fund(token);
```
Each token may be overriden to provide extra functionality and custom storage.
## Initialization options
### A: Minimal StateInit
1. Parent creates StateInit (parent, nonce).
2. Parent sends `init` message with the rest of the data.
3. Token verifies parent's address and stores data.
5. When the parent receives token, it only checks the identity but not the content.
* Parent needs to keep a counter for `nonce` to prevent duplicates.
* Token subtype may define a ±classic initializer focused on its own data, but this does not compose well with parent/nonce data.
### B: Maximal StateInit
1. Parent creates StateInit with all the data. Some of that data
## Multisig V1
1. `Multisig` contract controls various assets (coins, tokens) view messages. Contract contains a static list of participants.
2. Participants’ wallets initiate `Request` with an arbitrary message that is issued by `Multisig`.
3. `Request` collects votes from the participants.
4. When the threshold is reached, `Request` sends itself (and self-destructs) to `Multisig` to perform the request.
Initiate: `wallet` -> `multisig` -> `request`
Vote & unlock: `wallet`-> `request` -> `multisig` -> forward
In this scheme `Request` inherits `SingleToken` so it handles issuance and redemption by Multisig contract.
Option A1:
```
contract Multisig {
let threshold: Weight;
let parties: Hashmap[Address, Uint32];
let timeout: TimeInterval = 24*3600;
let mut seqno: Inc[64] = 0;
internal issue_request(message: RawMessage, mode: SendMode) {
let seqno = self.nonce.increment();
let req = Request.init(self.address(), seqno);
send req.mint(
message,
mode,
expiration: now() + self.timeout,
remaining_parties: parties,
threshold,
)
}
internal fulfill#ff11ff11(request: Request) {
// here the request is already verified
send_raw(request.message, request.mode);
}
action fallback() {
// do nothing: should we ignore
}
}
```
```
// How can this be uninitialized until minted???...
contract Request: OnceToken[Multisig] {
let message: RawMessage;
let mode: SendMode;
let expiration: Timestamp;
// with every new vote, an item will be removed and threshold will decrement
let mut remaining_parties: Hashmap[Address, Uint32];
let mut threshold: Uint32;
// This method is not even reached until Request is initialized
internal vote#ddeeffaa(msg.sender: Address, weight: Weight) {
verify(now() < self.expiration, "Request expired");
let weight = self.remaining_parties.remove(msg.sender) or fail("Not authorized");
this.threshold = this.threshold - weight or 0;
if this.threshold == 0 {
self.redeem(Multisig.fulfill)
}
}
}
```
```
contract OnceToken[W: Contract] {
let owner: Address[W];
let nonce: Inc[64];
let mut content: Maybe[T];
// Stateinit is fixed to the owner+nonce,
// all other state is provided dynamically
init(owner: Address[W], nonce: Inc[64]) {
self.owner = owner;
self.nonce = nonce;
self.content = None;
}
internal dispatch() {
super.dispatch(); // !!! seems like we need inheritance anyway
match self.content {
Some(c) => c.dispatch(msg);
}
}
internal mint#aabbccdd() {
verify(msg.sender == self.owner);
self.content = T;
}
//
fn get_data(slice: Slice) -> Slice {
}
fn redeem(selector: Selector[(Self)]) {
// self-destroy and send out
send(All+Destroy) self.owner.selector(self.content)
}
}
```
Option A2:
```
contract Multisig {
let threshold: Weight;
let parties: Hashmap[Address, Uint32];
let timeout: TimeInterval = 24*3600;
let mut seqno: Inc[64] = 0;
internal issue_request(message: RawMessage, mode: SendMode) {
let seqno = self.nonce.increment();
let token = OnceToken.init(self.address(), seqno);
send token.mint(
Request.init(
message,
mode,
expiration: now() + self.timeout,
remaining_parties: parties,
threshold,
)
)
}
internal fulfill#ff11ff11(request: Request) {
// here the request is already verified
send_raw(request.message, request.mode);
}
action fallback() {
// do nothing: should we ignore
}
}
```
```
contract Request {
let message: RawMessage;
let mode: SendMode;
let expiration: Timestamp;
// with every new vote, an item will be removed and threshold will decrement
let mut remaining_parties: Hashmap[Address, Uint32];
let mut threshold: Uint32;
// This method is not even reached until Request is initialized
internal vote#ddeeffaa(msg.sender: Address, weight: Weight) {
verify(now() < self.expiration, "Request expired");
let weight = self.remaining_parties.remove(msg.sender) or fail("Not authorized");
this.threshold = this.threshold - weight or 0;
if this.threshold == 0 {
// !!! requires inheritance too...
self.redeem(@selector(Multisig.fulfill))
}
}
}
```
```
contract OnceToken[W: Contract, T: Contract] {
let owner: Address[W];
let nonce: Inc[64];
let mut content: Maybe[T];
init(owner: Address[W], nonce: Inc[64]) {
self.owner = owner;
self.nonce = nonce;
self.content = None;
}
internal dispatch() {
super.dispatch(); // !!! seems like we need inheritance anyway
match self.content {
Some(c) => c.dispatch(msg);
}
}
internal mint#aabbccdd(c: T) {
verify(msg.sender == self.owner);
self.content = T;
}
//
fn get_data(slice: Slice) -> Slice {
}
fn redeem(selector: Selector[W]) {
// self-destroy and send out
send(All+Destroy) self.owner.selector(self.content)
}
}
```
-------------------------
```
contract Multisig {
let threshold: Weight;
let owners: Hashmap[Address, Weight];
let timeout: TimeInterval;
contract Request: OnceToken {
// OnceToken provides nonmutable field `owner: Address`.
// All nonmutable fields are inherited (threshold, owners)
// and unused ones optimized away (timeout).
// More immutable fields affect stateinit.
let message: RawMessage;
let mode: SendMode;
let expiration: Timestamp;
// Mutable fields MUST have static defaults.
// These are not part of stateinit.
let mut threshold: Weight = 0;
let mut remaining_voters: Hashmap[Address, Weight] = Hashmap.Empty;
action vote(msg.sender: Address, weight: Weight) {
verify(now() < self.expiration, "Request expired");
let weight = self.remaining_voters.remove(msg.sender) or fail("Not authorized");
this.threshold = this.threshold - weight or 0;
if this.threshold == 0 {
OnceToken::redeem_to("fulfill", 10)
}
}
}
action issue_request(message: RawMessage, mode: SendMode) {
send Request {
message,
mode,
expiration: now() + self.timeout,
}.init(threshold, owners); // ??? how to pass in self.address as owner?
}
action fulfill(request: Request, value: Int32) {
// here the request is already verified
send_raw(request.message, request.mode);
}
action(Request) fulfill(value: Int32) {
// here the request is already verified
send_raw(request.message, request.mode);
}
/*
action[request: Request] fulfill(value: Int32) {
}
action fulfill(resource req: Request, value: Int32) {
}
action fulfill(req: resource Request, value: Int32) {
}
action
Value: Int32
Req: Request
resources
request: Request
do
...
*/
/* // attempt to define return sytnactically,
// but direct the message to Request instead
action vote(weight: Weight)
using Request.vote(weight)
returns (value: Int32)
{
// here the request is already verified
send_raw(request.message, request.mode);
}
*/
action fallback() {
// do nothing: should we ignore
}
}
contract OnetimeToken[T: Interface] {
let owner: Address[T];
fn init_local(owner: T /* where do we get it from? */) {
self.owner = owner;
}
fn init_self(msg) {
verify(msg.sender == self.owner);
}
fn __prepare_to_send__() {
// specify flags
}
fn __verify_receipt__(msg, owner: T) {
verify(msg.sender == Request {
owner: $current_contract.address(),
message: request.message,
mode: request.mode,
expiration: request.expiration,
}.address());
}
fn redeem() {
// self-destroy and send out
}
}
// old stuff
contract Request: OnetimeToken[Multisig] {
let message: RawMessage;
let mode: SendMode;
let expiration: Timestamp;
let threshold: Weight = 0;
let voters: Hashmap[Address, Weight] = Hashmap.Empty;
override init(
msg.sender: Multisig,
threshold: Weight,
voters: Hashmap[Address, Weight]
) {
super.init(msg.sender);
self.threshold = threshold;
self.voters = voters;
}
}
/*
internal change_wallet(new_address: Address) {
let weight = self.members.remove(msg.sender) or fail("Not authorized");
self.members.insert(new_address, weight);
// send the remaining coins to the new address
send(IgnoreErrors+InboundCoins) new_address.transfer();
}
*/
```
## Multisig V1 - broken
Uses built-in dictionary with members.
```
type Weight = Uint64;
contract Multisig {
let members[Address, Weight];
let threshold: Weight;
internal vote(
message: RawMessage,
flags: MsgFlags,
nonce: Uint64,
msg.amount: Coins(Ton(0.1)),
) {
let weight = self.members.find(msg.sender) or fail("Not authorized");
let req = Request.init(
parent: self.address,
message,
flags,
self.threshold,
);
send(InboundCoins) req.vote(weight, msg.amount: MinRequestBalance);
}
internal fulfill_request(data: Request.Data) {
verify(data.parent == self.address());
verify(msg.sender == Request.init(*data).address());
send_raw(data.message, data.flags);
}
}
contract Request {
let parent: Multisig;
let message: RawMessage;
let flags: MsgFlags;
let threshold: Weight;
let expiration: Timestamp;
internal vote(weight: Weight) {
verify(now() < self.expiration, "Request expired");
verify(msg.sender == self.parent, "Not authorized");
this.threshold = this.threshold - weight or 0;
if this.threshold == 0 {
send(All+Destroy) self.parent.fullfill_request(self);
}
}
}
/*
internal change_wallet(new_address: Address) {
let weight = self.members.remove(msg.sender) or fail("Not authorized");
self.members.insert(new_address, weight);
// send the remaining coins to the new address
send(IgnoreErrors+InboundCoins) new_address.transfer();
}
*/
```
## Multisig V1 - broken
Uses built-in dictionary with members.
```
type Weight = Uint64;
let MinRequestBalance = Coins(ton: 0.1);
contract Multisig {
let members[Address, Weight];
let threshold: Weight;
internal initiate_request(
message: RawMessage,
flags: MsgFlags,
msg.amount: MinRequestBalance
) {
let weight = self.members.find(msg.sender) or fail("Not authorized");
let req = Request.init(
parent: self.address,
message,
flags,
self.threshold,
);
send(InboundCoins) req.vote(weight, msg.amount: MinRequestBalance);
}
internal fulfill_request(data: Request.Data) {
verify(data.parent == self.address());
verify(msg.sender == Request.init(*data).address());
send_raw(data.message, data.flags);
}
internal change_wallet(new_address: Address) {
let weight = self.members.remove(msg.sender) or fail("Not authorized");
self.members.insert(new_address, weight);
// send the remaining coins to the new address
send(IgnoreErrors+InboundCoins) new_address.transfer();
}
}
contract Request {
let parent: Multisig;
let message: RawMessage;
let flags: MsgFlags;
let threshold: Weight;
let expiration: Timestamp;
internal vote(weight: Weight) {
verify(now() < self.expiration, "Request expired");
verify(msg.sender == self.parent, "Not authorized");
this.threshold = this.threshold - weight or 0;
if this.threshold == 0 {
send(All+Destroy) self.parent.fullfill_request(self);
}
}
}
```
## Multisig V2
Uses jetton to represent membership votes.
Multisig contract receives coins with a message and returns them back to the user while creating the request.
```
let MinRequestBalance = Coins(ton: 0.1);
struct RequestData {
message: RawMessage,
flags: MsgFlags,
}
contract Multisig {
// trusted minter contract - this can be one-off contract.
let minter: Address;
impl JettonRecipient {
internal transfer_notification(
query_id:Uint64
amount: JettonAmount
sender: Address,
forward_payload:Either[Cell[RequestData],Ref[RequestData],
msg.amount: MinRequestBalance)
) {
let token = MultisigToken.init(
master: self.minter,
owner: sender
);
verify(msg.sender == token.address(), "Invalid token");
let reqdata = match forward_payload {
case Either.Left(let x): x.read(),
case Either.Right(let x): x.read(),
};
let req = Request.init(
parent: self.address,
reqdata.message,
reqdata.flags,
self.threshold,
);
send(InboundCoins) req.vote(amount, msg.amount: MinRequestBalance);
}
}
internal fulfill_request(data: Request.Data) {
verify(data.parent == self.address());
verify(msg.sender == Request.init(*data).address());
send_raw(data.message, data.flags);
}
}
contract Request {
let parent: Multisig;
let message: RawMessage;
let flags: MsgFlags;
let threshold: JettonAmount;
let expiration: Timestamp;
internal vote(weight: JettonAmount) {
verify(now() < self.expiration, "Request expired");
verify(msg.sender == self.parent, "Not authorized");
this.threshold = this.threshold - weight or 0;
if this.threshold == 0 {
send(All+Destroy) self.parent.fullfill_request(self);
}
}
}
// Voting token - implements standard Jetton
contract MultisigToken: Jetton {
}
```