Application of Waves Smart ...
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 languageWaves 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 => falsecase _ => 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.AuctionsTransparency 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) auctionIn 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 = 384120let finishHeight = startHeight + 3000let startPrice = 100000#extracting the sender’s address from the transactionlet this = extract(tx.sender)let token = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'match tx {case d : DataTransaction =>#checking if the price is defined in the statelet currentPrice = if isDefined(getInteger(this, "price"))#extracting the price from the statethen extract(getInteger(this, "price"))else startPrice#extracting the price from the transactionlet newPrice = extract(getInteger(d.data, "price"))let priceIsBigger = newPrice > currentPricelet fee = 700000let 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 transactionlet correctFields = size(d.data) == 2 &&d.sender == addressFromString(extract(getString(d.data,"sender")))startHeight <= height && height <= finishHeight && priceIsBigger && hasMoney && correctFieldscase e : ExchangeTransaction =>let senderIsWinner = e.sender == addressFromString(extract(getString(this, "sender"))) #making sure that the item is exchanged by the actual winnerlet correctAssetPair = e.sellOrder.assetPair.amountAsset == token && ! isDefined(e.sellOrder.assetPair.priceAsset)let correctAmount = e.amount == 1let correctPrice = e.price == extract(getInteger(this, "price"))height > finishHeight && senderIsWinner && correctAssetPair && correctAmount && correctPricecase _ => false}2. Dutch (descending-price) auctionIn 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 = 384120let finishHeight = startHeight + 3000let startPrice = 100000000let delta = 100#extracting the sender’s address from the transactionlet 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” fieldlet newPrice = extract(getInteger(d.data, "price"))#making sure that there is no “sender” field in the current account’s statelet noBetsBefore = !isDefined(getInteger(this, "sender"))let fee = 700000let hasMoney = wavesBalance(tx.sender) + fee >= newPrice#making sure there are two fields in the current transactionlet correctFields = size(d.data) == 2 && newPrice == currentPrice && d.sender == addressFromString(extract(getString(d.data, "sender")))startHeight <= height && height <= finishHeight && noBetsBefore && hasMoney && correctFieldscase e : ExchangeTransaction =>#making sure that the current transaction’s sender is indicated in the account’s state by the “sender” keylet senderIsWinner = e.sender == addressFromString(extract(getString(this, "sender")))#making sure that the аmount asset is stated correctly and the price asset is waveslet correctAssetPair = e.sellOrder.assetPair.amountAsset == token && ! isDefined(e.sellOrder.assetPair.priceAsset)let correctAmount = e.amount == 1let correctPrice = e.price == extract(getInteger(this, "price"))height > finishHeight && senderIsWinner && correctAssetPair && correctAmount && correctPricecase _ => false}3. All-pay auctionAn 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 = 1000let endHeight = 2000let this = extract(tx.sender)let token = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'match tx {case d: DataTransaction =>#extracting the field “price” from the received transactionlet newPrice = extract(getInteger(d.data, "price"))#extracting the account’s public key from the transaction’s proofslet pk = d.proofs[1]let address = addressFromPublicKey(pk)#extracting proofTx from the proofs of the received DataTransactionlet 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 == newPricecase _ => 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 / CrowdfundingLet’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 transactionlet this = extract(tx.sender)let freezePeriod = 150000let insurancePrice = 10000match tx {#verifying that if a DataTransaction has arrived, it has just one field and there is no second key in the statecase 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 signatureif !isDefined(e.proofs[7]) thensigVerify(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 heightlet purchaseTx = transactionById(e.proofs[7])let purchaseTxHeight = extract(transactionHeightById(e.proofs[7]))#processing the transaction from the proofmatch purchaseTx {case purchase : ExchangeTransaction =>let correctSender = purchase.sender == e.sellOrder.senderlet correctAssetPair = e.sellOrder.assetPair.amountAsset == insuranceToken &&purchase.sellOrder.assetPair.amountAsset == insuranceToken &&e.sellOrder.assetPair.priceAsset == purchase.sellOrder.assetPair.priceAssetlet correctPrice = e.price == purchase.price - insurancePrice && e.amount == purchase.amountlet correctHeight = height > purchaseTxHeight + freezePeriod#verifying that the current transaction’s ID is stated correctly in the proof transactionlet correctProof = extract(getBinary(this, toBase58String(purchase.id))) == e.sellOrder.idcorrectSender && correctAssetPair && correctPrice && correctHeight && correctProofcase _ => 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.TaxationSmart 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:We issue FeeCoin, which will be sent to users at a fixed price: 0.01 WAVES = 0.001 FeeCoin.We set sponsorship for FeeCoin and specify the exchange rate: 0.001 WAVES = 0.001 FeeCoin.We set the following script for the smart asset:let feeAssetId = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf’let taxDivisor = 10match tx {case t: TransferTransaction =>t.feeAssetId == feeAssetId && t.fee == t.amount / taxDivisorcase e: ExchangeTransaction | MassTransferTransaction => falsecase _ => 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 = 10match tx {case t : MassTransferTransaction =>let twoTransfers = size(t.transfers) == 2let issuerIsRecipient = t.transfers[0].recipient == addressFromString("3MgkTXzD72BTfYpd9UW42wdqTVg8HqnXEfc")let taxesPaid = t.transfers[0].amount >= t.transfers[1].amount / taxDivisortwoTransfers && issuerIsRecipient && taxesPaidcase _ => false}Cashback and loyalty schemesCashback 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 transactionlet this = extract(tx.sender)let cashbackDivisor = 10match tx {#verifying that if a DataTransaction has arrived, it has just one field and that key is not yet in the statecase 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 signatureif !isDefined(e.proofs[7]) thensigVerify(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 heightlet purchaseTx = transactionById(e.proofs[7])let purchaseTxHeight = extract(transactionHeightById(e.proofs[7]))#processing the transaction from the proofmatch purchaseTx {case purchase : TransferTransaction =>let correctSender = purchase.sender == e.senderlet correctAsset = e.assetId == cashbackTokenlet correctPrice = e.amount == purchase.amount / cashbackDivisor#verifying that the current transaction’s ID is correctly stated in the proof transactionlet correctProof = extract(getBinary(this, toBase58String(purchase.id))) == e.idcorrectSender && correctAsset && correctPrice && correctProofcase _ => false}case _ => sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)}Atomic swapAn 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 = 100000let secret = base58'BN6RTYGWcwektQfSFzH8raYo9awaLgQ7pLyWLQY4S4F5'match tx {case t: TransferTransaction =>let txToBob = t.recipient == Bob && sha256(t.proofs[0]) == secret && 20 + beforeHeight >= heightlet backToAliceAfterHeight = height >= 21 + beforeHeight && t.recipient == AlicetxToBob || backToAliceAfterHeightcase _ => 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 CommunityRead Waves News channelFollow Waves TwitterSubscribe to Waves SubredditApplication 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.
* Written questions can not be edited.
* The questions will be answered directly by the token team.