

Escrow Smart Contract
Cliente e prestador depositam o dinheiro em um smart contract. O prestador recebe quando o trabalho está feito e os dois lados concordam.
Como funciona
Cinco passos até o contrato
Crie um smart contract com um assistente simples.
Crie um smart contract com um assistente simples.
- Passo 1Papel no contratoEscolha seu papel no contrato. Há apenas dois papéis: cliente e prestador. Não existe arbitragem de terceiros.9:41LTE
Papel no contrato
- Passo 2Escolha da moedaEscolha a moeda do contrato. GRAM, a moeda nativa da TON, serve para negócios rápidos e pequenos, já que é bastante volátil. USDT é uma moeda mais estável para negócios grandes e longos.9:41LTE
Escolha da moeda
- Passo 3Valor do contratoDigite o valor do contrato na moeda escolhida. Esse valor será transferido ao prestador depois que o trabalho for feito.9:41LTE
Valor do contrato
O valor do contrato é o preço do trabalho. Quem deposita é o cliente.
De 10% a 90%. Padrão: 30%.
0,4% do valor, mas no máximo 15 GRAM.
- Passo 4Endereços das carteirasCole os endereços das carteiras. Depois, essas carteiras serão usadas para depositar e sacar valores.9:41LTE
Endereços das carteiras
- Passo 5Revisão do contratoConfira os dados do contrato antes do deploy.9:41LTE
Revisão do contrato
Papel no contrato
Depois da criação
Cartão do contrato
Você é o cliente, a moeda é USDT. Seis capturas do mesmo cartão do painel real, com todas as ações possíveis em cada etapa.
Recém-criado
Os saldos estão zerados. Você pode depositar o valor do contrato, depositá-lo já com um adicional de até 30% ou excluir o contrato enquanto ninguém tiver depositado nada.
Recarregado, aguardando
O valor do contrato já está no contrato, mas o depósito do prestador ainda não. Enquanto o status não for “Ativo”, você pode sacar seu dinheiro. Ainda é possível adicionar o depósito adicional de até 30%.
O prestador recarregou
Os dois valores combinados estão lá: o contrato está ativo. Os valores estão congelados e não é possível sacar USDT. Você pode aceitar o trabalho, propor outra parte ou adicionar o depósito adicional (garantia extra do cliente).
Depósito de 20% adicionado
Há +200 USDT acima do valor do contrato. Faltam 100 USDT para o teto de 30%. O saque continua bloqueado: um contrato ativo retém o valor do contrato mais o depósito adicional.
80% propostos ao prestador.
O prestador receberá 800 USDT se concordar. O botão “Aceitar o trabalho” sumiu; ele só volta se você definir 100%. A proposta pode ser substituída, e o depósito adicional, complementado.
O prestador concordou
Contrato concluído: 800 USDT foram para o prestador, 4 USDT são os royalties do serviço. Restam 400 USDT para você, que podem ser sacados.
Q&A
Perguntas frequentes
Onde está o terceiro?
Não existe. O prestador arrisca o depósito, então é vantajoso para ele terminar o trabalho.
Por padrão, o depósito do prestador é de 30% do valor do negócio. Pode parecer que ele leva vantagem: ele arrisca 30%, e você, 100%. Mas o que importa não é o percentual, e sim o dinheiro. Esses 30% são dinheiro dele, e perdê-los pode doer tanto para ele quanto perder os seus 100% doeria para você. Ninguém abre mão do próprio dinheiro à toa.
Outro risco: o cliente recebe o trabalho e pode não pagar. Há duas opções. A primeira: quando o trabalho estiver quase pronto, propor acertar as contas antes. A segunda: pedir que o cliente deposite um adicional de até 30% sobre o valor (depósito). Depois que o contrato é concluído, o cliente recebe o depósito de volta, então ele tem motivo para fechar o negócio.
Por que eu deveria confiar em vocês?
Não é preciso confiar. O smart contract é público. O endereço do seu contrato é um hash dos dados iniciais e do código. Se o código fosse trocado, o endereço seria outro.
Você pode abri-lo e conferir (por exemplo, no Tonviewer). A única coisa é que é preciso entender o código — esse é o principal problema. Mas, em compensação, é uma prova 100%. Você também pode entregar o código a um modelo de IA. Abaixo está o código do contrato com a explicação mais simples possível.
ContractUSDT
Esta é a lista de importações: constantes e funções auxiliares.
1import "@contracts/common-messages"2import "@contracts/errors"3import "@contracts/contract_USDT/usdt-utils"4import "fees-management"5import "messages"6import "storage"Breve descrição do contrato.
8contract ContractUSDT {9 author: "Alexandr_K"10 version: "0.0.4"11 description: "Escrow contract USDT"12 storage: StorageUsdt13 incomingMessages: AllowedMessage14}Lista de ações disponíveis com o contrato.
16type AllowedMessage =17 | TransferNotificationForRecipient18 | ChangePercent19 | AcceptPercent20 | RefundUsdt21 | ErrRefund22 | ErrMode23 | BurnContractFunção auxiliar: indica se o contrato está ativo ou não. Quando todos depositam seus valores, o contrato fica ativo e já não dá para sacar simplesmente.
28fun StorageUsdt.isActive(self, extra: ExtraFieldsUsdt): bool {29 if (extra.completed) {30 return false;31 }32 return self.clientAmount >= extra.clientAmountInit &&33 self.providerDeposit >= extra.providerDepositInit;34}Função do troco. Cada mensagem leva um pouco de GRAM para processar a transação. O que sobra, ela devolve.
67fun sendExcessesTon(to: address, amount: coins) {68 if (amount > MIN_MESSAGE_VALUE_BOUND) {69 val excessesMsg = createMessage({70 bounce: BounceMode.NoBounce,71 dest: to,72 value: amount,73 });74 excessesMsg.send(SEND_MODE_IGNORE_ERRORS);75 }76}Calcula o endereço da carteira de jettons. Ele depende do endereço do contrato.
78fun contractUsdtWallet(): address {79 return calcAddressOfJettonWallet(contract.getAddress());80}Se uma transferência falhar, o dinheiro volta e o contrato corrige os registros de quanto cada um tem.
105fun onBouncedMessage(in: InMessageBounced) {106 in.bouncedBody.skipBouncedPrefix();107 val msg = lazy BounceOpToHandle.fromSlice(in.bouncedBody);108 var storage = lazy StorageUsdt.load();109 match (msg) {110 TransferBounce => {111 if (msg.queryId == QueryRole.Client as uint64) {112 storage.clientAmount += msg.jettonAmount;113 } else if (msg.queryId == QueryRole.Provider as uint64) {114 storage.providerDeposit += msg.jettonAmount;115 } else if (msg.queryId == QueryRole.Admin as uint64) {116 var extra: ExtraFieldsUsdt = lazy storage.extra.load();117 extra.royaltyInit += msg.jettonAmount;118 storage.extra = extra.toCell();119 } else {120 // ErrRefund (QueryRole.None) и неизвестные id: учёт не уменьшали121 return;122 }123 }124 }125 storage.save();126}Se o corpo da mensagem estiver vazio, não fazemos nada.
131fun onInternalMessage(in: InMessage) {132 if (in.body.isEmpty()) {133 return;134 }Em seguida, a declaração das variáveis.
137val msg = lazy AllowedMessage.fromSlice(in.body);138var storage: StorageUsdt = lazy StorageUsdt.load();139val tonBalanceBeforeMsg = contract.getOriginalBalance() - in.valueCoins;140val storageFee = MIN_TONS_FOR_STORAGE_THIS_CONTRACT -141min(tonBalanceBeforeMsg, MIN_TONS_FOR_STORAGE_THIS_CONTRACT);142var msgValue = in.valueCoins - storageFee - CONTRACT_GAS_CONSUMPTION;Recarga de USDT: aceita transferências e registra quanto USDT cada um tem.
145TransferNotificationForRecipient => {146 assert (in.senderAddress == contractUsdtWallet()) throw Errors.InvalidWallet;147 assert (msg.transferInitiator != null) throw Errors.InvalidData;148 if (msg.transferInitiator == storage.clientAddress) {149 storage.clientAmount += msg.jettonAmount;150 storage.save();151 } else if (msg.transferInitiator == storage.providerAddress) {152 storage.providerDeposit += msg.jettonAmount;153 storage.save();154 }155 sendExcessesTon(msg.transferInitiator, msgValue);156}Altera o percentual do pagamento. Se o trabalho foi feito só pela metade, você pode propor 50%. Se o prestador concordar, o contrato é cumprido. Se não, continuem negociando.
162ChangePercent => {163 assert (msgValue > 0) throw Errors.NotEnoughGas;164 assert (in.senderAddress == storage.clientAddress) throw Errors.UnauthorizedSender;165 assert (msg.percent >= 0 && msg.percent <= 100) throw Errors.WrongPercent;166 var extra: ExtraFieldsUsdt = lazy storage.extra.load();167 assert (extra.completed == false) throw Errors.ContractIsCompleted;168 assert (storage.isActive(extra)) throw Errors.ContractIsNotActive;169 extra.percent = msg.percent;170 if (msg.percent == 100) {171 extra.completed = true;172 storage.providerDeposit += extra.clientAmountInit - extra.royaltyInit;173 storage.clientAmount -= extra.clientAmountInit;174 }175 storage.extra = extra.toCell();176 storage.save();177 sendExcessesTon(in.senderAddress, msgValue);178}Se o prestador concordar, o contrato paga a ele a parte dele mais o depósito, e o restante volta para você. Com 0%, o negócio inteiro é cancelado e todos recebem o dinheiro de volta. Nesse caso, a taxa de 0,4% não é cobrada.
183AcceptPercent => {184 assert (msgValue > 0) throw Errors.NotEnoughGas;185 assert (in.senderAddress == storage.providerAddress) throw Errors.UnauthorizedSender;186 var extra: ExtraFieldsUsdt = lazy storage.extra.load();187 assert (extra.completed == false) throw Errors.ContractIsCompleted;188 assert (extra.percent != 100) throw Errors.WrongPercent;189 assert (msg.percent == extra.percent) throw Errors.WrongPercent;190 extra.completed = true;191 val percentValue = extra.clientAmountInit / 100 * msg.percent;192 if (msg.percent == 0) {193 extra.royaltyInit = 0;194 }195 storage.providerDeposit += percentValue - extra.royaltyInit;196 storage.clientAmount -= percentValue;197 storage.extra = extra.toCell();198 storage.save();199 sendExcessesTon(in.senderAddress, msgValue);200}O maior bloco: gerencia as devoluções de valores.
208RefundUsdt => {209 msgValue -= (MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT);210 assert (msgValue > 0) throw Errors.NotEnoughGas;211 assert (msg.jettonAmount > 0) throw Errors.InvalidData;212 var extra: ExtraFieldsUsdt = lazy storage.extra.load();213 val transferRecipient: address = msg.addressRefund != null214 ? msg.addressRefund215 : in.senderAddress;216 val isActive = storage.isActive(extra);217 var queryId: uint64;218 var jettonAmount: coins;219 if (in.senderAddress == storage.clientAddress) {220 queryId = QueryRole.Client;221 if (isActive) {222 val deposit: uint72 = (extra.clientAmountInit / 100) * 30;223 val locked = extra.clientAmountInit + deposit;224 assert (storage.clientAmount > locked) throw Errors.NotEnoughJettons;225 jettonAmount = min(storage.clientAmount - locked, msg.jettonAmount);226 } else {227 jettonAmount = min(storage.clientAmount, msg.jettonAmount);228 }229 storage.clientAmount -= jettonAmount;230 } else if (in.senderAddress == storage.providerAddress) {231 queryId = QueryRole.Provider;232 if (isActive) {233 assert (234 storage.providerDeposit > extra.providerDepositInit235 ) throw Errors.NotEnoughJettons;236 jettonAmount = min(237 storage.providerDeposit - extra.providerDepositInit,238 msg.jettonAmount,239 );240 } else {241 jettonAmount = min(storage.providerDeposit, msg.jettonAmount);242 }243 storage.providerDeposit -= jettonAmount;244 } else if (in.senderAddress == extra.adminAddress) {245 queryId = QueryRole.Admin;246 val maxRefundUsdt: uint72 = extra.completed ? extra.royaltyInit : 0;247 jettonAmount = min(maxRefundUsdt, msg.jettonAmount);248 extra.royaltyInit -= jettonAmount;249 storage.extra = extra.toCell();250 } else {251 throw Errors.UnauthorizedSender;252 }253 assert (jettonAmount > 0) throw Errors.NotEnoughJettons;254 storage.save();255 sendAskToTransferUsdt(256 queryId,257 jettonAmount,258 transferRecipient,259 msgValue + MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT,260 );261}Modo de erro. Se uma carteira for perdida ou o dinheiro tiver entrado sem registro, uma parte pode passar o controle para a outra. A rigor, o dinheiro entrar sem registro é impossível, a não ser de propósito. Mas a opção fica, por via das dúvidas. Já esquecer ou perder uma carteira é bem possível.
271ErrMode => {272 assert (msgValue > 0) throw Errors.NotEnoughGas;273 var extra: ExtraFieldsUsdt = lazy storage.extra.load();274 var noticeTo: address? = null;275 var noticeAboutClient: bool = false;276 if (in.senderAddress == storage.clientAddress) {277 if (msg.value == ErrModeVal.ProviderFullRights as int8) {278 extra.errMode = ErrModeVal.ProviderFullRights;279 } else if (msg.value == ErrModeVal.BlockErrorMode as int8) {280 extra.errMode = ErrModeVal.BlockErrorMode;281 } else if (282 msg.value == ErrModeVal.ClientFullRightsAfterHalfYear as int8 &&283 extra.errMode != ErrModeVal.BlockErrorMode as int8284 ) {285 extra.errMode = ErrModeVal.ClientFullRightsAfterHalfYear;286 extra.errModeAt = blockchain.now() as uint64;287 noticeTo = storage.providerAddress;288 noticeAboutClient = true;289 }290 } else if (in.senderAddress == storage.providerAddress) {291 if (msg.value == ErrModeVal.ClientFullRights as int8) {292 extra.errMode = ErrModeVal.ClientFullRights;293 } else if (msg.value == ErrModeVal.BlockErrorMode as int8) {294 extra.errMode = ErrModeVal.BlockErrorMode;295 } else if (296 msg.value == ErrModeVal.ProviderFullRightsAfterHalfYear as int8 &&297 extra.errMode != ErrModeVal.BlockErrorMode as int8298 ) {299 extra.errMode = ErrModeVal.ProviderFullRightsAfterHalfYear;300 extra.errModeAt = blockchain.now() as uint64;301 noticeTo = storage.clientAddress;302 noticeAboutClient = false;303 }304 } else {305 throw Errors.UnauthorizedSender;306 }307 if (noticeTo != null) {308 assert (msgValue > HALF_YEAR_NOTICE_VALUE) throw Errors.NotEnoughGas;309 msgValue -= HALF_YEAR_NOTICE_VALUE;310 }311 storage.extra = extra.toCell();312 storage.save();313 if (noticeTo != null) {314 sendHalfYearControlNotice(noticeTo, noticeAboutClient);315 }316 sendExcessesTon(in.senderAddress, msgValue);317}10enum ErrModeVal {11 ClientFullRights = 112 ProviderFullRights = 213 ClientFullRightsAfterHalfYear = 16 // клиент получает полные права через полгода14 ProviderFullRightsAfterHalfYear = 26 // исполнитель — через полгода15 BlockErrorMode = 9916}ErrRefund: saque de emergência de jettons que chegaram sem notificação e não foram registrados. Ou quando a carteira foi perdida (mais precisamente, a senha dela).
321ErrRefund => {322 msgValue -= (MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT);323 assert (msgValue > 0) throw Errors.NotEnoughGas;324 assert (msg.jettonAmount > 0) throw Errors.InvalidData;325 val extra: ExtraFieldsUsdt = lazy storage.extra.load();326 if (327 !extra.allowsErrRefund(328 in.senderAddress,329 storage.clientAddress,330 storage.providerAddress,331 )332 ) {333 throw Errors.UnauthorizedSender;334 }335 val transferRecipient: address = msg.addressRefund != null336 ? msg.addressRefund337 : in.senderAddress;338 sendAskToTransferUsdt(339 QueryRole.None,340 msg.jettonAmount as coins,341 transferRecipient,342 msgValue + MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT,343 );344}36fun ExtraFieldsUsdt.allowsErrRefund(37 self,38 sender: address,39 client: address,40 provider: address,41): bool {42 if (sender == client) {43 if (self.errMode == ErrModeVal.ClientFullRights as int8) {44 return true;45 }46 return self.errMode == ErrModeVal.ClientFullRightsAfterHalfYear as int8 &&47 (blockchain.now() as uint64) >= self.errModeAt + HALF_YEAR;48 }49 if (sender == provider) {50 if (self.errMode == ErrModeVal.ProviderFullRights as int8) {51 return true;52 }53 return self.errMode == ErrModeVal.ProviderFullRightsAfterHalfYear as int8 &&54 (blockchain.now() as uint64) >= self.errModeAt + HALF_YEAR;55 }56 return false;57}Burn: destrói o contrato para que uma casca vazia não fique solta na rede. Quem pode queimá-lo é quem o criou (o cliente ou o prestador) ou o administrador, e só quando os saldos do cliente e do prestador estão zerados.
351BurnContract => {352 assert (msgValue > 0) throw Errors.NotEnoughGas;353 val extra: ExtraFieldsUsdt = lazy storage.extra.load();354 val deployer = extra.deployerAddress(storage.clientAddress, storage.providerAddress);355 assert (356 in.senderAddress == deployer || in.senderAddress == extra.adminAddress357 ) throw Errors.UnauthorizedSender;358 assert (359 storage.clientAmount == 0 && storage.providerDeposit == 0360 ) throw Errors.BalancesNotEmpty;361 if (extra.completed && extra.royaltyInit > 0) {362 val jettonTon = MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT;363 msgValue -= jettonTon;364 assert (msgValue > 0) throw Errors.NotEnoughGas;365 sendAskToTransferUsdt(366 QueryRole.Admin,367 extra.royaltyInit as coins,368 extra.adminAddress,369 msgValue + jettonTon,370 );371 }372 val burn = createMessage({373 bounce: BounceMode.NoBounce,374 dest: in.senderAddress,375 value: 0,376 });377 burn.send(SEND_MODE_CARRY_ALL_BALANCE | SEND_MODE_DESTROY);378}59fun ExtraFieldsUsdt.deployerAddress(self, client: address, provider: address): address {60 if (self.mode == QueryRole.Client as int8) {61 return client;62 }63 assert (self.mode == QueryRole.Provider as int8) throw Errors.UnauthorizedSender;64 return provider;65}Código-fonte verificado dos três contratos. O hash do código coincide com o que está na rede. Cada link abre o código no TON Verifier.
ContractMinterContractUSDTContractTON