Articles
No Results
Article Thumbnail Image

Algorand Exchange Price Oracle

Overview

At Rand Labs, we have been researching ways to insert pricing information on-chain from an Oracle to use it within TEAL code and to use it to create group transactions on My Algo Algorand Wallet. In this article, we show the implementation of a simple exchange using an on-chain Oracle as the price source.

The code used for this article is freely available in the following repository: algorand-exchange-price-oracle. It uses the AlgoExplorer Algorand API so it does not require any key or local node to run it, so just clone, install, and run asset-exchange.js with NodeJS.

Oracle

The Oracle, a real-world trusted entity, must create a pre-signed transaction with the data included in that transaction, in this case, the price information, and make it available to everyone. Those that need to use the price oracle in their system can simply take the pre-signed Oracle transaction through the Oracle’s website or API and broadcast it from another account that needs to use the price information in a smart contract.

For the implementation, a price submitter is needed where the price is attached to the Note field of the transaction signed by the Oracle. Any TEAL code using this Oracle data must pay a fee that is used to run the on-chain submitter. Curiously, if there were enough DeFi Dapps using this service it can be self-sustaining. In addition to this, an on-chain decentralized exchange could use more than one Oracle price source and the price submitter can provide up to 3 price sources signed by different entities in the same transaction.

Price Submitter

For the Price Submitter, a LogicSig TEAL code is used to allow anyone to create a transaction from the Oracle account if the requirements of the signed TEAL object are satisfied. This only happens if the transaction containing Oracle data (1) pays the fee, (2) fills the Note field with the correct price, and (3) use the data before its expiration.

By looking closely at the TEAL code, one can see that the Note and LastValid fields are verified by using invalid values (more on this in the Javascript code section below).

TEAL code

oracle-delegated.teal.tmpl
oracle-delegated.teal

// Signs statements to be used as inputs to the TEAL script.
// This is an ephemeral escrow.
// TMPL_ORACLE_FEE_COLLECTOR: public key of the oracle fee collector that receives the fees
// TMPL_ORACLE_FEE:    fee to pay to the oracle
txn TypeEnum
int 1
==
txn Fee
global MinTxnFee
==
&&
// Oracle fee
txn Amount
int TMPL_ORACLE_FEE
>=
&&
txn Receiver
addr TMPL_ORACLE_FEE_COLLECTOR
==
&&
txn CloseRemainderTo
global ZeroAddress
==
&&
txn Note
btoi
// this number is replaced by the inject function
int 5
==
&&
txn LastValid
// this number is replaced by the inject function
int 6
<=
&&

JavaScript code

The following code submits the price of the ASA LECOP every round with the signed TEAL code including setting the price and the expiration round. The TEAL code is signed and submitted in the Message Pack object attached as the Note field of the transaction. As an example, check the following transaction SJKWRO5DATGUTQ3MXJHQRSB3TFXFMHVNT5FV7JTVC3GIY57TPAJQ:

Oracle Signed Transaction in AlgoExplorer

Instead of just submitting the price directly to the Note field, it is done in this way in order to provide it as a LogicSig object that can work combined in a transaction group. This object is the authorization of the Oracle to use the price following the rules described above.

This Javascript code has a trick used to replace the invalid values in the TEAL code with the real values. It is preferable to use TEAL arguments but, unfortunately, there is no way to sign them. For this reason, an injection technique is used based on this code. The idea is simple but requires a slightly low-level implementation. First, the offset of the values that need to be replaced from the TEAL bytecode needs to be known in order to replace them. Compiling the TEAL code will generate a Base64 program that, after converting it to hexadecimal, generates the bytecode shown below. It can be seen that the 5 and 6 (the invalid values used in the TEAL) at the beginning of the bytecode, the place were the constants are located, are found on offset 7 and 8 (the first byte is offset 0):

0120040190a10f05062601202bedccce77cc38a20277b645a5ba19e50e234523ff81eb6b7964286038cfaaa3311022123101320012103108230f1031072812103109320312103105172412103104250e10

In the Javascript code the offsets, the types, and the new values need to be specified.

Function priceSubmitter

let oracleProgramReferenceProgramBytesReplace = Buffer.from("ASAEAZChDwUGJgEgK+3MznfMOKICd7ZFpboZ5Q4jRSP/getreWQoYDjPqqMxECISMQEyABIQMQgjDxAxBygSEDEJMgMSEDEFFyQSEDEEJQ4Q", 'base64');
let referenceOffsets = [ /*Price*/ 7, /*LastValid*/ 8];
let injectionVector =  [price, params.lastRound + priceExpiration];
let injectionTypes = [templates.valTypes.INT, templates.valTypes.INT];

var buff = templates.inject (oracleProgramReferenceProgramBytesReplace, referenceOffsets, injectionVector, injectionTypes);

let oracleProgram = new Uint8Array (buff);   
let lsigOracle = algosdk.makeLogicSig (oracleProgram);
lsigOracle.sign (oracleAccount.sk);
let priceObj = {
  signature: lsigOracle.get_obj_for_encoding(),
  price: price,
  decimals: 4
}

let oraclePriceSubmitterTx = algosdk.makePaymentTxnWithSuggestedParams (submitterAccount.addr,
 submitterAccount.addr, 0, undefined,
 algosdk.encodeObj(priceObj), suggestedParams);

let oraclePriceSubmitterTxSigned = oraclePriceSubmitterTx.signTxn (submitterAccount.sk);
let oraclePriceTx = await algodClient.sendRawTransaction (oraclePriceSubmitterTxSigned);

Exchange Atomic Swap

Finally, the most fascinating part is the way the Oracle price is combined in the group transaction.

The fee is paid on the first transaction of the group to allow the Oracle account to send the fee to the submitter (TMPL_ORACLE_FEE_COLLECTOR). If the Oracle does not have enough balance, the transaction group is not inserted in the blockchain. For this reason, the Oracle account must always have a minimum balance and the user of the Oracle must make sure to send enough funds to the Oracle account to cover the fees and the transaction costs.

In order to atomically swap the LECOP coins with the Algos using the Oracle price, there is another TEAL program used in Transaction 2. This program verifies that the amount of Algos in one of the transactions is equivalent, based on the submitted price, to the amount of LECOP received in the other. The atomic swap consists of 4 transactions:

Transaction 0: from the user to TMPL_ORACLE to pay Oracle fee.

Transaction 1: from TMPL_ORACLE to TMPL_ORACLE_FEE_COLLECTOR sending the fee. If the Oracle does not have enough funds, the transaction is not completed, that is why Transaction 0 must be done prior to Transaction 1. The Note field contains the price while the TEAL code in oracle-delegated.teal verifies that it is correct and has not expired.

Transaction 2: from TMPL_EXCHANGE to the user where the amount of assets TMPL_ASA_ID corresponds to the value of the Algos sent in Transaction 3 based on the price submitted by the Oracle. This transaction uses asset-exchange.teal signed exchange account. Using the delegated TEAL anyone can submit transactions from the exchange account if the signed TEAL code is validated by the transaction group.

Transaction 3: from user to TMPL_EXCHANGE sending the Algos to pay for the assets TMPL_ASA_ID.

Exchange Group Transaction

An example can be seen in transaction Group ID +znTV4zfTKwF1z915MkPxQ8AmzxDNVpo1wDJb8LEsrc=.

TEAL Code

asset-exchange.teal.tmpl
asset-exchange.teal

// Asset Exchange Teal
// Allows exchanging algos for assets based on a price submitted in another transaction part of the group
// TMPL_EXCHANGE:  public key of the Exchange
// TMPL_ORACLE:   public key of the Oracle
// TMPL_ASA_ID:   asset id to exchange
// TMPL_PRICE_DECIMALS: number to divide the price to get the real price which is 10^DECIMALS (e.g.: for 4 decimals use 10000)

global GroupSize
int 4
==
txn GroupIndex
int 2
==
&&
txn TypeEnum
int 4
==
&&
gtxn 3 TypeEnum
int 1
==
&&
txn Fee
global MinTxnFee
==
&&
gtxn 3 Receiver
addr TMPL_EXCHANGE
==
&&
txn CloseRemainderTo
global ZeroAddress
==
&&
// verify that the price was submitted by the Oracle
gtxn 1 Sender 
addr TMPL_ORACLE
==
&&
// Note: price
gtxn 1 Note
len
int 8
==
&&
txn XferAsset
int TMPL_ASA_ID
==
&&
gtxn 3 Amount 
int TMPL_PRICE_DECIMALS
*
gtxn 1 Note
btoi
/
txn AssetAmount 
>=
&&

Javascript code

Function submitOracleTransactions

let lsigExchange = algosdk.makeLogicSig(exchangeProgram);
lsigExchange.sign(exchangeAccount.sk);
let oracleRetrieveTx;

// get the last price submitted
algodClient.transactionByAddress(submitterAccount.addr, undefined, undefined, 1);
oracleRetrieveTx = apiRes.transactions[0];

let priceMessagePackDecoded = algosdk.decodeObj(oracleRetrieveTx.note);
let decodedLsig = priceMessagePackDecoded.signature;
lsigOracle = algosdk.makeLogicSig(decodedLsig.l, decodedLsig.arg);
lsigOracle.sig = decodedLsig.sig;
lsigOracle.msig = decodedLsig.msig;

// Transaction 0
// pay Oracle fee 
let oracleFeeTx = algosdk.makePaymentTxnWithSuggestedParams(consumerAccount.addr, oracleAccount.addr, oracleFee + 1000, undefined, new Uint8Array(0), suggestedParams);
let note = getInt64Bytes(priceMessagePackDecoded.price);

// Transaction 1
// insert price from the oracle account and send the fee to the submitter
let oracleTx = algosdk.makePaymentTxnWithSuggestedParams(oracleAccount.addr,  submitterAccount.addr, oracleFee, undefined, note, suggestedParams);

// Transaction 2
// send assets from Exchange to User 
let exchangeTx = algosdk.makeAssetTransferTxnWithSuggestedParams(exchangeAccount.addr, consumerAccount.addr, undefined, undefined, assetAmount, new Uint8Array(Buffer.from("Price: " + priceMessagePackDecoded.price, "utf8")), assetId, suggestedParams);

// Transaction 3 
// send algos needed to buy assetAmount assets based on price
let algosTx = algosdk.makePaymentTxnWithSuggestedParams(consumerAccount.addr, exchangeAccount.addr, Math.ceil(assetAmount*priceMessagePackDecoded.price/priceDecimals), undefined, new Uint8Array(Buffer.from("Price: " + priceMessagePackDecoded.price, "utf8")), suggestedParams);

// Store all transactions
let txns = [oracleFeeTx, oracleTx, exchangeTx, algosTx];

// Group all transactions
let txgroup = algosdk.assignGroupID(txns);

// Sign each transaction in the group with correct key or LogicSig
let signed = []
let oracleFeeTxSigned = oracleFeeTx.signTxn(consumerAccount.sk);
let oracleTxSigned = algosdk.signLogicSigTransactionObject(oracleTx, lsigOracle);
let exchangeTxSigned = algosdk.signLogicSigTransactionObject(exchangeTx, lsigExchange);
let algosTxSigned = algosTx.signTxn(consumerAccount.sk);
signed.push(oracleFeeTxSigned);
signed.push(oracleTxSigned.blob);
signed.push(exchangeTxSigned.blob);
signed.push(algosTxSigned);

let tx = (await algodClient.sendRawTransactions(signed));

Conclusion

Algorand Smart Contract Layer1 (ASC1) is very powerful and it is useful to achieve very complex tasks. This approach based on micro-services ensures a very simple and easy to understand code while preserving an incredible overall performance of the network.

The way ASC1 works is novel and different from other blockchains because the developer needs to divide complex operations in different transactions instead of thinking in a sequential code like in Solidity. It takes some time to get used to this logic but the result is simpler code, easier to audit, and with a drastically reduced attack surface.

Algorand is still in its infancy stage, patience and hard work will be needed to build the initial building blocks and the development tools to help engineers produce and troubleshoot code. At Rand Labs, we know that it is imperative to support these efforts and to generate enough of these resources to show the power of Algorand’s technology to the world.

asset transfer

javascript

logicsignature

smart contracts

atomic transfer

APIs

asc1

teal

July 20, 2020