diff --git a/sources/contracts/errors.tolk b/sources/contracts/errors.tolk index 27b4fc4..f3e6901 100644 --- a/sources/contracts/errors.tolk +++ b/sources/contracts/errors.tolk @@ -33,6 +33,8 @@ const WITHDRAWN_LIQUIDITY_AMOUNT_IS_LESS_THAN_LIMIT = 57106 // // proofs const INVALID_STATE_INIT_PROOF = 10789 +const SENDER_IS_NOT_THE_DISCOVERY_REQUESTER = 1543 +const SENDER_IS_NOT_THE_MINTER = 11580 // actions const SENDER_IS_NOT_THE_VAULT_JETTON_WALLET = 14041 // TODO: separate opcodes here diff --git a/sources/contracts/jetton-vault.tolk b/sources/contracts/jetton-vault.tolk index 1df107a..2332147 100644 --- a/sources/contracts/jetton-vault.tolk +++ b/sources/contracts/jetton-vault.tolk @@ -12,6 +12,8 @@ struct JettonVaultStorage { ammPoolCode: cell liquidityDepositContractCode: cell jettonWalletCode: cell + // TEP89 proof check proxy code + proxyCode: cell } fun JettonVaultStorage.load() { @@ -42,7 +44,13 @@ fun onInternalMessage(in: InMessage) { if (storage.jettonWallet == null) { // TODO: handle proofs try { - checkProof(msg.jettonVaultPayload.proof, storage.jettonMaster, in.senderAddress); + checkProof( + msg.jettonVaultPayload.proof, + storage.jettonMaster, + in.senderAddress, + msg.toCell(), + storage.proxyCode + ); } catch (error) { // refund if error msg.refundWithError(in.senderAddress, error); diff --git a/sources/contracts/messages.tolk b/sources/contracts/messages.tolk index 26c2f5f..45b436e 100644 --- a/sources/contracts/messages.tolk +++ b/sources/contracts/messages.tolk @@ -206,3 +206,24 @@ fun JettonTransferNotificationWithVaultAction.refundWithError(self, sender: addr commitContractDataAndActions(); throw error; } + +struct (0x7a1267fd) TEP89DiscoveryResult { + discoveryId: uint64 + expectedJettonWallet: address + actualJettonWallet: address? + action: Cell +} + +struct (0xd1735400) TakeWalletAddress { + queryId: uint64 + jettonWalletAddress: address? + ownerAddress: Cell
? +} + +struct (0x2c76b973) ProvideWalletAddress { + queryId: uint64 + ownerAddress: address + includeAddress: bool +} + +type AllowedMessagesForProxy = TakeWalletAddress diff --git a/sources/contracts/proof.tolk b/sources/contracts/proof.tolk index e2e0262..6146e21 100644 --- a/sources/contracts/proof.tolk +++ b/sources/contracts/proof.tolk @@ -1,4 +1,5 @@ import "errors" +import "storage" import "messages" fun address.isFromStateInitInBasechain(self, stateInit: StateInit) { @@ -8,12 +9,42 @@ fun address.isFromStateInitInBasechain(self, stateInit: StateInit) { }.addressMatches(self); } -fun checkProof(proof: JettonVaultProof, minter: address, msgSender: address) { +fun exit(): void + asm """ +<{ }> PUSHCONT +CALLCC +""" + +fun checkProof( + proof: JettonVaultProof, + minter: address, + msgSender: address, + msgCell: Cell, + tep89ProxyCode: cell, +) { match (proof) { NoProof => { return; } - MinterDiscoveryProof => {} + MinterDiscoveryProof => { + val proxyCode = tep89ProxyCode; + val proxyData = ProxyStorage { + discoveryId: blockchain.now(), + minter, + expectedJettonWallet: msgSender, + discoveryRequester: contract.getAddress(), + action: msgCell, + }.toCell(); + + val proxyMsg = createMessage({ + dest: { stateInit: { code: proxyCode, data: proxyData } }, + bounce: false, + value: 0, + }); + + proxyMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); + exit(); + } OnchainGetterProof => { val vaultJettonWallet = calcJettonWalletAddressWithOnchainGetter( contract.getAddress(), diff --git a/sources/contracts/storage.tolk b/sources/contracts/storage.tolk index 779b81d..9b0525f 100644 --- a/sources/contracts/storage.tolk +++ b/sources/contracts/storage.tolk @@ -144,3 +144,15 @@ fun address.isAddressOfLiquidityDeposit( ); return liquidityDeposit.addressMatches(self); } + +struct ProxyStorage { + minter: address + discoveryRequester: address + expectedJettonWallet: address + action: Cell + discoveryId: uint64 // Unique discovery ID (salt) +} + +fun ProxyStorage.load() { + return ProxyStorage.fromCell(contract.getData()); +} diff --git a/sources/contracts/tep89-proxy.tolk b/sources/contracts/tep89-proxy.tolk new file mode 100644 index 0000000..0d39107 --- /dev/null +++ b/sources/contracts/tep89-proxy.tolk @@ -0,0 +1,68 @@ +import "storage" +import "errors" +import "messages" + +// Proxy for TEP-89 wallet discovery process +// Handles interaction with Jetton Master and result delivery +fun onInternalMessage(in: InMessage) { + var msg = lazy AllowedMessagesForProxy.fromSlice(in.body); + match (msg) { + TakeWalletAddress => { + var storage = lazy ProxyStorage.load(); + assert (in.senderAddress == storage.minter) throw SENDER_IS_NOT_THE_DISCOVERY_REQUESTER; + // Return discovery result to requester + val msg = createMessage({ + dest: storage.discoveryRequester, + bounce: false, + value: 0, + body: TEP89DiscoveryResult { + discoveryId: storage.discoveryId, + expectedJettonWallet: storage.expectedJettonWallet, + actualJettonWallet: msg.jettonWalletAddress, + action: storage.action, + }, + }); + + msg.send(SEND_MODE_CARRY_ALL_BALANCE | SEND_MODE_DESTROY); + } + // will be triggered on deployment + else => { + var storage = lazy ProxyStorage.load(); + assert (in.senderAddress == storage.discoveryRequester) throw SENDER_IS_NOT_THE_MINTER; + val msg = createMessage({ + dest: storage.minter, + bounce: true, // So we can save some tons (we won't pay storage fees for JettonMaster) + value: 0, + body: ProvideWalletAddress { + queryId: 0, + ownerAddress: storage.discoveryRequester, + // We could ask to provide address, but it is cheaper to store it in data + includeAddress: false, + }, + }); + + msg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); + } + } +} + +fun onBouncedMessage(in: InMessageBounced) { + // handle failed discovery request + in.bouncedBody.skipBouncedPrefix(); + + val msg = lazy ProvideWalletAddress.fromSlice(in.bouncedBody); + var storage = lazy ProxyStorage.load(); + val bounceMsg = createMessage({ + dest: storage.discoveryRequester, + bounce: false, + value: 0, + body: TEP89DiscoveryResult { + discoveryId: storage.discoveryId, + expectedJettonWallet: storage.expectedJettonWallet, + actualJettonWallet: null, + action: storage.action, + }, + }); + + bounceMsg.send(SEND_MODE_CARRY_ALL_BALANCE | SEND_MODE_DESTROY); +} diff --git a/sources/tolk-toolchain/generator.ts b/sources/tolk-toolchain/generator.ts index 0f097df..08c0d87 100644 --- a/sources/tolk-toolchain/generator.ts +++ b/sources/tolk-toolchain/generator.ts @@ -72,6 +72,7 @@ export const createJettonVaultContract = async (jettonMaster: Address) => { ammPoolCode: dex["amm-pool"], liquidityDepositContractCode: dex["liquidity-deposit"], jettonWalletCode: dex["lp-jetton-wallet"], + proxyCodeCell: dex["tep89-proxy"], }, dex["jetton-vault"], ) diff --git a/sources/tolk-toolchain/sources.ts b/sources/tolk-toolchain/sources.ts index 0537878..3ee6ce5 100644 --- a/sources/tolk-toolchain/sources.ts +++ b/sources/tolk-toolchain/sources.ts @@ -8,6 +8,7 @@ export const DEX_SOURCES = { "jetton-vault": "sources/contracts/jetton-vault.tolk", "sharded-jetton-minter": "sources/contracts/sharded-jettons/jetton-minter-contract.tolk", "sharded-jetton-wallet": "sources/contracts/sharded-jettons/jetton-wallet-contract.tolk", + "tep89-proxy": "sources/contracts/tep89-proxy", } export type ContractName = keyof typeof DEX_SOURCES diff --git a/sources/tolk-wrappers/JettonVault.ts b/sources/tolk-wrappers/JettonVault.ts index d37b48b..8a442b7 100644 --- a/sources/tolk-wrappers/JettonVault.ts +++ b/sources/tolk-wrappers/JettonVault.ts @@ -23,6 +23,7 @@ export type JettonVaultConfig = { ammPoolCode: Cell liquidityDepositContractCode: Cell jettonWalletCode: Cell + proxyCodeCell: Cell } export type NoProof = { @@ -57,6 +58,7 @@ export function jettonVaultConfigToCell(config: JettonVaultConfig): Cell { .storeRef(config.ammPoolCode) .storeRef(config.liquidityDepositContractCode) .storeRef(config.jettonWalletCode) + .storeRef(config.proxyCodeCell) .endCell() } diff --git a/sources/tolk-wrappers/TEP89Proxy.ts b/sources/tolk-wrappers/TEP89Proxy.ts new file mode 100644 index 0000000..82cf878 --- /dev/null +++ b/sources/tolk-wrappers/TEP89Proxy.ts @@ -0,0 +1,56 @@ +import { + Address, + beginCell, + Cell, + Contract, + ContractABI, + contractAddress, + ContractProvider, + Sender, + SendMode, +} from "@ton/core" + +export type TEP89ProxyConfig = { + discoveryId: number + jettonMaster: Address + expectedJW: Address + discoveryRequester: Address + action: Cell +} + +export function TEP89ProxyConfigToCell(config: TEP89ProxyConfig): Cell { + return beginCell() + .storeUint(config.discoveryId, 64) + .storeAddress(config.jettonMaster) + .storeAddress(config.expectedJW) + .storeAddress(config.discoveryRequester) + .storeRef(beginCell().endCell()) + .endCell() +} + +export class TEP89Proxy implements Contract { + abi: ContractABI = {name: "TEP89Proxy"} + + constructor( + readonly address: Address, + readonly init?: {code: Cell; data: Cell}, + ) {} + + static createFromAddress(address: Address) { + return new TEP89Proxy(address) + } + + static createFromConfig(config: TEP89ProxyConfig, code: Cell, workchain = 0) { + const data = TEP89ProxyConfigToCell(config) + const init = {code, data} + return new TEP89Proxy(contractAddress(workchain, init), init) + } + + async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { + await provider.internal(via, { + value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell().endCell(), + }) + } +}