Ужыванне смарт-акаўнтаў Waves: ад аўкцыёнаў да бонусных праграм

Ужыванне смарт-акаўнтаў Waves: ад аўкцыёнаў да бонусных праграм

Блокчейн часта асацыюецца толькі з криптовалютами, але вобласці ўжывання тэхналогіі DLT значна шырэй. Адно з самых перспектыўных напрамкаў для прымянення блокчейна - смарт-кантракт, які выконваецца аўтаматычна і не патрабуе даверу паміж бакамі, якія яго заключылі.

RIDE - мова для смарт-кантрактаў

Waves распрацавала адмысловую мову для смарт-кантрактаў – RIDE. Яго поўная дакументацыя знаходзіцца тут. А вось - артыкул на гэтую тэму на Хабры.

Кантракт на RIDE з'яўляецца прэдыкатам і вяртае на выхадзе "true" ці "false". Адпаведна, транзакцыя альбо запісваецца ў блокчэйн, альбо адпрэчваецца. Смарт-кантракт поўнасцю гарантуе выкананне зададзеных умоў. Генерацыя транзакцый з кантракта ў RIDE на дадзены момант немагчымая.

На сёння існуе два тыпу смарт-кантрактаў Waves: смарт-акаўнты і смарт-асеты. Смарт-акаўнт - гэта звычайны акаўнт карыстальніка, але для яго задаецца скрыпт, які кантралюе ўсе транзакцыі. Скрыпт смарт-акаўнта можа выглядаць, напрыклад, так:

match tx {
  case t: TransferTransaction | MassTransferTransaction => false
  case _ => true
}

tx - апрацоўваная транзакцыя, якую мы дазваляем, выкарыстоўваючы механізм патэрн-матчынга, толькі ў выпадку, калі яна не з'яўляецца транзакцыяй перакладу. Патэрн-матчынг у RIDE выкарыстоўваецца для праверкі тыпу транзакцыі. У скрыпце смарт-акаўнта могуць быць апрацаваны ўсе існуючыя тыпы транзакцый.

Таксама ў скрыпце могуць аб'яўляцца зменныя, выкарыстоўвацца канструкцыі "if-then-else" і іншыя метады паўнавартаснай праверкі ўмоў. Каб кантракты мелі даказальную завяршальнасць і складанасць (кошт), якую лёгка прадказаць да пачатку выканання кантракту, RIDE не ўтрымоўвае цыклаў і аператараў тыпу jump.

Сярод іншых асаблівасцяў акаўнтаў Waves - наяўнасць «стэйта», гэта значыць станы акаўнта. У стейт акаўнта можна запісаць бясконцую колькасць пар (ключ, значэнне) з дапамогай дата-транзакцый (DataTransaction). Далей гэтую інфармацыю можна апрацоўваць як праз REST API, так і напрамую ў смарт-кантракце.

Кожная транзакцыя можа змяшчаць масіў пруфаў (proofs), у які можна ўнесці подпіс удзельніка, ID неабходнай транзакцыі і г.д.

Праца з RIDE праз IDE дазваляе бачыць скампіляваны від кантракта (калі ён кампілюецца), ствараць новыя акаўнты і задаць для яго скрыпты, а таксама пасылаць транзакцыі праз камандны радок.

Для паўнавартаснага цыклу, які ўключае стварэнне акаўнта, усталёўку на яго смарт-кантракту і адпраўку транзакцый, можна таксама выкарыстоўваць бібліятэку для ўзаемадзеяння з REST API (напрыклад, C#, C, Java, JavaScript, Python, Rust, Elixir). Для пачатку працы з IDE дастаткова націснуць кнопку NEW.

Магчымасці прымянення смарт-кантрактаў шырокія: ад забароны транзакцый на пэўныя адрасы ("чорны спіс") да складанаскладаных dApps.

Цяпер разгледзім канкрэтныя прыклады прымянення смарт-кантрактаў у бізнэсе: пры правядзенні аўкцыёнаў, страхаванні і стварэнні праграм лаяльнасці.

Аўкцыёны

Адна з умоў паспяховага правядзення аўкцыёну - празрыстасць: удзельнікі павінны быць упэўненыя ў немагчымасці маніпуляцый стаўкамі. Гэтага можна дасягнуць дзякуючы блокчейну, дзе нязменныя дадзеныя аб усіх стаўках і часе, калі яны былі зроблены, будуць даступныя ўсім удзельнікам.

На блокчейне Waves стаўкі могуць запісвацца ў стэйт акаўнта аўкцыёну з дапамогай DataTransaction.

Таксама можна задаць час пачатку і канчаткі аўкцыёну з дапамогай нумароў блокаў: частата генерацыі блока ў блокчейне Waves прыкладна роўная 60 секундам.

1. Англійская аўкцыён павышаецца цаны

Удзельнікі ангельскага аўкцыёну робяць стаўкі, спаборнічаючы сябар з сябрам. Кожная новая стаўка павінна перавышаць папярэднюю. Аўкцыён заканчваецца, калі больш няма ахвочых перавысіць апошнюю стаўку. У гэтым выпадку ўдзельнік, які зрабіў самую высокую стаўку, павінен выдзеліць заяўленую суму.

Існуе і варыянт аўкцыёну, у якім прадавец устанаўлівае мінімальную цану лота, і фінальная цана павінна яе перавышаць. У адваротным выпадку лот застаецца непрададзеным.

У гэтым прыкладзе мы працуем з акаўнтам, спецыяльна створаным для правядзення аўкцыёну. Працягласць аўкцыёну складае 3000 блокаў, а пачатковы кошт лота – 0,001 WAVES. Удзельнік можа зрабіць стаўку, даслаўшы DataTransaction з ключом «price» і значэннем сваёй стаўкі.

Кошт новай стаўкі павінна быць вышэй, чым бягучая цана па гэтым ключы, і ўдзельнік павінен мець на рахунку як мінімум [новая стаўка + камісія] токенаў. Адрас удзельніка павінен быць запісаны ў поле "sender" у DataTransaction, а бягучая вышыня блока стаўкі павінна знаходзіцца ў межах перыяду аўкцыёну.

Калі па выніках аўкцыёну ўдзельнік прызначыў самы высокі кошт, ён можа адправіць ExchangeTransaction для аплаты адпаведнага лота па ўказаным кошце і валютнай пары.

let startHeight = 384120
let finishHeight = startHeight + 3000
let startPrice = 100000
 
#извлекаем из транзакции адрес отправителя
let this = extract(tx.sender)
let token = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
 
match tx {
case d : DataTransaction =>
  #проверяем, задана ли в стейте цена
  let currentPrice = if isDefined(getInteger(this, "price"))
 
                      #извлекаем цену из стейта
                      then extract(getInteger(this, "price"))
                      else startPrice
 
  #извлекаем цену из транзакции
  let newPrice = extract(getInteger(d.data, "price"))
  let priceIsBigger = newPrice > currentPrice
  let fee = 700000
  let hasMoney = wavesBalance(tx.sender) + fee >= newPrice
 
  #убеждаемся, что в текущей транзакции два поля и что отправитель совпадает с указанным в транзакции
  let correctFields = size(d.data) == 2 &&      
      d.sender == addressFromString(extract(getString(d.data,"sender")))
  startHeight <= height && height <= finishHeight && priceIsBigger && hasMoney && correctFields
case e : ExchangeTransaction =>
  let senderIsWinner = e.sender == addressFromString(extract(getString(this, "sender"))) #убеждаемся, что лот обменивает тот, кто его выиграл
  let correctAssetPair = e.sellOrder.assetPair.amountAsset == token && ! isDefined(e.sellOrder.assetPair.priceAsset)
  let correctAmount = e.amount == 1
  let correctPrice = e.price == extract(getInteger(this, "price"))
 
  height > finishHeight && senderIsWinner && correctAssetPair && correctAmount && correctPrice
case _ => false
}

2. Галандскі аўкцыён якая зніжаецца кошту

На галандскім аўкцыёне лот першапачаткова прапануецца па цане, якая перавышае тую, што пакупнік гатовы заплаціць. Кошт пакрокава зніжаецца да таго часу, пакуль адзін з удзельнікаў не пагодзіцца купіць лот па бягучай цане.

У гэтым прыкладзе мы выкарыстоўваем тыя ж канстанты, што і ў папярэднім, а таксама крок цаны пры паніжэнні delta. Скрыпт акаўнта правярае, ці сапраўды ўдзельнік з'яўляецца першым, хто зрабіў стаўку. У адваротным выпадку DataTransaction не прымаецца блокчейном.

let startHeight = 384120
let finishHeight = startHeight + 3000
let startPrice = 100000000
let delta = 100
 
#извлекаем из транзакции адрес отправителя
let this = extract(tx.sender)
let token = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
match tx {
case d : DataTransaction =>
  let currentPrice = startPrice - delta * (height - startHeight)
 
  #извлекаем из поступившей дата-транзакции поле "price"
  let newPrice = extract(getInteger(d.data, "price"))
 
  #убеждаемся, что в стейте текущего аккаунта не содержится поля "sender"
  let noBetsBefore = !isDefined(getInteger(this, "sender"))
  let fee = 700000
  let hasMoney = wavesBalance(tx.sender) + fee >= newPrice
 
  #убеждаемся, что в текущей транзакции только два поля
  let correctFields = size(d.data) == 2 && newPrice == currentPrice && d.sender == addressFromString(extract(getString(d.data, "sender")))
  startHeight <= height && height <= finishHeight && noBetsBefore && hasMoney && correctFields
case e : ExchangeTransaction =>
 
  #убеждаемся, что отправитель текущей транзакции указан в стейте аккаунта по ключу sender
  let senderIsWinner = e.sender == addressFromString(extract(getString(this, "sender")))
 
  #убеждаемся, что аmount ассета указан корректно, и что прайс-ассет - waves
  let correctAssetPair = e.sellOrder.assetPair.amountAsset == token && ! isDefined(e.sellOrder.assetPair.priceAsset)
  let correctAmount = e.amount == 1
  let correctPrice = e.price == extract(getInteger(this, "price"))
  height > finishHeight && senderIsWinner && correctAssetPair && correctAmount && correctPrice
case _ => false
}

3. Аўкцыён "all-pay"

"All-pay" - аўкцыён, усе ўдзельнікі якога аплачваюць стаўку, плацяць, незалежна ад таго, хто выйграе лот. Кожны новы ўдзельнік аплочвае стаўку, а выйграе лот удзельнік, які зрабіў максімальную стаўку.

У нашым прыкладзе кожны ўдзельнік аўкцыёну робіць стаўку праз DataTransaction з (key, value)* = ("winner", address),("price", price). Такая DataTransaction ухваляецца толькі ў выпадку, калі для гэтага ўдзельніка ўжо маецца TransferTransaction з яго подпісам і яго стаўка вышэй за ўсе папярэднія. Аўкцыён працягваецца да дасягнення вышыні endHeight.

let startHeight = 1000
let endHeight = 2000
let this = extract(tx.sender)
let token = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
match tx {
 case d: DataTransaction =>
   #извлекаем из поступившей дата-транзакции поле "price"
   let newPrice = extract(getInteger(d.data, "price"))
 
   #извлекаем из пруфов транзакции публичный ключ аккаунта
   let pk = d.proofs[1]
   let address = addressFromPublicKey(pk)
 
   #извлекаем транзакцию доказательство из пруфов поступившей дата транзакции
   let proofTx = extract(transactionById(d.proofs[2]))
   
   height > startHeight && height < endHeight
   && size(d.data) == 2
   #убеждаемся, что адрес победителя, извлеченный из текущей транзакции, совпадает с адресом, извлеченным из пруфов
   && extract(getString(d.data, "winner")) == toBase58String(address.bytes)
   && newPrice > extract(getInteger(this, "price"))
   #проверяем, что транзакция подписана
   && sigVerify(d.bodyBytes, d.proofs[0], d.proofs[1])
   #проверяем корректность транзакции, указанной в пруфах
   && match proofTx {
     case tr : TransferTransaction =>
       tr.sender == address &&
       tr.amount == newPrice
     case _ => false
   }
 case t: TransferTransaction =>
 sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)
 || (
   height > endHeight
   && extract(getString(this, "winner")) == toBase58String((addressFromRecipient(t.recipient)).bytes)
   && t.assetId == token
   && t.amount == 1
 )
 case _ => sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)
}

Страхаванне / Краўдфандынг

Разгледзім сітуацыю, калі трэба застрахаваць актывы карыстальнікаў ад фінансавых страт. Напрыклад, карыстач жадае атрымаць гарантыю, што ў выпадку абясцэньвання токена ён зможа вярнуць поўную суму, выплачаную за гэтыя токены, і гатовы заплаціць разумную суму страхоўкі.

Для рэалізацыі гэтага трэба выпусціць "страхавыя токены". Затым на рахунак страхавальніка усталёўваецца скрыпт, які дазваляе выконваць толькі тыя ExchangeTransactions, якія задавальняюць вызначаным умовам.

Каб прадухіліць падвойнае марнаванне, трэба запытаць у карыстача своечасовую адпраўку DataTransaction на акаўнт страхавальніка з (key, value) = (purchaseTransactionId, sellOrderId) і забараніць адпраўку DataTransactions з ужо выкарыстаным ключом.

Таму пруфы карыстальніка павінны змяшчаць ID транзакцыі пакупкі страхавога токена. Валютная пара павінна быць такой жа, як і ў транзакцыі пакупкі. Кошт таксама павінен быць роўны той, што зафіксаваная пры куплі, за вылікам кошту страхоўкі.

Мяркуецца, што пасля страхавы рахунак выкупляе страхавыя токены ў карыстача па кошце не ніжэй той, па якой ён іх набыў: страхавы рахунак стварае ExchangeTransaction, карыстач падпісвае ордэр (калі транзакцыя складзена карэктна), страхавы рахунак адпраўляе другі ордэр. .

Калі пакупкі не адбываецца, карыстальнік можа стварыць ExchangeTransaction у адпаведнасці з правіламі, апісанымі ў скрыпце, і адправіць транзакцыю ў блокчейн. Так карыстач можа вярнуць грошы, выдаткаваныя на куплю застрахаваных токенаў.

let insuranceToken = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
 
#извлекаем из транзакции адрес отправителя
let this = extract(tx.sender)
let freezePeriod = 150000
let insurancePrice = 10000
match tx {
 
 #убеждаемся, что, если поступила дата-транзакция, то у нее ровно одно поле и в стейте еще нет такого ключа
 case d : DataTransaction => size(d.data) == 1 && !isDefined(getBinary(this, d.data[0].key))
 case e : ExchangeTransaction =>
 
   #если у транзакции нет седьмого пруфа, проверяем корректность подписи
   if !isDefined(e.proofs[7]) then
     sigVerify(e.bodyBytes, e.proofs[0], e.senderPublicKey)
   else
     #если у транзакции есть седьмой пруф, извлекаем из него транзакцию и узнаём её высоту
     let purchaseTx = transactionById(e.proofs[7])
     let purchaseTxHeight = extract(transactionHeightById(e.proofs[7]))
    
     #обрабатываем транзакцию из пруфа
     match purchaseTx {
       case purchase : ExchangeTransaction =>
         let correctSender = purchase.sender == e.sellOrder.sender
         let correctAssetPair = e.sellOrder.assetPair.amountAsset == insuranceToken &&
                                purchase.sellOrder.assetPair.amountAsset == insuranceToken &&
                                e.sellOrder.assetPair.priceAsset == purchase.sellOrder.assetPair.priceAsset
         let correctPrice = e.price == purchase.price - insurancePrice && e.amount == purchase.amount
         let correctHeight = height > purchaseTxHeight + freezePeriod
 
         #убеждаемся, что в транзакции-пруфе указан верный ID текущей транзакции
         let correctProof = extract(getBinary(this, toBase58String(purchase.id))) == e.sellOrder.id
         correctSender && correctAssetPair && correctPrice && correctHeight && correctProof
     case _ => false
   }
 case _ => sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)
}

Страхавы токен можна зрабіць смарт-асэтам - напрыклад, каб забараніць яго перадачу трэцім асобам.

Гэтая схема можа быць рэалізавана і для токенаў краўдфандынгу, якія вяртаюцца ўладальнікам, калі неабходная сума не была сабрана.

Падаткі з транзакцый

Смарт-кантракты дастасавальныя і ў выпадках, калі трэба збіраць падатак з кожнай транзакцыі з некалькімі тыпамі актываў. Гэта можна зрабіць праз новы асет з усталяваным спонсарствам для транзакцый са смарт-асэтамі:

1. Выпускаем FeeCoin, які будзе адпраўлены карыстальнікам па фіксаваным кошце: 0,01 WAVES = 0,001 FeeCoin.

2. Задаем спонсарства для FeeCoin і курс абмену: 0,001 WAVES = 0,001 FeeCoin.

3. Задаем наступны скрыпт для смарт-асета:

let feeAssetId = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
let taxDivisor = 10
 
match tx {
  case t: TransferTransaction =>
    t.feeAssetId == feeAssetId && t.fee == t.amount / taxDivisor
  case e: ExchangeTransaction | MassTransferTransaction => false
  case _ => true
}

Цяпер кожны раз, калі хто-небудзь перакладае N смарт-асэтаў, ён дасць вам FeeCoin у суме N / taxDivisor (які можа быць набыты ў вас па 10 * N / taxDivisor WAVES), а вы аддасце майнер N / taxDivisor WAVES. У выніку ваш прыбытак (падатак) складзе 9*N / taxDivisor WAVES.

Таксама можна ажыццяўляць падаткаабкладанне з дапамогай скрыпту смарт-асета і MassTransferTransaction:

let taxDivisor = 10
 
match tx {
  case t : MassTransferTransaction =>
    let twoTransfers = size(t.transfers) == 2
    let issuerIsRecipient = t.transfers[0].recipient == addressFromString("3MgkTXzD72BTfYpd9UW42wdqTVg8HqnXEfc")
    let taxesPaid = t.transfers[0].amount >= t.transfers[1].amount / taxDivisor
    twoTransfers && issuerIsRecipient && taxesPaid
  case _ => false
}

Кэшбэк і праграмы лаяльнасці

Кэшбэк - тып праграмы лаяльнасці, у якім пакупніку вяртаецца частка сумы, выдаткаванай на тавар або паслугу.

Пры рэалізацыі гэтага кейса з дапамогай смарт-акаўнта мы павінны праверыць пруфы гэтак жа, як рабілі гэта ў кейсе страхавання. Для прадухілення падвойнага марнавання перад атрыманнем кэшбэка карыстач павінен адправіць DataTransaction з (key, value) = (purchaseTransactionId, cashbackTransactionId).

Таксама мы павінны ўсталяваць забарону на ўжо існыя ключы з дапамогай DataTransaction. cashbackDivisor - адзінка, падзеленая на дзель кэшбэка. Г.зн. у выпадку, калі доля кэшбэка роўная 0.1, то cashbackDivisor 1 / 0.1 = 10.

let cashbackToken = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
 
#извлекаем из транзакции адрес отправителя
let this = extract(tx.sender)
let cashbackDivisor = 10
match tx {
 
 #убеждаемся, что, если поступила дата-транзакция, то у нее ровно одно поле и в стейте еще нет такого ключа
 case d : DataTransaction => size(d.data) == 1 && !isDefined(getBinary(this, d.data[0].key))
 case e : TransferTransaction =>
 
   #если у транзакции нет седьмого пруфа, проверяем корректность подписи
   if !isDefined(e.proofs[7]) then
     sigVerify(e.bodyBytes, e.proofs[0], e.senderPublicKey)
   else
 
     #если у транзакции есть седьмой пруф, извлекаем из него транзакцию и узнаём её высоту
     let purchaseTx = transactionById(e.proofs[7])
     let purchaseTxHeight = extract(transactionHeightById(e.proofs[7]))
    
     #обрабатываем транзакцию из пруфа
     match purchaseTx {
       case purchase : TransferTransaction =>
         let correctSender = purchase.sender == e.sender
         let correctAsset = e.assetId == cashbackToken
         let correctPrice = e.amount == purchase.amount / cashbackDivisor
 
         #убеждаемся, что в транзакции-пруфе указан верный ID текущей транзакции
         let correctProof = extract(getBinary(this, toBase58String(purchase.id))) == e.id
         correctSender && correctAsset && correctPrice && correctProof
     case _ => false
   }
 case _ => sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)
}

Атамарны своп

Атамарны своп дазваляе карыстальнікам абменьвацца асэтамі без дапамогі біржы. Пры атамарным свопе ад абодвух удзельнікаў здзелкі патрабуецца яе пацвярджэнне на працягу пэўнага прамежку часу.

Калі хаця б адзін з удзельнікаў не дае карэктнага пацверджання транзакцыі на працягу адведзенага на здзелку часу, транзакцыя адмяняецца і абмену не адбываецца.

У нашым прыкладзе мы будзем выкарыстоўваць такі скрыпт смарт-акаўнта:

let Bob = Address(base58'3NBVqYXrapgJP9atQccdBPAgJPwHDKkh6A8')
let Alice = Address(base58'3PNX6XwMeEXaaP1rf5MCk8weYeF7z2vJZBg')
 
let beforeHeight = 100000
 
let secret = base58'BN6RTYGWcwektQfSFzH8raYo9awaLgQ7pLyWLQY4S4F5'
match tx {
  case t: TransferTransaction =>
    let txToBob = t.recipient == Bob && sha256(t.proofs[0]) == secret && 20 + beforeHeight >= height
    let backToAliceAfterHeight = height >= 21 + beforeHeight && t.recipient == Alice
    txToBob || backToAliceAfterHeight
  case _ => false
}

У наступным артыкуле мы разгледзім прымяненне смарт-акаўнтаў у фінансавых інструментах - такіх, як апцыёны, ф'ючэрсы і вэксалі.

Крыніца: habr.com

Дадаць каментар