Application of Waves Smart Accounts and Smart Assets for Financial Instruments
In the previous article, we considered several use cases for smart accounts in business, including auctions and customer loyalty programs.
Today, we will discuss how smart accounts and smart assets could improve the transparency and reliability of financial instruments such as options, futures and bills of exchange.
Options
An option is a contract that gives the buyer the right but not the obligation to buy an asset at a specified price, prior to or on a certain date.
An option can be implemented in the following way. We use a smart asset for an option and a smart account for a participant who takes the role of an exchange and issues options (“exchange” participant). The “exchange” participant pledges to sell a certain amount of an asset at sellPrice between block heights expirationStart and expirationEnd.
In the smart asset’s code, we just need to check that it trades only between the set heights, while responsibility for observing the rules will be delegated to the “exchange” participant’s code.
Smart asset code:
let expirationStart = 100000let expirationEnd = 101440match tx {case some : ExchangeTransaction | TransferTransaction =>height > expirationStart && height <= expirationEndcase _ => false}
We’ll assume that the following actions will take place: the “exchange” participant sells options for buying a certain asset, and the other participants can send or trade these options. To execute their right to buy the asset, a potential buyer has to transfer a corresponding number of options to the buyer’s account — i.e. to the account of the “exchange” participant.
Subsequently, the buyer adds the information on the transfer to the account’s state, and only after that can an ExchangeTransaction with specified conditions for buying/selling be accepted by the blockchain.
In the smart account code, we need to check that any ExchangeTransaction going through it for the final deal corresponds to the set conditions and the buyer purchases only the same number of units they sent to the account of the “exchange” participant.
The buyer needs to send a correct DataTransaction with the transfer, so that the “exchange” participant can avoid double spending. In this DataTransaction, the buyer adds to the key, which corresponds to their address, a value that is equal to the number of options transferred to the account of the “exchange” participant, i.e. equal to the number of units of the asset they can buy.
Smart account code:
#the account owner pledges to sell a certain number of the asset's units#at sellPrice between block heights expirationStart and expirationEnd
let expirationStart = 100000let expirationEnd = 101440let sellPrice = 10000let amountAsset = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'let priceAsset = base58'9jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
#option asset IDlet optionsAsset = base58'7jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
#extracting the sender's address from the transactionlet this = tx.sendermatch tx {case dataTx : DataTransaction =>
#extracting the number of units from the DataTransaction by the key (user ID)let units = extract(getInteger(dataTx.data, dataTx.data[0].key))
#extracting the transaction from the prooflet e = transactionById(o.proofs[7])match e {case exchangeTx : ExchangeTransaction =>let correctSeller = exchangeTx.sellOrder.sender == Boblet correctBuyer = exchangeTx.buyOrder.sender == Alicelet correctAssetPair = (exchangeTx.sellOrder.assetPair.amountAsset == amountAsset && exchangeTx.sellOrder.amount == sellAmount && exchangeTx.sellOrder.price == sellPrice)|| (exchangeTx.buyOrder.assetPair.amountAsset == amountAsset && exchangeTx.buyOrder.amount == sellAmount && exchangeTx.buyOrder.price == sellPrice)
#making sure that the order’s ID is correctly stated in the proof transactionlet correctProof = extract(getBinary(extract(tx.sender), toBase58String(exchangeTx.sellOrder.id))) == o.id|| extract(getBinary(extract(tx.sender), toBase58String(exchangeTx.buyOrder.id))) == o.idcorrectAssetPair && correctProof && correctSeller && correctBuyercase _ => false} &&(height >= expiration) &&#verifying the signature(sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)) &&(tx.sender == Bob)case _ => false}
Futures contracts
Unlike options, futures contracts (futures) represent the buyer’s obligation to buy an asset at a predetermined price at a specified time in the future, not merely the opportunity to do so.
Overall, the implementation of futures is similar to that of options. Here, a smart asset acts as the futures.
We also need to check that both buyer and seller sign a transaction order. A futures contract is an obligation that has to be executed in any case. So, if the buyer or seller repudiate their obligations, any participant in the network can send a transaction and thereby execute the contract.
The smart asset’s script controls all TransferTransactions and ExchangeTransactions of the futures assets, approving them only if the buyer has created an order for a future purchase of the future assets from the “exchange” participant.
This order has to be valid and meet the conditions under which the futures contract was issued. To check the order, all of its fields could be recorded to the buyer’s account’s state along with the byte code of the signed order, after which validation by a third party could be carried out.
Currently, RIDE doesn’t contain a native feature for parsing transaction bytes, but has all the necessary tools for its implementation. Developers could therefore try to implement that feature by themselves.
Multi-signature accounts / Escrow
A multi-signature account allows several users to jointly manage assets (for instance, transactions with assets could only be allowed if signed by three users out of four). To create multi-signature accounts with RIDE, we can use transaction proofs.
A multi-signature account could also be used for an escrow account, in which funds are held until transacting parties have satisfied their contractual obligations.
let alicePubKey = base58'5AzfA9UfpWVYiwFwvdr77k6LWupSTGLb14b24oVdEpMM'let bobPubKey = base58'2KwU4vzdgPmKyf7q354H9kSyX9NZjNiq4qbnH2wi2VDF'let cooperPubKey = base58'GbrUeGaBfmyFJjSQb9Z8uTCej5GzjXfRDVGJGrmgt5cD'
#checking who provided correct signatureslet aliceSigned = if(sigVerify(tx.bodyBytes, tx.proofs[0], alicePubKey)) then 1 else 0let bobSigned = if(sigVerify(tx.bodyBytes, tx.proofs[1], bobPubKey)) then 1 else 0let cooperSigned = if(sigVerify(tx.bodyBytes, tx.proofs[2], cooperPubKey)) then 1 else 0
#adding all correct signatures and checking their numberaliceSigned + bobSigned + cooperSigned >= 2
Token curated registry (TCR)
Many blockchain platforms face the problem of toxic assets. For instance, on Waves Platform, any address that has paid the issuance fee can create an asset.
One way to approach the toxic asset issue is with a token curated registry (TCR), generated by token holders.
To vote to add a specific token to the list, holders place a stake corresponding to their share in the total number of issued tokens. A token is added to the registry if the majority of its holders vote for it.
In our example, a user is allowed to add a token to the consideration list (during the “challenge” period) by the state key = asset_name solely if the current value of the count = 0.
Also, the user has to have an non-zero balance of the token in question in their wallet. Then, during the voting period, the user can vote for each asset in their wallet, but just once, rating them on a scale from 1 to 10. Users’ votes are represented by keys that look like this: user_address+assetID.
let asset = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'let addingStartHeight = 1000let votingStartHeight = 2000let votingEndHeight = 3000#extracting the sender’s address from the transactionlet this = extract(tx.sender)#extracting the address from the transaction’s prooflet address = addressFromPublicKey(tx.proofs[1])match tx {case t: DataTransaction =>if(height > addingStartHeight)then(if(height < votingStartHeight)then(
#adding#checking if this address has this assetlet hasTokens = assetBalance(address, asset) > 0size(t.data) == 1
#making sure this assets has not yet been added&& !isDefined(getInteger(this, toBase58String(asset)))
#checking if 0 has been added by the asset key&& extract(getInteger(t.data, toBase58String(asset))) == 0&& hasTokens)else(if(height < votingEndHeight)then(
#voting#finding out the current number of votes for this asset and the base numberlet currentAmount = extract(getInteger(this, toBase58String(asset)))let newAmount = extract(getInteger(t.data, toBase58String(asset)))let betString = toBase58String(address.bytes) + toBase58String(asset)#checking that this address has not yet voted for this assetlet noBetBefore = !isDefined(getInteger(this, betString))let isBetCorrect = extract(getInteger(t.data, betString)) > 0&& extract(getInteger(t.data, betString)) <= 10#checking if the voter has enough tokenslet hasTokens = assetBalance(address, asset) > 0
#checking that the transaction is defined correctlysize(t.data) == 2 && isDefined(getInteger(this, toBase58String(asset)))&& newAmount == currentAmount + 1&& noBetBefore && isBetCorrect && hasTokens)else false) && sigVerify(tx.bodyBytes, tx.proofs[0], tx.proofs[1]))else falsecase _ => false}
Subscription fee
In this example, we look at using smart accounts for executing a “subscription fee” — regular payments for goods or services in a set time frame.
If a user sends to a smart account (through transaction proofs) an ID TransferTransaction with a required amount of transferred funds, they could add to the account state: {key: address, value: true}.
This would mean that that the user confirms a subscription for goods or services. When the subscription period expires, any user in the network could change that value to false.
let subscriptionPeriod = 44000let signature = tx.proofs[0]let pk = tx.proofs[1]let requiredAmount = 100000#extracting the sender’s address from the transactionlet this = extract(tx.sender)match tx {case d: DataTransaction =>
#extracting the date of the most recent paymentlet lastPaymentHeight = extract(getInteger(this, d.data[0].key + "_lastPayment"))size(d.data) == 1 && d.data[0].value == "false" && lastPaymentHeight + subscriptionPeriod < height||(let address = d.data[0].key#extracting the transfer transaction by the ID indicated in the proofs.let ttx = transactionById(d.proofs[0])
size(d.data) == 2&& d.data[0].value == "true"&& d.data[1].key == address + "_lastPayment"&& match ttx {case purchase : TransferTransaction =>d.data[1].value == transactionHeightById(purchase.id)&& toBase58String(purchase.sender.bytes) == address&& purchase.amount == requiredAmount&& purchase.recipient == this
#making sure the asset is WAVES&& !isDefined(purchase.assetId)case _ => false})case _ => false}
Voting
Smart accounts could be used for implementing voting on the blockchain. One example is voting for the best ambassador’s report in an ambassador program. The account state is used as a platform to record votes for various options.
In this example, only those who have purchased “voting” tokens can vote. A user sends a DataTransaction with (key, value) = (purchaseTransactionId, buyTransactionId). Other values for this key are not allowed. Using their address and voting option, a DataEntry can be done only once. Voting is allowed only within a specified period.
let asset = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'let address = addressFromPublicKey(tx.proofs[1])let votingStartHeight = 2000let votingEndHeight = 3000
#extracting the sender’s address from the transactionlet this = extract(tx.sender)match tx {case t: DataTransaction =>(height > votingStartHeight && height < votingEndHeight) &&
#verifying that the signature of the transaction is correctsigVerify(tx.bodyBytes, tx.proofs[0], tx.proofs[1]) &&
#verifying that the user puts his vote next to his addressif (t.data[0].key == toBase58String(address.bytes))then (
#extracting the transaction for transfer of a voting token from the prooflet purchaseTx = transactionById(t.proofs[7])match purchaseTx {case purchase : TransferTransaction =>let correctSender = purchase.sender == t.senderlet correctAsset = purchase.assetId == assetlet correctPrice = purchase.amount == 1let correctProof = extract(getBinary(this, toBase58String(purchase.id))) == t.idcorrectSender && correctAsset && correctPrice && correctProofcase _ => false})elsesize(t.data) == 1 && !isDefined(getBinary(this, t.data[0].key))case _ => false}
Bill of exchange
A bill of exchange is a written order under which one party is obliged to pay a fixed sum of money to another party on demand or at a predetermined date.
In our example, a smart account is used, the expiration date of which corresponds to the payment date of the bill of exchange.
let expiration = 100000let amount = 10let asset = base58'9jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'let Bob = Address(base58'3NBVqYXrapgJP9atQccdBPAgJPwHDKkh6A8')let Alice = Address(base58'3PNX6XwMeEXaaP1rf5MCk8weYeF7z2vJZBg')match tx {case t: TransferTransaction =>(t.assetId == asset)&&(t.amount == amount)&&(t.sender == Bob)&&(t.recipient == Alice)&&(sigVerify(t.bodyBytes, t.proofs[0], t.senderPublicKey))&&(height >= expiration)case _ => false}
Deposit
A deposit is the placement of funds at a bank under certain conditions (duration, interest rate). In our example, a smart account acts as a bank. After reaching a certain block height, which corresponds to the period of the deposit, a user can have their cash repaid with interest. In the script, a block height is set, after which the user can withdraw the deposited amount (finalHeight).
heightUnit is the number of blocks in one period unit (such as a month or a year). Initially, we check the presence of a record with the pair (key, value) = (initialTransferTransaction, futureDataTransaction). Then, the user has to send a TransferTransaction with correct information on the deposited sum and interest collected over the deposit period. This information is checked against the initial TransferTransaction, which is contained in the current proof of the TransferTransaction. depositDivisor is the inverse of the deposit share (if the deposit is placed at a 10% interest, the share is 0.1 and depositDevisor equals 1/0.1 = 10).
let this = extract(tx.sender)let depositDivisor = 10let heightUnit = 1000let finalHeight = 100000match tx {case e : TransferTransaction =>#extracting the height of the transaction by the transaction ID, which is in the seventh prooflet depositHeight = extract(transactionHeightById(e.proofs[7]))#extracting the deposit transactionlet purchaseTx = transactionById(e.proofs[7])match purchaseTx {case deposit : TransferTransaction =>let correctSender = deposit.sender == e.sender#checking that the user transfers to himself the correct sum of the deposit and the interestlet correctAmount = deposit.amount + deposit.amount / depositDivisor * (height - depositHeight) / heightUnit == e.amountlet correctProof = extract(getBinary(this, toBase58String(deposit.id))) == e.idcorrectSender && correctProof && correctAmountcase _ => false}&& finalHeight <= heightcase _ => sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)}
In the third and final article in this series, we will look at some specific use cases for smart assets, including asset freezing and restricting transactions for specific addresses.
Join Waves Community
Read Waves News channel
Follow Waves Twitter
Subscribe to Waves Subreddit