Case Studies
1. Skate Builder
About
Skate Builder is an ongoing testnet campaign that involves collecting different parts of a skateboard across multiple blockchains and assembling the complete skateboard on those various chains.
Check it out at https://park.skatechain.org/en/nft!
Skate Builder NFT as a TON SkateApp
Skate Builder NFT is a SkateApp and TEP-62 NFT comprises of two contracts:
-
Collection Contract (the SkateApp):
- Defines the properties of the Skate Builder NFT, including SkateBoard metadata for the collection and individual items, as well as the NFT item code.
- Specifies the structure for user intents to mint/build a skateboard.
- Provides a receive handler for the Skate Gateway to mint the necessary parts after processing user intents.
-
Item Contract (minted by Collection contract):
- Manages the ownership of the NFT (note: SkateBuilder NFTs are non-transferable).
- Stores the metadata URI associated with the NFT.
The Skate Builder NFT is non-transferable. Additionally, the NFT follows an ordered but non-sequential index, with missing indices belonging to other VMs. For example, NFTs may reside on platforms like EVM or Solana.
To interact with the Skate Gateway, the contract includes a mint_to#775281a7 user:address token_id:uint256 part:uint8 = MintTo
handler that can exclusively be called by Skate Gateway. This action is intended to be initiated by Skate executors with the following signature:
receive(msg: MintTo) {
self.only_gateway();
require(msg.part >=0 && msg.part <=3, "SkateBuilder.MintTo()::Invalid skateboard part, must be within 0-3");
let nft_init = self._get_nft_item_state_init(msg.token_id);
let ctx: Context = context();
send(SendParameters{
to: contractAddress(nft_init),
value: 0,
mode: SendRemainingValue,
body: Transfer{
query_id: 0,
new_owner: msg.user,
response_destination: newAddress(0, 0),
custom_payload: self.nft_parts_content.get(msg.part),
forward_amount: 0,
forward_payload: emptySlice(),
}.toCell(),
code: nft_init.code,
data: nft_init.data,
})
}
Mind the only_gateway()
modifier inside this function.
The source code is available at https://verifier.ton.org/EQAxQ0pJHat3VqT0nVsYZuk_zp0sISk-hSSWHzfxbpILAU6S?testnet=
2. Polymarket on TON
About
Skate is developing a Polymarket connector on TON to allow millions of TON users to seamlessly participate in Polymarket.
A sneak peek demo was showcased on our Twitter: https://x.com/skate_chain/status/1819265829244211340/video/1
This is still a work in progress, but we will be launching a mainnet version with updated code in the near future!
Polymarket as a TON SkateApp
-
To enable user intent for asset transfers, such as transferring USDT on TON for placing a bet, we use the
SkateInitiateTaskNotification
feature to register the intent by utilizing custom payload fromJettonTransferNotification
whenever USDT is moved from the user’s wallet to Skate. -
To settle a bet, user simply signs their request.
-
The rest of the intent settlement flow by the executor follows the same process as all Skate App transactions.
contract PolyMarket with Deployable, SkateAppBase, Ownable {
...
inline fun construct_initiate_params(query_id: Int, user: Address, execution_data: Cell): SkateInitiateTask {
let initiate_params = SkateInitiateTask {
query_id: query_id,
user: user, // the actual jetton wallet owner.
processing_fee: self.SKATE_FEE + self.NOTIFICATION_COST,
execution_info: ExecutionInfo {
value: 0,
expiration: now() + 120, // 2 minutes
payload: Payload {
destination: Destination {
chain_id: self.POLYGON_CHAIN,
chain_type: self.EVM_TYPE,
address: self.POLYMARKET_CTF,
}.toCell(),
data: execution_data,
}
}
};
return initiate_params;
}
//////////// User interactions ////////////
//// 1. PLACE BET
const TRANSFER_USDT_GAS: Int = ton("0.0028");
receive(msg: JettonTransferNotification) {
let ctx: Context = context();
require(ctx.value >= self.USER_FEE, "PolyMarket::Not enough user fee!");
let bet_config: BetConfig = BetConfig.fromSlice(msg.forward_payload);
require(msg.amount >= self.MIN_BET_SIZE, "PolyMarket::JettonTransferNotification::Bet size too small!");
require(bet_config.candidate_id <= self.MAX_CANDIDATE_ID, "PolyMarket::JettonTransferNotification::Invalid candidate id!");
// 1. Create and save the bet struct
let new_bet = RequestPlaceBet {
candidate_id: bet_config.candidate_id,
usd_amount: msg.amount, // Jetton USDT amount
direction: bet_config.direction,
};
// 2. Notify SkateGateway to initiate a task
self.query_id += 1;
let initiate_params = self.construct_initiate_params(self.query_id, msg.sender, new_bet.toCell());
self.notify_gateway(initiate_params);
// NOTE: rebate this amount for user as gas consumption
send(SendParameters{
to: msg.sender,
value: self.TRANSFER_USDT_GAS + ctx.value,
mode: SendPayGasSeparately,
})
}
//// 2. Settle Bet - sign a request using `signData` in tonConnect V2
get fun settle_hash(candiate_id: Int, share_amount: Int): Int {
let hash = beginCell()
.storeUint(candidate_id, 8)
.storeUint(share_amount, 256) // conditional token amount, 18-decimals
.storeUint(0, 32) // workchain
.endCell().hash();
return hash;
}
///////////////////////////////////////////
////////// Executor interactions //////////
// NOTE: Amount is verified by relayer, this only happens after actual execution is done on PolyMarket (Polygon)
// Settle the bet by:
// 1. Transfer Jetton (USDT) TO USER
// 2. Transfer fee to executor.
receive(msg: SettleBet) {
self.only_gateway();
let ctx: Context = context();
// 1 Transfer USDT
let transfer_msg = beginCell()
.storeUint(OP_JettonTransfer, 32) // opcode
.storeUint(0, 64) // queryId
.storeCoins(msg.usd_amount) // usd amount
.storeAddress(msg.user) // to address
.storeAddress(myAddress()) // response address
.storeUint(0, 1) // custom payload
.storeCoins(0) // forward_amount
.storeUint(0, 1) // forward_payload
.endCell();
send(SendParameters{
to: self.jetton_wallet,
value: self.TRANSFER_USDT_COST, // NOTE: depends on payload size, this must be enough to pay gas to transfer
mode: SendPayGasSeparately, // make sure user get their amount
body: transfer_msg
});
// 2. Transfer ton to executor
if (self.EXECUTOR_FEE > 0) {
send(SendParameters{
to: msg.fee_receiver,
value: self.EXECUTOR_FEE, // NOTE: depends on payload size, this must be enough to pay gas to transfer
mode: SendPayGasSeparately, // make sure user get their amount
});
}
}
...