Community

Application of Waves Smart Accounts: from Auctions to Customer Loyalty Schemes

Waves | 03.06| 546

Blockchain is often associated solely with cryptocurrencies, but application areas for DLT are far broader. One of the most promising directions for blockchain application is smart contracts: code that is executed automatically and that doesn’t require trust between the parties who agree it.

RIDE: a smart contract language

Waves has developed a special language for smart contracts, RIDE. Complete documentation is available here.

A RIDE-based contract is a predicate and returns either “true” or “false”. Respectively, a transaction is either added to the blockchain or rejected. A smart contract fully guarantees the execution of conditions set out in it. At this point, the option of generating transactions from a RIDE contract is not available.

Currently, Waves offers two types of smart contracts, smart accounts and smart assets. A smart account is a user’s regular account, for which a script can be set that controls all transactions. A Smart Account’s script could look, for instance, like this:

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

tx is a processed transaction that we allow, using the pattern matching tool, unless it is a TransferTransaction. In RIDE, pattern matching is used for checking the type of transaction. All existing [transaction types] (https://docs.wavesplatform.com/en/technical-details/transactions-structure.html) can be processed in a Smart Account’s script.

A script can also use variables, “if-then-else” statements and other methods of proper verification of conditions. To facilitate contracts’ provable termination and complexity (value) that can be predicted prior to the contract’s execution, RIDE doesn’t have cycles or jump operators.

Among Waves accounts’ other features is the state. An indefinite number of pairs (key, value) can be added to an account’s state using DataTransactions. Subsequently, that information can be processed either over REST API, or directly in the smart contract.

Every transaction can contain an array of proofs, in which a participant’s signature, a transaction ID, etc, might be entered.

Using RIDE in IDE https://ide.wavesplatform.com/ enables display of a smart contract’s compiled view (as long as it can be compiled), creation of new accounts and setting scripts for them, as well as sending transactions from the command prompt.

For a full cycle that includes creating an account, adding a smart contract to it and sending a transaction, a library for REST API can be used (such as C#, C, Java, JavaScript, Python, Rust or Elixir). To start using IDE, just hit the NEW button.

Smart contracts’ possible applications range from banning transactions to selected addresses (blacklisting) to creating complex dApps.

Now we’ll consider several specific examples of smart contract application in business, such as auctions, insurance and customer loyalty programs.

Auctions

Transparency is one of the conditions necessary for running a successful auction: participants have to be sure that bid manipulation is impossible. This can be achieved thanks to a blockchain on which immutable data for all bids and the times when they were made is available to all participants.

On Waves’ blockchain, bids can be recorded in the auction’s account state using DataTransactions.

In addition, the auction’s start and end times can be set using block numbers, as Waves’ block generation frequency is roughly 60 seconds.

1. English (ascending price) auction

In an English auction, buyers place bids, competing with each other to set the highest price. Every higher bid displaces an earlier bid. If no competing bidder challenges the current bid within a given time frame, the bidder who placed it becomes the winner.

In a variation of this, the seller sets a minimum price for an item. If no buyer places a higher bid than this minimum price, the item remains unsold.

In this example, we show a smart account specifically created for an auction. The auction’s time frame equals 3,000 blocks and the initial price equals 0.001 WAVES. A buyer can place a bid by sending a DataTransaction with the key “price” and the value of their bid.

The price of any new bid has to be higher than the current price, and the bidder has to have at least [new price + fee] tokens in their balance. The bidder’s address is added to the “sender” field in a DataTransaction and the current height of the block has to be within the auction’s time frame.

If a buyer places the highest bid, they pay for the item with an ExchangeTransaction specifying the price and asset pair.

let startHeight = 384120
let finishHeight = startHeight + 3000
let startPrice = 100000
#extracting the sender’s address from the transaction
let this = extract(tx.sender)
let token = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
match tx {
case d : DataTransaction =>
#checking if the price is defined in the state
let currentPrice = if isDefined(getInteger(this, "price"))
#extracting the price from the state
then extract(getInteger(this, "price"))
else startPrice
#extracting the price from the transaction
let newPrice = extract(getInteger(d.data, "price"))
let priceIsBigger = newPrice > currentPrice
let fee = 700000
let hasMoney = wavesBalance(tx.sender) + fee >= newPrice
#making sure there are two fields in the current transaction and the sender is the same as the same as stated in the transaction
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"))) #making sure that the item is exchanged by the actual winner
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. Dutch (descending-price) auction

In a Dutch auction, an item is initially offered at a price exceeding the amount the buyer expects to pay. Then the price is gradually lowered until a bidder accepts the current price.

In this example, we use the same tools as in the previous one, also adding a delta for a step price. The account’s script checks if a participant has actually placed the first bid for the current price. Otherwise, the blockchain doesn’t accept the DataTransaction.

let startHeight = 384120
let finishHeight = startHeight + 3000
let startPrice = 100000000
let delta = 100
#extracting the sender’s address from the transaction
let this = extract(tx.sender)
let token = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
match tx {
case d : DataTransaction =>
let currentPrice = startPrice - delta * (height - startHeight)
#extracting from the received DataTransaction the “price” field
let newPrice = extract(getInteger(d.data, "price"))
#making sure that there is no “sender” field in the current account’s state
let noBetsBefore = !isDefined(getInteger(this, "sender"))
let fee = 700000
let hasMoney = wavesBalance(tx.sender) + fee >= newPrice
#making sure there are two fields in the current transaction
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 =>
#making sure that the current transaction’s sender is indicated in the account’s state by the “sender” key
let senderIsWinner = e.sender == addressFromString(extract(getString(this, "sender")))
#making sure that the аmount asset is stated correctly and the price asset is 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 auction

An all-pay auction is an auction in which all bidders pay, regardless of who wins the item. Every new bidder has to pay their bid, and the highest bidder wins the item, as in a conventional auction.

In our example, every participant places a bid by sending a DataTransaction with (key, value)* = (“winner”, address),(“price”, price). A bidder’s Data Transaction only gets approved if a TransferTransaction signed by them already exists and the bid is higher than the earlier bids. The auction runs until endHeight is reached.

let startHeight = 1000
let endHeight = 2000
let this = extract(tx.sender)
let token = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
match tx {
case d: DataTransaction =>
#extracting the field “price” from the received transaction
let newPrice = extract(getInteger(d.data, "price"))

#extracting the account’s public key from the transaction’s proofs
let pk = d.proofs[1]
let address = addressFromPublicKey(pk)
#extracting proofTx from the proofs of the received DataTransaction
let proofTx = extract(transactionById(d.proofs[2]))
height > startHeight && height < endHeight
&& size(d.data) == 2
#making sure the winner, extracted from the current transaction is the same as the address extracted from the proofs
&& extract(getString(d.data, "winner")) == toBase58String(address.bytes)
&& newPrice > extract(getInteger(this, "price"))
#verifying that the transaction was signed
&& sigVerify(d.bodyBytes, d.proofs[0], d.proofs[1])
#checking the correctness of the transaction, stated in the proofs
&& 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)
}

Insurance / Crowdfunding

Let’s consider a situation where we need to protect users’ assets from financial loss. For instance, a user may want to ensure that they will be able to recover the entire amount paid for tokens in case of the token’s devaluation, and are prepared to pay a reasonable insurance premium.

For that purpose, “insurance tokens” will be issued. A script to the insured’s account is applied, allowing only ExchangeTransactions that meet certain conditions.

To prevent double spending, the insured user initially needs to send a DataTransaction to the insurer’s account with (key, value) = (purchaseTransactionId, sellOrderId) and prohibit sending DataTransactions with the same keys.

Consequently, the user’s proofs have to contain the ID of the transaction for the purchase of the insurance token. The asset pair has to be the same as in the purchase transaction. The price has to be equal to the token purchase price, with the insurance premium price deducted.

It is implied that the insurer’s account will later buy the insurance tokens from the user at a price no lower than the purchase price. The insurer’s account will create an ExchangeTransaction, and the insured user will sign an order (if the transaction is correct). The insurer’s account will sign the second order and the entire transaction, and send it to the blockchain.

If no purchase takes place, the user can create an ExchangeTransaction under conditions set in the script and send the transaction to the blockchain. That way, the user will recover the funds spent for the purchase of insured tokens.

let insuranceToken = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
#extracting the sender’s address from the transaction
let this = extract(tx.sender)
let freezePeriod = 150000
let insurancePrice = 10000
match tx {
#verifying that if a DataTransaction has arrived, it has just one field and there is no second key in the state
case d : DataTransaction => size(d.data) == 1 && !isDefined(getBinary(this, d.data[0].key))
case e : ExchangeTransaction =>
#if a transaction doesn’t have the seventh proof, we’re verifying the signature
if !isDefined(e.proofs[7]) then
sigVerify(e.bodyBytes, e.proofs[0], e.senderPublicKey)
else
#if the transaction has the seventh proof, we’re extracting from it the transaction and discovering its height
let purchaseTx = transactionById(e.proofs[7])
let purchaseTxHeight = extract(transactionHeightById(e.proofs[7]))
#processing the transaction from the proof
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
#verifying that the current transaction’s ID is stated correctly in the proof transaction
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)
}

The insurance token can be made a smart asset — for example, to prohibit its transfer to third parties.

This scheme can also be used for crowdfunding tokens, which are to be returned to the holders if a required sum of money has not been collected.

Taxation

Smart contracts are also applicable in a situation when every transaction for several types of asset needs to be taxed. This can be implemented with a new asset, with added sponsorship for smart asset transactions:

  1. We issue FeeCoin, which will be sent to users at a fixed price: 0.01 WAVES = 0.001 FeeCoin.
  2. We set sponsorship for FeeCoin and specify the exchange rate: 0.001 WAVES = 0.001 FeeCoin.
  3. We set the following script for the smart asset:
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
}

Now, every time someone transfers N smart assets, they will send you N / taxDivisor FeeCoins (which could be bought from you at 10 *N / taxDivisor WAVES) and will send N / taxDivisor WAVES to the miner. As a result, you will collect a profit (tax) of 9 *N / taxDivisor WAVES.

Taxation could also be implemented with a smart asset script and 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
}

Cashback and loyalty schemes

Cashback is a type of a customer loyalty scheme in which a percentage of funds spent on goods or services is paid back to the customer.

In the implementation of this case with a smart account, we need to check proofs in the same way as in the insurance use case. To prevent double spending, before collecting a cashback, a user has to send a DataTransaction with (key, value) = (purchaseTransactionId, cashbackTransactionId).

Also, we need to prohibit setting DataTransactions with the same keys. cashbackDivisor is a reverse number for a cashback share. If the cashback share is 0.1, cashbackDivisor will be 1 / 0.1 = 10.

let cashbackToken = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
#extracting the sender’s address from the transaction
let this = extract(tx.sender)
let cashbackDivisor = 10
match tx {
#verifying that if a DataTransaction has arrived, it has just one field and that key is not yet in the state
case d : DataTransaction => size(d.data) == 1 && !isDefined(getBinary(this, d.data[0].key))
case e : TransferTransaction =>
#if the transaction has the seventh proof, we’re verifying the signature
if !isDefined(e.proofs[7]) then
sigVerify(e.bodyBytes, e.proofs[0], e.senderPublicKey)
else
#if the transaction has the seventh proof, we’re extracting the transaction from it and finding out its height
let purchaseTx = transactionById(e.proofs[7])
let purchaseTxHeight = extract(transactionHeightById(e.proofs[7]))
#processing the transaction from the proof
match purchaseTx {
case purchase : TransferTransaction =>
let correctSender = purchase.sender == e.sender
let correctAsset = e.assetId == cashbackToken
let correctPrice = e.amount == purchase.amount / cashbackDivisor
#verifying that the current transaction’s ID is correctly stated in the proof transaction
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)
}

Atomic swap

An atomic swap enables users to swap assets without using exchanges. In an atomic swap, approval from both participants of the transaction is required within a specified time frame.

If at least one of the participants fails to approve the transaction within the specified time frame, the transaction is cancelled and funds are not exchanged.

In our example, we use a smart account script:

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
}

In our next article, we’ll look at the application of smart accounts for financial tools, such as options, futures and bills of exchange.

Join Waves Community
Read Waves News channel
Follow Waves Twitter
Subscribe to Waves Subreddit


Application of Waves Smart Accounts: from Auctions to Customer Loyalty Schemes was originally published in Waves Platform on Medium, where people are continuing the conversation by highlighting and responding to this story.

Comment 0

delete

Are you sure you want to delete this post?