Protecting Funds in Algorand Smart Signature Contract Accounts
Public/Secret Keys + ed25519verify_bare opcode
Think of an Escrow Account containing funds earmarked for any given recipient.
It is essential those funds are protected from unauthorized withdrawal.
Since Algorand’s smart signature code (the TEAL) is publicly visible on the blockchain, your implementation must prevent the impostor from gaming your code and stealing funds.
One way to do this is by employing a Public/Secret Key implementation alongside the TEAL ed25519verify_bare opcode.
In the scenario discussed below, we cannot hard-code the recipient’s wallet address in the Smart Contract (TEAL) because we don’t know it ahead of time. The Recipient may not even have a wallet yet.
DISCLAIMER: The code in this article, as-is, is not intended for production use. The core concepts are presented here but it is essential the developer employs Algorand best practices for various checks/audits before deploying to production.
High Level Flow
The smart signature contract accounts in these examples are intended for 1-time usage (1) Sender deposits funds into the Smart Signature (2) Intended recipient withdraws all funds and account is closed.
- Create a Public / Secret Key pair.
- Embed the public key inside the TEAL code to be used along with the ed25519verify_bare opcode (example code below).
- Protect the Secret Key & give it only to the intended recipient. The Secret Key is essentially like a mailbox key that will unlock a specific mailbox (your Escrow Smart Signature).
Code Fragments
// Import the necessary support libraries
const nacl = require('tweetnacl')
const base32 = require('hi-base32');
// Generate Public/Secret Keys
const keypair = nacl.sign.keyPair();
const secretKeyBytes = keypair.secretKey
const publicKeyBytes = keypair.publicKey
// The base64 version of PubKey gets hard-coded into the TEAL
const publicKeyB64 = Buffer.from(publicKeyBytes).toString("base64");
// base32 is used since it results in a shorter string to deal with
const secretKey = base32.encode(secretKeyBytes);
Give confidential Secret Key to intended recipient.
Allow user to click a link with embedded base32 Secret Key, e.g.
https://www.yoursite.com?token=SECRET_KEY_GOES_HERE
NOTE: Since url params (token) can live in webserver logs, your implementation should include a separate piece of information (like a unique code) which is not part of the URL. I leave this to you on how best to implement for your project.
Now, recipient wishes to receive the funds — this can only happen if Receiver possesses the Secret Key.
Grab the url parameter (token) & convert it back into bytes for further processing.
const secretKeyBytes = new Uint8Array(base32.decode.asBytes(token));
Using the Recipient’s Algorand Wallet address (which we now know after wallet connection), convert it into a regular** public key.
**NOTE: When Algorand creates addresses it modifies a Public Key. Below decodeAddress is the reverse of that. This is necessary in order to work with the nacltweet library. Algorand Ref: https://developer.algorand.org/docs/get-details/accounts/
const recipientAddress = 'ABCDEF...XYZ' // After wallet connect
const pk = algosdk.decodeAddress(recipientAddress)
console.log('decoded address', pk.publicKey)
Now sign the Recipient’s decoded wallet address using the Secret Key & pass the resulting signature (as arg 1 in this case) into the Smart Signature.
const signature = nacl.sign.detached (pk.publicKey, secretKeyBytes);
let smartSigArgs = [];
// TEAL arg 0 = Denotes we're doing a Receive
smartSigArgs.push(Buffer.from("r"));
// TEAL arg 1 = The Recipient's signed wallet address
smartSigArgs.push(Buffer.from(signature));
// Refresher on how the Smart Signature is initially created & instantiated
const smartSigTemplate = getSmartSigTemplate(publicKeyB64); // See below
const smartSigCode = await algoClient.compile(smartSigTemplate).do();
const compiledCode = new Uint8Array(Buffer.from(smartSigCode.result, "base64")),
const smartSig = new algosdk.LogicSigAccount(compiledCode, smartSigArgs);
….
let txn = algosdk.makeAssetTransferTxnWithSuggestedParams(….)
let signedTxn = algosdk.signLogicSigTransaction(txn, smartSig);
Substitute in any variables to hardcode into TEAL prior to compilation.
const getSmartSigTemplate = (pubKey) => {
// Subset of code to illustrate the ed25519verify_bare usage
// NOTE: the base64 version of pubKey is hardcoded pre-compile.
return `
#pragma version 7
arg 0
byte "r"
==
bnz handle_asset_receiver_check
handle_asset_receiver_check:
txn AssetReceiver
arg 1 // Signature
pushbytes b64 ${pubKey}
ed25519verify_bare
bnz handle_next_thing // whatever that is for you
// If we reach here, impostor alert, the signature is no good!
b handle_fail
handle_fail:
err
`;
}
Summary
The above provides a simpler technique to protect contract funds as compared to a standard Algorand Smart Contract (Application).
The latter requires Application optins, increased minimum balances and global/local state issues, i.e. more to deal with.
This Smart Signature technique has been employed at https://www.sigsend.com — a webapp that facilitates emailing/texting of Algorand assets.