Применение смарт-аккаунтов и смарт-ассетов Waves в финансовых инструментах
В предыдущей статье мы рассмотрели несколько кейсов применения смарт-аккаунтов в бизнесе — включая аукционы и программы лояльности.
Сегодня мы поговорим о том, как смарт-аккаунты и смарт-ассеты могут повысить прозрачность и надежность таких финансовых инструментов, как опционы, фьючерсы и векселя.
Опцион
Опцион — биржевой контракт, дающий покупателю право купить актив по определенной цене или до определенной даты, но не обязывающий его это сделать.
Реализация опциона может быть следующей:
Используем смарт-ассет для самих опционов как инструмента и смарт-аккаунт для участника, который выполняет роль биржи и выпускает опционы. Участник-биржа обещает, что продаст некоторое количество определенного ассета по цене sellPrice между высотами блоков expirationStart и expirationEnd).
В коде смарт-ассета мы просто проверим, что он торгуется только между указанными высотами, и больше ничего проверять не будем, оставим всю ответственность за соблюдение правил на код участника-биржи.
Код смарт-ассета:
let expirationStart = 100000
let expirationEnd = 101440
match tx {
case some : ExchangeTransaction | TransferTransaction =>
height > expirationStart && height <= expirationEnd
case _ => false
}
Будем считать, что действия происходят следующим образом: участник-биржа продает опционы на покупку какого-то ассета, и остальные участники могут пересылать эти опционы или торговать ими. Чтобы воспользоваться своим правом на покупку, потенциальный покупатель должен перевести желаемое количество опционов на счет продавца, то есть участника-биржи. Далее он записывает информацию о совершенном трансфере в стейт аккаунта участника-биржи и только затем ExchangeTransaction по заданным условиям покупки-продажи сможет пройти.
В коде смарт-аккаунта мы должны убедиться, что любая проходящая через него ExchangeTransaction для финального акта покупки-продажи соответствует заданным условиям, и участник покупает ровно то число юнитов, которое он отправил на счет участника-биржи. Потенциальный покупатель должен отправить корректную DataTransaction о произошедшем трансфере, чтобы участник-биржа мог избежать двойной траты. В этой DataTransaction покупатель кладет по ключу, равному его адресу, значение, равное числу опционов переведенных на счет участника-биржи, то есть числу юнитов ассета, которое он может купить.
Код смарт-аккаунта:
#владелец аккаунта дает обязательство продать определенное количество юнитов ассета
#по цене sellPrice между высотами блоков expirationStart и expirationEnd
let expirationStart = 100000
let expirationEnd = 101440
let sellPrice = 10000
let amountAsset = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
let priceAsset = base58'9jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
#ID ассета-опциона
let optionsAsset = base58'7jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
#извлекаем из транзакции адрес отправителя
let this = tx.sender
match tx {
case dataTx : DataTransaction =>
#извлекаем количество юнитов из дата-транзакции по ключу (ID пользователя)
let units = extract(getInteger(dataTx.data, dataTx.data[0].key))
#извлекаем трансфер-транзакцию опционов из пруфа
let e = transactionById(dataTx.proofs[2]) #
match e {
case transferTx : TransferTransaction =>
#убеждаемся, что трансфер был на текущий адрес
(transferTx.recipient == this) &&
#убеждаемся, что отправитель транзакции написал в качестве ключа свой ID
dataTx.data[0].key == toBase58String(transferTx.sender.bytes) &&
sigVerify(dataTx.bodyBytes, dataTx.proofs[0], transferTx.senderPublicKey) &&
#убеждаемся, что указанное количество юнитов соответствует посланному количеству опционов
(units == transferTx.amount) &&
#убеждаемся, что был переведен именно ассет-опцион
(transferTx.assetId == optionsAsset)
case _ => false
} &&
size(dataTx.data) == 1 && !isDefined(getInteger(this, dataTx.data[0].key))
&& height > expirationStart && height <= expirationEnd
case exchangeTx : ExchangeTransaction =>
#убеждаемся, что итоговый обмен происходит по указанным заранее правилам
let correctAssetPair = exchangeTx.sellOrder.assetPair.amountAsset == amountAsset &&
exchangeTx.sellOrder.assetPair.priceAsset == priceAsset
let correctPrice = exchangeTx.sellOrder.price == sellPrice
#извлекаем дата-транзакцию из пруфа
let d = transactionById(exchangeTx.proofs[2])
match d{
case dataTx : DataTransaction =>
let buyOrderSender = dataTx.data[0].key
toBase58String(exchangeTx.buyOrder.sender.bytes) == buyOrderSender &&
exchangeTx.amount == extract(getInteger(dataTx.data, buyOrderSender))
case _ => false
} &&
exchangeTx.sellOrder.sender == this &&
correctAssetPair && correctPrice &&
height > expirationStart && height <= expirationEnd
case _ => false
}
Фьючерсы на смарт-аккаунтах
В отличие от опциона, фьючерс (фьючерсный контракт) — это не право, а обязательство покупателя совершить покупку актива по зафиксированной контрактом цене в определенный момент в будущем.
В целом, реализация фьючерса похожа на реализацию опциона. Здесь смарт-ассет выступает в качестве фьючерса.
Также необходимо убедиться, что и покупатель, и продавец подписывают ордер на покупку. Фьючерс является обязательством, которое должно быть исполнено в любом случае. Значит, если продавец или участник отказываются от своих обязательств, любой участник сети может отправить транзакцию, исполнив таким образом фьючерс.
Скрипт смарт-ассета контролирует все TransferTransaction и ExchangeTransaction ассета-фьючерса, одобряя их только в том случае, если участник-покупатель создал ордер на будущую покупку ассета-фьючерса у участника-биржи.
Этот ордер должен быть действительным и удовлетворять условиям, на которых выпущен фьючерс. Чтобы проверить ордер, можно внести все его поля в стейт аккаунта-покупателя вместе с байтовым представлением подписанного ордера, а затем провести валидацию извне.
На данный момент RIDE не содержит нативной функции парсинга байтов транзакции, но включает все необходимые для ее реализации инструменты. Поэтому разработчики могут попробовать реализовать эту функцию самостоятельно.
Аккаунт с мульти-подписью / Эскроу
Аккаунт с мульти-подписью позволяет нескольким пользователям совместно управлять активами (например, операции с активами могут быть возможны лишь при наличии подписей трех пользователей из четырех). Для создания аккаунтов с мульти-подписью в языке RIDE мы можем использовать пруфы транзакций.
Аккаунт с мульти-подписью может быть также использован для эскроу-счета, на котором денежные средства хранятся до выполнения сторонами, заключившими договор, взятых на себя обязательств.
let alicePubKey = base58'5AzfA9UfpWVYiwFwvdr77k6LWupSTGLb14b24oVdEpMM'
let bobPubKey = base58'2KwU4vzdgPmKyf7q354H9kSyX9NZjNiq4qbnH2wi2VDF'
let cooperPubKey = base58'GbrUeGaBfmyFJjSQb9Z8uTCej5GzjXfRDVGJGrmgt5cD'
#выясняем, кто предоставил корректные подписи
let aliceSigned = if(sigVerify(tx.bodyBytes, tx.proofs[0], alicePubKey)) then 1 else 0
let bobSigned = if(sigVerify(tx.bodyBytes, tx.proofs[1], bobPubKey)) then 1 else 0
let cooperSigned = if(sigVerify(tx.bodyBytes, tx.proofs[2], cooperPubKey)) then 1 else 0
#суммируем все корректные подписи и проверяем их количество
aliceSigned + bobSigned + cooperSigned >= 2
Управляемый токенами реестр — token curated registry (TCR)
На многих блокчейн-платформах существует проблема токсичных ассетов. Например, создать ассет на Waves может любой адрес, заплативший комиссию.
Проблему защиты пользователей и самого блокчейна от токсичных ассетов помогает решить управляемый токенами реестр — token curated registry (TCR), — генерируемый держателями токенов.
Чтобы проголосовать за добавление конкретного токена в список, держатель делает ставку, равную его доле токенов от общего числа выпущенных. Токен включается в реестр, если за это проголосовали большинство его держателей.
В нашем примере мы позволяем пользователю добавить токен в список на рассмотрение (в период «challenge») по ключу стейта key = asset_name, только если текущее значение count = 0.
Также у пользователя в кошельке должен быть ненулевой баланс этого токена. Затем наступает период голосования, во время которого пользователь может отдать голос за каждый ассет в своем кошельке, но только по одному разу, поставив оценку от 1 до 10. Голоса пользователей представлены ключами вида user_address+assetID.
let asset = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
let addingStartHeight = 1000
let votingStartHeight = 2000
let votingEndHeight = 3000
#извлекаем из транзакции адрес отправителя
let this = extract(tx.sender)
#извлекаем адрес из пруфа транзакции
let address = addressFromPublicKey(tx.proofs[1])
match tx {
case t: DataTransaction =>
if(height > addingStartHeight)
then(
if(height < votingStartHeight)
then(
#adding
#выясняем, есть ли этот ассет у этого адреса
let hasTokens = assetBalance(address, asset) > 0
size(t.data) == 1
#убеждаемся, что этот ассет еще не был добавлен
&& !isDefined(getInteger(this, toBase58String(asset)))
#убеждаемся, что по ключу-ассету добавляется значение равное 0
&& extract(getInteger(t.data, toBase58String(asset))) == 0
&& hasTokens
)
else(
if(height < votingEndHeight)
then
(
#voting
#узнаем текущее количество голосов за данный ассет и задаваемое количество
let currentAmount = extract(getInteger(this, toBase58String(asset)))
let newAmount = extract(getInteger(t.data, toBase58String(asset)))
let betString = toBase58String(address.bytes) + toBase58String(asset)
#убеждаемся, что этот адрес еще не голосовал за этот ассет
let noBetBefore = !isDefined(getInteger(this, betString))
let isBetCorrect = extract(getInteger(t.data, betString)) > 0
&& extract(getInteger(t.data, betString)) <= 10
#убеждаемся, что у голосующего есть необходимые токены
let hasTokens = assetBalance(address, asset) > 0
#проверяем корректность значений транзакции
size(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 false
case _ => false
}
Абонентская плата
В этом примере мы рассмотрим использование смарт-аккаунтов для осуществления регулярных платежей за товар или услугу через заданные промежутки времени — «абонентской платы».
Если пользователь предоставляет смарт-аккаунту (через пруфы транзакции) ID TransferTransaction с требуемым количеством переведенных средств, он может записать в стейт аккаунта {key: address, value: true}.
Это будет означать, что пользователь подтверждает подписку на товар или услугу. Когда срок подписки истечет, любой пользователь сети может установить напротив соответствующего ключа в стейте значение false.
let subscriptionPeriod = 44000
let signature = tx.proofs[0]
let pk = tx.proofs[1]
let requiredAmount = 100000
#извлекаем из транзакции адрес отправителя
let this = extract(tx.sender)
match tx {
case d: DataTransaction =>
#извлекаем дату последнего платежа
let 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
#извлекаем трансфер-транзакцию по ID, указанному в пруфах
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
#убеждаемся, что ассет waves
&& !isDefined(purchase.assetId)
case _ => false
}
)
case _ => false
}
Голосование
Смарт-аккаунты могут быть использованы для реализации голосования на блокчейне. Примером может служить голосование за лучший отчет амбассадора в рамках амбассадорской программы. Стейт аккаунта используется в качестве платформы для записи голосов за тот или иной вариант.
В этом примере голосовать разрешено только тем, кто приобрел специальные «голосовательные» токены. Участник заранее отправляет DataTransaction с парой (key, value) = (purchaseTransactionId, buyTransactionId). Установка другого значения по этому ключу запрещена. Используя свой адрес и вариант голосования, можно установить DataEntry только раз. Голосование возможно только в установленный период.
let asset = base58'8jfD2JBLe23XtCCSQoTx5eAW5QCU6Mbxi3r78aNQLcNf'
let address = addressFromPublicKey(tx.proofs[1])
let votingStartHeight = 2000
let votingEndHeight = 3000
#извлекаем из транзакции адрес отправителя
let this = extract(tx.sender)
match tx {
case t: DataTransaction =>
(height > votingStartHeight && height < votingEndHeight) &&
#убеждаемся, что у транзакции правильная подпись
sigVerify(tx.bodyBytes, tx.proofs[0], tx.proofs[1]) &&
#проверяем, что пользователь отдает свой голос напротив своего адреса
if (t.data[0].key == toBase58String(address.bytes))
then (
#извлекаем транзакцию перевод голосовательного токена из пруфов
let purchaseTx = transactionById(t.proofs[7])
match purchaseTx {
case purchase : TransferTransaction =>
let correctSender = purchase.sender == t.sender
let correctAsset = purchase.assetId == asset
let correctPrice = purchase.amount == 1
let correctProof = extract(getBinary(this, toBase58String(purchase.id))) == t.id
correctSender && correctAsset && correctPrice && correctProof
case _ => false
}
)
else
size(t.data) == 1 && !isDefined(getBinary(this, t.data[0].key))
case _ => false
}
Вексель
Вексель — письменное обязательство, по которому одна сторона должна выплатить другой фиксированную сумму в момент требования или в заранее установленную дату.
В нашем примере используется смарт-аккаунт, дата экспирации которого соответствует дате выплаты по векселю.
let expiration = 100000
let amount = 10
let 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
}
Депозит
Депозит — размещение денежных средств в банке на определенных условиях (срок, процент).
В нашем примере функцию банка выполняет смарт-аккаунт. После определенного количества блоков, что соответствует сроку депозита, пользователь может вернуть свои деньги с процентом. В скрипте задана высота блока (finalHeight), после достижения которой пользователь может снять деньги со счета.
heightUnit — количество блоков в одной единице времени (например, месяц, год и т. д.). Сначала мы проверяем наличие записи с парой (key, value) = (initialTransferTransaction, futureDataTransaction). Затем пользователь должен отправить TransferTransaction с правильной информацией о сумме депозита и процентов, начисленных за период депозита. Эта информация сверяется с исходной TransferTransaction, которая содержится в текущем пруфе TransferTransaction. depositDivisor — число, обратное доле депозита (если депозит принимается под 10%, доля депозита составляет 0,1, а depositDevisor = 1/0,1 = 10).
let this = extract(tx.sender)
let depositDivisor = 10
let heightUnit = 1000
let finalHeight = 100000
match tx {
case e : TransferTransaction =>
#извлекаем высоту транзакции по ID транзакции в седьмом пруфе
let depositHeight = extract(transactionHeightById(e.proofs[7]))
#извлекаем транзакцию депозита
let purchaseTx = transactionById(e.proofs[7])
match purchaseTx {
case deposit : TransferTransaction =>
let correctSender = deposit.sender == e.sender
#убеждаемся, что пользователь переводит себе корректную сумму депозита + проценты
let correctAmount = deposit.amount + deposit.amount / depositDivisor * (height - depositHeight) / heightUnit == e.amount
let correctProof = extract(getBinary(this, toBase58String(deposit.id))) == e.id
correctSender && correctProof && correctAmount
case _ => false
}
&& finalHeight <= height
case _ => sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)
}
В третьей и заключительной статье этой серии мы рассмотрим еще варианты применения смарт-ассетов, включая заморозку и ограничение транзакций для конкретных адресов.