Застосування смарт-акаунтів 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, а поточна висота блоку ставки повинна знаходитись у межах періоду аукціону.

Якщо за підсумками аукціону учасник призначив найвищу ціну, він може відправити Exchange Transaction для оплати відповідного лота за вказаною ціною та парою.

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)
}

Страхування / Краудфандінг

Розглянемо ситуацію, коли необхідно застрахувати активи користувачів від фінансових втрат. Наприклад, користувач хоче отримати гарантію, що у разі знецінення токена він зможе повернути повну суму, сплачену за ці токени, та готовий заплатити розумну суму страховки.

Для цього потрібно випустити «страхові токени». Потім на обліковий запис страхувальника встановлюється скрипт, що дозволяє виконувати лише ті Exchange Transactions, які задовольняють певним умовам.

Щоб запобігти подвійній витраті, потрібно запросити у користувача завчасне відправлення DataTransaction на обліковий запис страхувальника з (key, value) = (purchaseTransactionId, sellOrderId) і заборонити відправку DataTransactions з уже використаним ключем.

Тому пруфи користувача повинні містити ID транзакції купівлі страхового токена. Валютна пара має бути такою самою, як і в транзакції покупки. Вартість також повинна дорівнювати тій, що зафіксована при покупці, за вирахуванням ціни страховки.

Мається на увазі, що згодом страховий аккаунт викуповує страхові токени у користувача за ціною не нижче тієї, за якою він їх придбав: страховий аккаунт створює Exchange Transaction, користувач підписує ордер (якщо транзакція складена коректно), страховий аккаунт підписує другий ордер. .

Якщо покупки не відбувається, користувач може створити Exchange Transaction відповідно до правил, описаних у скрипті, і відправити транзакцію до блокчейну. Так користувач може повернути гроші, витрачені на купівлю застрахованих токенів.

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

Додати коментар або відгук