Example Digital Exchange Smart Contract Application
Overview
Exchanging digital goods in a decentralized way, with no centralized authority, is one of the key promises of blockchain technology. Digital exchanges and blockchains realize the fulfillment of this promise in different ways. For high frequency exchanges, much of the work is done in layer-2 applications where transactions are finalized on the blockchain. For lower frequency exchanges, most of the work is done directly on the chain. On Algorand a digital exchange can be implemented in many ways. This solution illustrates one of those ways where all orders are stored and fulfilled on the blockchain. This article is meant to be more instructive, to help developers understand the key concepts of various Algorand technology pieces and as such it is fairly simplistic in operation.
In this solution, we will illustrate how to connect buyers and sellers and facilitate exchange of assets in a decentralized fashion on the Algorand blockchain. We will use limit order contracts as the method of exchange, where a buyer opens an order and specifies the asset they want to purchase and at what exchange rate. This order then resides on the blockchain where any seller of the asset can fulfill the order at a later time. The solution also offers the ability for the buyer to close the order at any time.
This application will involve using many of the Algorand technologies, including stateless smart contracts, stateful smart contracts, atomic transfers, standard assets, asset transactions, and the Algorand Indexer. We will begin by covering the overall application design and then dive into the specifics on how to build the application.
Design Overview
To implement this solution, four basic operations are required. A user should be able to open, close, or execute an order. A user should also be able to list all open orders.
Note
This example uses hardcoded addresses for a user opening an order and a user to execute the order. The example also uses hardcoded connections to Algorand’s TestNet and Indexer. This is done to simplify the example but ideally the application would use AlgoSigner for managing accounts and connect to the node the application deployer uses or to one of the API services available.
1 - Open Order
The solution allows a buyer to create a limit order where they specify what Algorand Asset they are interested in purchasing. The order should also contain a minimum and maximum amount of micro Algos they are willing to spend and an exchange rate. In the solution we use a simple ‘N’ and ‘D’ notation to represent this exchange rate. Where ‘N’ represents the ammount of the Asset and ‘D’ is the micoAlgos they are willing to spend. Once entered, the user can place the order.
The order is converted into a stateless contract that is used for delegated authority. Stateless contracts can be used either as escrows or delegation. With an Escrow the funds are moved to an account where the logic determines when they leave. With delegation, the logic is signed by a user and this logic can be used later to remove funds from the signers account. Either could have been used in this example. This solution implemented delegation, where the logic is signed by the user listing the order. This is referred to as creating a logic signature. This logic signature is then saved to a file that is pushed to the server for later use. The signed logic is deleted when the user closes the order or the order is executed.
As part of this solution, there is a main stateful smart contract that has methods for opening, closing and executing an order. The stateless smart contract delegation logic is linked to this stateful smart contract. This is done to make the stateless delegation logic invalid if not used in conjunction with the stateful smart contract application call.
When the user opens the order, a call is made to the stateful smart contract to open the order. The stateful smart contract stores the order number in the user’s local storage. This limits the number of open orders to 16. This could have been extended by using a different order number generator, but for simplicity and readability this limitation is used.
2 - View Open Orders
Once an order has been placed, the solution provides a list box and a refresh orders button. Once clicked, the web application calls the Algorand Indexer to search all accounts that have opted into the stateful smart contract. These accounts are iterated over and their local storage values (open orders) are read back and populated into the list box.
3 - Execute Open Order
Once the open orders are listed, another user can login to the web application, select an open order and execute it. The executing user can specify how many of the assets they are selling and how much micro Algos they are requesting. If they specify more than the original limit order’s maximum or less than the original minimum, the execution will fail. If they specify an exchange rate that is less than the original limit order specified, the execution will also fail. Once the executing user presses the execute order button, the web application will generate three transactions. The first is a call to the stateful smart contract specifying they are executing the specific order. The second is a payment transaction from the limit order lister to the execution user in the specified amount of micro Algos. The third transaction is an asset transfer from the execution user to the limit order lister’s account transferring the specified asset amount. The first and third transactions are signed by the execution user. The second transaction (payment) is signed with the stateless smart contract logic that the listing user signed earlier when opening the order. This logic signature is read from the file that was uploaded to the server. These three transactions are grouped atomically and pushed to the server. With atomic transactions, if any transaction fails they all fail. This results is both parties getting what they were expecting.
The stateful smart contract in the first transaction will clear the order from the listing user’s local state and the signed logic file is then deleted from the server.
4 - Close Order
Any user that has an open order can select this open order from the list of orders and click the close order button. This simply removes the open order from the stateful smart contract’s local state for the user and deletes the signed logic file from the server.
Info
This Application uses the JavaScript SDK to implement all calls to the Algorand node and Indexer.
Open Order - Step 1
The first step involves opening an order where a user specifies a set of criteria for a limit order.
The required data includes the asset ID of the asset they are interested in acquiring, the minimum and maximum micro Algos they are willing to spend, and a N
and D
value. These two values together equate to the exchange rate of N
/D
. So for every N
amount of the asset they are willing to spend D
micro Algos. When the Place Order button is pressed, the web app code generates a delegate stateless smart contract using a basic TEAL template string that is shown below.
Info
Assets must be opted into before they can be traded. This application does not handle this operation.
This contract code does the following checks.
- Verify that there are three transactions submitted at once.
- The first transaction must be a call to a stateful contract.
- The second must be a payment transaction.
- The third must be an asset transfer.
- The stateful app call must be a NoOp call to our dex stateful contract.
- None of the transactions should be a Rekey operation.
- The payment transaction should be between min and max values specified.
- The asset transfer should be the one specified in the GUI.
- Verified that the exchange rate is correct.
let delegateTemplate = `#pragma version 2
// this delegate is
// only used on an execute order
global GroupSize
int 3
==
// The first transaction must be
// an ApplicationCall (ie call stateful smart contract)
gtxn 0 TypeEnum
int appl
==
&&
// The second transaction must be
// an payment tx
gtxn 1 TypeEnum
int pay
==
&&
// The third transaction must be
// an asset xfer tx
gtxn 2 TypeEnum
int axfer
==
&&
// Verify fee is reasonable
txn Fee
int 10000
<=
&&
// The specific App ID must be called
// This should be changed after creation
// This links this contract to the stateful contract
gtxn 0 ApplicationID
int 12867764 //stateful contract app id
==
&&
// The application call must be
// A general application call
gtxn 0 OnCompletion
int NoOp
==
&&
// verify no transaction
// contains a rekey
gtxn 0 RekeyTo
global ZeroAddress
==
&&
gtxn 1 RekeyTo
global ZeroAddress
=
&&
gtxn 2 RekeyTo
global ZeroAddress
==
&&
gtxn 0 CloseRemainderTo
global ZeroAddress
==
&&
gtxn 1 CloseRemainderTo
global ZeroAddress
==
&&
gtxn 2 CloseRemainderTo
global ZeroAddress
==
&&
gtxn 0 AssetCloseTo
global ZeroAddress
==
&&
gtxn 1 AssetCloseTo
global ZeroAddress
==
&&
gtxn 2 AssetCloseTo
global ZeroAddress
==
&&
bz fail
// min algos spent
gtxn 1 Amount
int <min>
>=
// max algos spent
gtxn 1 Amount
int <max>
<=
&&
// asset id to trade for
int <assetid>
gtxn 2 XferAsset
==
&&
bz fail
// handle the rate
// gtxn[2].AssetAmount * D >= gtxn[1].Amount * N
// N units of the asset per D microAlgos
gtxn 2 AssetAmount
int <D> // put D value here
mulw // AssetAmount * D => (high 64 bits, low 64 bits)
store 2 // move aside low 64 bits
store 1 // move aside high 64 bits
gtxn 1 Amount
int <N> // put N value here
mulw
store 4 // move aside low 64 bits
store 3 // move aside high 64 bits
// compare high bits to high bits
load 1
load 3
>
bnz done
load 1
load 3
==
load 2
load 4
>=
&& // high bits are equal and low bits are ok
bnz done
err
done:
int 1
return
fail:
int 0
return
`;
The values entered in the web app are substituted into the contract. This code is then compiled. An order number is generated using the entry values supplied by the user. This compiled logic is then signed by the user placing the order. This signed logic (logic signature) is uploaded to the server for later use. The web application then calls the stateful smart contract application to open the order, supplying the order number which is stored in the user’s local state. The portion of the stateful smart contract code for opening the order is shown below.
Info
Instead of using delegation here, an escrow could be created using the same logic but the maximum funds would need to be transferred to it at this time.
This logic does the following checks.
- Verify that only one transaction is in the group.
- The order number should be the second argument to the contract call.
- Check the local state to see if the same order already exists. If so, fail.
- Store the order number in the local state.
open:
// only works for app call
global GroupSize
int 1
==
bz fail
int 0 //sender
txn ApplicationID //current smart contract
// 2nd arg is order number
txna ApplicationArgs 1
app_local_get_ex
// if the value already exists fail
bnz p_fail
pop
// store the order number as the key
int 0
txna ApplicationArgs 1
int 1
app_local_put
int 1
return
View Open Orders - Step 2
Once an order is placed, other users can see the order by clicking on the Refresh Orders button. This same function could also be placed on a timed interval but for simplicity the solution does not implement this feature. The order numbers are just the concatenated values specified when the order was opened.
The JavaScript SDK is used to connect to the Indexer and search for all accounts that have opted into the stateful smart contract. These accounts are iterated and their local state is examined to find currently opened orders. Within the source files, additional code is provided to simplify this section of code so that it relies only on the Indexer. This additional code is commented out as a current indexer bug prevents it from working. This issue should be resolved soon and the code can be exchanged with the simpler implementation.
(async () => {
let accountInfo = await indexerClient.searchAccounts()
.applicationID(APPID).do();
console.log(accountInfo);
let accounts = accountInfo['accounts'];
numAccounts = accounts.length;
for (i = 0; i < numAccounts; i++) {
let add = accounts[i]['address'];
let accountInfoResponse = await algodClient.accountInformation(add).do();
for (let i = 0; i < accountInfoResponse['apps-local-state'].length; i++) {
if (accountInfoResponse['apps-local-state'][i].id == APPID) {
if (accountInfoResponse['apps-local-state'][i][`key-value`] != undefined) {
console.log("User's local state:");
for (let n = 0; n < accountInfoResponse['apps-local-state'][i][`key-value`].length; n++) {
console.log(accountInfoResponse['apps-local-state'][i][`key-value`][n]);
let kv = accountInfoResponse['apps-local-state'][i][`key-value`]
let ky = kv[n]['key'];
console.log(window.atob(ky));
let option = document.createElement('option');
option.value = add + "-" + window.atob(ky);
option.text = window.atob(ky);
ta.add(option);
}
}
}
}
}
})().catch(e => {
console.log(e);
});
Execute Open Order - Step 3
A user executes an order by selecting the order from the open orders list. This will populate a few entry boxes specifying the asset being exchanged, the quantity of that asset and how many micro Algos the user executing the limit order will receive. These boxes are populated with the initial correct exchange rate but the user can alter these to sell more assets as long as the exchange rate is at least as favorable to the order lister as the initial rate and the minimum and maximum micro Algos limits are respected. If any of these conditions are violated the execution will fail due to a logic error. This logic is in the stateless smart contract that the listing user signed when the order was placed.
When the Execute Order button is pressed, the web application loads the logic signature signed earlier by the listing user and creates three transactions. The first is a call to the stateful smart contract passing two parameters, the string “execute” and the order number. Additionally, it passes the original listing account address into the stateful smart contract’s account array. This is so the stateful smart contract will have access to that account’s local state and can clear that order once the order is executed.
The second transaction is a payment transaction from the listing user’s account to the executing account in the amount specified by the executing user. The third transaction is an asset transfer from the executing user’s account to the listing user’s account. These transactions are grouped and signed. The first and third transactions are signed by the executing user. The second transaction is signed by the logic signature that was signed when the order was placed. These transactions are then submitted to the Algorand network. They either all succeed or all fail.
let params = await algodClient.getTransactionParams().do();
let appAccts = [];
appAccts.push(rec);
//call stateful contract
let transaction1 = algosdk.makeApplicationNoOpTxn(account.addr, params, appId, appArgs, appAccts)
//make payment tx signed with lsig
let transaction2 = algosdk.makePaymentTxnWithSuggestedParams(rec, account.addr, amount, undefined, undefined, params);
//make asset xfer
let transaction3 = algosdk.makeAssetTransferTxnWithSuggestedParams(account.addr, rec, undefined, undefined,
assetamount, undefined, assetId, params);
let txns = [transaction1, transaction2, transaction3];
// Group both transactions
let txgroup = algosdk.assignGroupID(txns);
// Sign each transaction in the group
let signedTx1 = transaction1.signTxn(executeAccount.sk)
let signedTx2 = algosdk.signLogicSigTransactionObject(txns[1], lsig);
let signedTx3 = transaction3.signTxn(executeAccount.sk)
// Combine the signed transactions
let signed = []
signed.push(signedTx1);
signed.push(signedTx2.blob);
signed.push(signedTx3);
let tx = await algodClient.sendRawTransaction(signed).do();
await waitForConfirmation(client, tx.txId);
await deleteLsigFile(fn);
Once the transactions succeed, the signed stateless contract is deleted from the server.
The portion of the stateful contract that handles the order execution is shown below. This code performs the following checks.
- Verify that three transactions are submitted atomically.
- The first must be a call to a stateful smart contract.
- The second must be a payment transaction.
- The third must be an asset transfer transaction.
- Verify that the order exists. If it does not fail.
- Delete the executed order from the original listers local state.
execute:
// Must be three transactions
global GroupSize
int 3
==
// First Transaction must be a call to a stateful contract
gtxn 0 TypeEnum
int appl
==
&&
// The second transaction must be a payment transaction
gtxn 1 TypeEnum
int pay
==
&&
// The third transaction must be an asset transfer
gtxn 2 TypeEnum
int axfer
==
&&
bz fail
int 1 // Creator of order
txn ApplicationID // Current stateful smart contract
txna ApplicationArgs 1 // 2nd argument is order number
app_local_get_ex
bz p_fail // If the value doesnt exists fail
pop
// Delete the ordernumber
int 1 //creator of order
// 2nd arg is order number
txna ApplicationArgs 1
app_local_del
int 1
return
Close Open Order - Step 4
The original listing user of an order can at any time select the placed order from the open orders list and press the Close Order button to cancel the order. The web application will trigger a stateful smart contract call passing two parameters, the string “close” and the order number. The stateful contract will then delete that order number from the local state of the user and the web app will then delete the previous signed stateless contract from the server.
The portion of the stateful smart contract that handles the close operation is shown below. The code performs the following checks.
- Verify that this is a single transaction.
- Verify that the order actually exists in the users local state. If not, fail.
- Delete the ordernumber from the local state of the user.
close:
// only works for app call
global GroupSize
int 1
==
bz fail
int 0 //account that opened order
txn ApplicationID //current smart contract
// 2nd arg is order number
txna ApplicationArgs 1
app_local_get_ex
// if the value doesnt exists fail
bz p_fail
pop
// delete the ordernumber
int 0 //account that opened order
// 2nd arg is order number
txna ApplicationArgs 1
app_local_del
int 1
return
Some Caveats
The application has very little error checking and is meant as a learning exercise. Additional error checking and handling should be done before using this logic in a production application. For example, the order numbers are generated based solely on what values are used in the limit order. In the web application, this is prepended with the users address but the listing box and the stateful teal application remove this prepending. This will allow duplicate order numbers from two different users. While this does not cause issues, it can be seen as confusing. Additionally, the logic signature is uploaded unencrypted and while this should not matter it is probably good practice to make this secure.
The application also uses hardcoded addresses for two fictitious users, which should be changed to allow integration with AlgoSigner.
Conclusion
The simple DEX application illustrates using several of Algorand’s layer-1 features to implement a functional smart contract application. The full source code for the application is available on Github. Make sure to see the readme for instructions on setup and running the application.