Example Crowdfunding Stateful Smart Contract Application
Overview
CrowdFunding applications have become very popular in many ways across the global economy. They are used to source for charities, cover legal fees, fund non-profits, and invest in new products and services. The primary issue with many crowdfunding applications is lack of transparency or decentralization, making it an ideal blockchain candidate. This solution walks through the creation of a simple crowdfunding application on the Algorand blockchain.
This application will involve using many of the Algorand technologies, including stateless and stateful smart contracts, atomic transfers, timed deadlines, and payment transactions. This article will begin with an overview of the application design and then go into the details on how to build the application on Algorand.
Design Overview
Crowdfunding applications create a fund drive for a specific receiver that tries to reach a goal before a particular date. Individuals are allowed to donate to the fund up to a specific date. The receiver can claim the funds if the goal is achieved. If not, the original givers can recover the donations.
To create this type of application on Algorand, there are five steps that must be supported.
-
Create the Fund - Anyone should be able to create a fund, setting a begin, end, and closeout dates. The creator and receiver should also be stored. The creator is stored only to allow that address to modify or delete the smart contract.
1a. Update - As part of linking the escrow account to the stateful smart contract, you first need the application id returned from creating the stateful smart contract. The escrow account code should be modified to add the specific returned ID and compiled, returning the escrow address. This address should be placed in the global state using an update operation of the stateful smart contract. The escrow account will hold all donations.
-
Donate - Donations are accepted after the beginning date and not after the end date. The escrow account accrues all donations.
- Withdraw Funds - The fund’s recipient should be able to withdraw the funds from the escrow account after the end date if the fund goal is met.
- Recover Donations - Original donors should be allowed to recover their donations after the end date if the fund did not make its goal.
- Delete Fund - The fund should be deletable by the original creator after the fund close date. It must be accompanied by a closeout transaction from the escrow to the receiver if the escrow contains funds. This gives any unclaimed funds to the receiver of the fund, including non-recovered donations.
Info
This solution solely makes use of goal
to make the application calls against the stateful application. The SDKs also provide the same functionality and can be used instead of goal
.
Escrow Account
From the design above, you can see the use of a common pattern of grouping transaction calls atomically. These groups are generally composed of a call to the stateful smart contract and a payment or asset transaction. If either call fails, both will fail. The escrow account, which is a stateless smart contract, holds all donations. With escrow accounts, any account can send Algos or Assets to the escrow, and the logic determines when the funds can be spent from the escrow. The crowdfunding application only allows funds to be spent from the escrow if the payment transaction is grouped with a call to our specific stateful smart contract and that call returns True. In the code below, the application ID is set to 1, but this code should be changed to the application ID of whatever is returned from step 1, described below. Step 1-a illustrates storing the escrow address in the stateful smart contract.
#pragma version 2
// deploy app first then get id
// replace id in this teal to create
// the escrow address
// use goal app update to set the
// escrow address
// This contract only spends out
// it two transactions are grouped
global GroupSize
int 2
==
// The first transaction must be
// an ApplicationCall (ie call stateful smart contract)
gtxn 0 TypeEnum
int 6
==
&&
// The specific App ID must be called
// This should be changed after creation
gtxn 0 ApplicationID
int 1
==
&&
// The application call must either be
// A general applicaiton call or a delete
// call
gtxn 0 OnCompletion
int NoOp
==
// Delete must empty the escrow if it has any funds
int DeleteApplication
gtxn 0 OnCompletion
==
||
&&
// verify neither transaction
// contains a rekey
gtxn 1 RekeyTo
global ZeroAddress
==
&&
gtxn 0 RekeyTo
global ZeroAddress
==
&&
When the above code is compiled, it produces an Algorand Address that can then accept funds.
Application Creation - Step 1
The goal
command-line tool provides a set of application-related commands to manipulate applications. The goal app create
command creates the application. This is a specific application transaction to the blockchain, and similar to the way Algorand Assets work, it will return an application ID. Several parameters are passed to the creation method. These parameters primarily revolve around how much storage the application uses. In stateful smart contracts, you specify storage as either global or local. Global storage represents the amount of space that is available to the application itself, and local storage represents the amount of space in every account’s balance record that will be used by the application per account.
The crowdfunding application uses eight global variables ( three byte slices and five integers) and one local storage variable (integer). The byte slices store the addresses. The global integers store the timestamps for the dates, the fund goal, and the current fund total. The local integer stores the amount a specific user gives.
$ goal app create --creator {ACCOUNT} --approval-prog ./crowd_fund.teal --global-byteslices 3 --global-ints 5 --local-byteslices 0 --local-ints 1 --app-arg “int:begindatetimestamp” --app-arg "int:enddatetimespamp" --app-arg "int:1000000" --app-arg "addr:"{ACCOUNT} --app-arg "int:fundclosedatetimestamp" --clear-prog ./crowd_fund_close.teal -d ~/node/data
The smart contract is composed of two programs passed at creation. A set of parameters is also used in the create method to configure the contract. The parameters set the global state variables. So in the above example, three required dates, the fund goal, and the creator address, are passed in. See Passing Arguments to a Stateful Application for more details on passing arguments. For more information on creating a stateful smart contract, see Creating the Smart Contract.
The stateful smart contract has the following section of code to handle the creation of the smart contract. This code handles storing the key global variables and setting up the receiver of the fund and the creator of the application.
//Approval Program
//if the app id is 0 then it is being created
int 0
txn ApplicationID
==
// if not creation skip this section
bz not_creation
// save the creator address to the global state
byte "Creator"
txn Sender
app_global_put
// verify that 5 arguments are passed
txn NumAppArgs
int 5
==
bz failed
//store the start date
byte "StartDate"
txna ApplicationArgs 0
btoi
app_global_put
// store the end date
byte "EndDate"
txna ApplicationArgs 1
btoi
app_global_put
// store fund goal
byte "Goal"
txna ApplicationArgs 2
btoi
app_global_put
// store the fund receiver
byte "Receiver"
txna ApplicationArgs 3
app_global_put
// set the total raised to 0 and store it
byte "Total"
int 0
app_global_put
// store the fund close date
byte "FundCloseDate"
txna ApplicationArgs 4
btoi
app_global_put
// return a success
int 1
return
not_creation:
The goal app create
command will return an application ID that can be used to make calls to the stateful smart contract. All future calls to the smart contract should use this ID.
For more information on creating stateful smart contracts, see Creating the Smart Contract.
Application Update - Step 1a
Warning
An update application transaction can be used to modify the source code of a stateful smart contract, which can be dangerous. If this is not desired behavior, include code that returns a failure on this type of application call transaction.
The smart contracts global state is modified to add the escrow account during the update operation. The contract performs the following additional operations.
- Verifies that the creator of the stateful smart contract is making the update call.
- Verifies that one parameter is passed in, which should be the escrow address.
- Stores the address in the application’s global state.
// check if this is update ---
int UpdateApplication
txn OnCompletion
==
bz not_update
// verify that the creator is
// making the call
byte "Creator"
app_global_get
txn Sender
==
// the call should pass the escrow
// address
txn NumAppArgs
int 1
==
&&
bz failed
// store the address in global state
// this parameter should be addr:
byte "Escrow"
txna ApplicationArgs 0
app_global_put
int 1
return
not_update:
The goal
command should be similar to the following.
$ goal app update --app-id={APPID} --from {ACCOUNT} --approval-prog ./crowd_fund.teal --clear-prog ./crowd_fund_close.teal --app-arg "addr:F4HJHVIPILZN3BISEVKXL4NSASZB4LRB25H4WCSEENSPCJ5DYW6CKUVZOA"
Note that the code for the contract did not change. The update operation links the two contracts.
For more information on updating a stateful smart contract see Update Stateful Smart Contract.
Application Optin and Donate - Step 2
Before a stateful smart contract can use any local state, an account must first opt into the application. Note that it is not required if the stateful smart contract does not use any local state. The goal
command to opt into a specific application is shown below.
$ goal app optin --app-id {APPID} --from {ACCOUNT}
In the crowdfunding application, every transaction type is checked in the code to verify proper calls. The developer documentation explains the different transaction types. The opt-in call happens to be the last one checked in this example. The code performs the following:
- Verifies application arguments have been passed.
- If no arguments, assumes an account is just opting into the application.
- Checks the transaction type to verify it is opt-in.
If an account donates and opts into the application in one call, this code will skip directly to the check_parms
label and the parameters will be processed.
// check if no params are
// passed in, which should
// only happen when someone just
// wants to optin
// note the code is written
// to allow opting in and donating with
// one call
int 0
txn NumAppArgs
==
bz check_parms
// Verify someone is
// not just opting in
int OptIn
txn OnCompletion
==
bz failed
int 1
return
check_parms:
The check_params
code handles donating, claiming, or reclaiming funds. The code block for check_params
branches based on the application argument passed into the call. The smart contract only supports the argument donate
, reclaim,
or claim.
If the argument does not specify one of these, the smart contract call will fail.
check_parms:
// donate
txna ApplicationArgs 0
byte "donate"
==
bnz donate
// reclaim
txna ApplicationArgs 0
byte "reclaim"
==
bnz reclaim
// claim
txna ApplicationArgs 0
byte "claim"
==
bnz claim
b failed
For donating, the stateful smart contract performs the following:
- Ensures the donation is within the beginning and ending dates of the fund.
- Verifies that this is a grouped transaction with the second one being a payment to the escrow.
- Increments and stores the global total amount.
- Stores the given amount in the local storage of the giver.
donate:
// check dates to verify
// in valid range
global LatestTimestamp
byte "StartDate"
app_global_get
>=
global LatestTimestamp
byte "EndDate"
app_global_get
<=
&&
bz failed
// check if grouped with
// two transactions
global GroupSize
int 2
==
// second tx is an payment
gtxn 1 TypeEnum
int 1
==
&&
bz failed
// verify escrow is receiving
// second payment tx
byte "Escrow"
app_global_get
gtxn 1 Receiver
==
bz failed
// increment the total
// funds raised so far
byte "Total"
app_global_get
gtxn 1 Amount
+
store 1
byte "Total"
load 1
app_global_put
// increment or set giving amount
// for the account that is donating
int 0 //sender
txn ApplicationID
byte "MyAmountGiven"
app_local_get_ex
// check if a new giver
// or existing giver
// and store the value
// in the givers local storage
bz new_giver
gtxn 1 Amount
+
store 3
int 0 //sender
byte "MyAmountGiven"
load 3
app_local_put
b finished
new_giver:
int 0 // sender
byte "MyAmountGiven"
gtxn 1 Amount
app_local_put
b finished
The donation operation requires that two transactions be grouped. The first transaction is the stateful TEAL call, and the second is a payment transaction to the escrow fund. The goal command to make the donation calls should look similar to the following. The stateful TEAL call is passed with a string parameter containing the word “donate”.
$ goal app call --app-id {APPID} --app-arg "str:donate" --from={ACCOUNT} --out=unsignedtransaction1.tx -d ~/node/data
$ goal clerk send --from={ACCOUNT} --to="F4HJHVIPILZN3BISEVKXL4NSASZB4LRB25H4WCSEENSPCJ5DYW6CKUVZOA" --amount=500000 --out=unsignedtransaction2.tx -d ~/node/data
$ cat unsignedtransaction1.tx unsignedtransaction2.tx > combinedtransactions.tx
$ goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx
$ goal clerk sign -i groupedtransactions.tx -o signout.tx
$ goal clerk rawsend -f signout.tx
For more information on grouping transactions, see Atomic Transfers.
Application Call Withdraw Funds - Step 3
Once the fund end date has passed, the receiver of the fund can claim the funds if the fund goal is met. To do this, the receiver must submit a payment transaction from the escrow to their account. This payment transaction is grouped with a call to the stateful TEAL application claiming the fund. The payment transaction also should use the closeRemainderTo
transaction property to close all funds in the escrow account to the receiver.
The first transaction must be a call to the stateful smart contract passing the string “claim” as an argument. The second transaction should be a payment transaction from the escrow TEAL program to the receiver. It should have an amount of 0 and should set the --close-to
attribute to the receiver of the fund. This will empty the escrow fund into the receiver’s account.
$ goal app call --app-id {APPID} --app-arg "str:claim" --from {ACCOUNT} --out=unsignedtransaction1.tx -d ~/node/data
$ goal clerk send --to={ACCOUNT} --close-to={ACCOUNT} --from-program=./crowd_fund_escrow.teal --amount=0 --out=unsignedtransaction2.tx -d ~/node/data
cat unsignedtransaction1.tx unsignedtransaction2.tx > combinedtransactions.tx
$ goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx
$ goal clerk split -i groupedtransactions.tx -o split.tx
$ goal} clerk sign -i split-0.tx -o signout-0.tx
cat signout-0.tx split-1.tx > signout.tx
$ goal clerk rawsend -f signout.tx
In the goal commands above, the second transaction is not signed as it is a stateless TEAL contract, and the logic handles signing the transaction.
The stateful smart contract processes this transaction by performing several checks.
- There are two transactions in the group.
- The sender in the payment transaction is the escrow account.
- The receiver of the payment transaction is the receiver that was set when the fund was created.
- The payment transaction uses
CloseRemainderTo
to close out the escrow. - The fund end date has passed.
- The fund goal was met.
claim:
// verify there are 2 transactions
// in the group
global GroupSize
int 2
==
bz failed
// verify that the receiver
// of the payment transaction
// is the address stored
// when the fund was created
gtxn 1 Receiver
byte "Receiver"
app_global_get
==
// verify the sender
// of the payment transaction
// is the escrow account
gtxn 1 Sender
byte "Escrow"
app_global_get
==
&&
// verify that the CloseRemainderTo
// attribute is set to the receiver
gtxn 1 CloseRemainderTo
byte "Receiver"
app_global_get
==
&&
// verify that the fund end date
// has passed
global LatestTimestamp
byte "EndDate"
app_global_get
>
&&
bz failed
// verify that the goal was reached
byte "Total"
app_global_get
byte "Goal"
app_global_get
>=
bz failed
b finished
Application Call Reclaim Funds - Step 4
If the fund goal is not met, the original givers need to recover their donations. This operation requires that two transactions be grouped. The first transaction is a call to the stateful smart contract, passing the argument “reclaim”. The second transaction is a payment transaction from the escrow to the original giver. The amount of the payment transaction should be the amount given minus the transaction fee, as the escrow will pay this fee.
# app account is the escrow
$ goal app call --app-id {APPID} --app-account=F4HJHVIPILZN3BISEVKXL4NSASZB4LRB25H4WCSEENSPCJ5DYW6CKUVZOA --app-arg "str:reclaim" --from {ACCOUNT} --out=unsignedtransaction1.tx
# note that the reclaim has to account for the tx fee hence why the amount does not match the donation
$ goal clerk send --to={ACCOUNT} --close-to={ACCOUNT} --from-program=./crowd_fund_escrow.teal --amount=499000 --out=unsignedtransaction2.tx
$ cat unsignedtransaction1.tx unsignedtransaction2.tx > combinedtransactions.tx
$ goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx
$ goal clerk split -i groupedtransactions.tx -o split.tx
$ goal clerk sign -i split-0.tx -o signout-0.tx
$ cat signout-0.tx split-1.tx > signout.tx
$ goal clerk rawsend -f signout.tx
In the goal commands above, the second transaction is not signed as it is a stateless TEAL contract, and the logic handles signing the transaction.
The stateful smart contract processes this transaction by performing several checks and writes. This is a more complex operation as the giver does not have to recover their total amount and in fact, may decide to never recover their donations. Step 5 handles this situation. Also, if this is the last giver to recover their donations, the payment transaction must use the CloseRemainderTo
attribute to close out the escrow. This part of the smart contract checks for the following conditions.
- There are two transactions in the group.
- The smart contract caller is the payment transaction receiver.
- The sender of the payment transaction is the escrow
- The end date for the fund has passed
- The fund goal was missed.
- Add the payment transaction amount with the payment transaction fee and verify that it is equal to or less than the original given amount.
- Check to see if this is the last donation to recover. If so, verify
CloseRemainderTo
is set, which will close out the escrow.
reclaim:
// verify there are 2 transactions
// in the group
global GroupSize
int 2
==
bz failed
// verify that smart contract
// caller is the payment
// transction receiver
gtxn 1 Receiver
gtxn 0 Sender
==
// Verify that payment
// transaction is from the escrow
gtxn 1 Sender
byte "Escrow"
app_global_get
==
&&
// verify that fund end date has passed
global LatestTimestamp
byte "EndDate"
app_global_get
>
&&
// verify the fund goal was
// not met
byte "Total"
app_global_get
byte "Goal"
app_global_get
<
&&
// because the escrow
// has to pay the fee
// the amount of the
// payment transaction
// plus the fee should
// be less than or equal to
// the amount originally
// given
gtxn 1 Amount
gtxn 1 Fee
+
int 0
byte "MyAmountGiven"
app_local_get
<=
&&
bz failed
//check the escrow account total
//--app-account for the escrow
// needs to pass the address
// of the escrow
// check that this is the
// last recoverd donation
// if it is the closeremainderto
// should be set
gtxn 1 Fee
gtxn 1 Amount
+
// the int 1 is the ref to escrow
int 1
balance
==
gtxn 1 CloseRemainderTo
global ZeroAddress
==
||
bz failed
// decrement the given amount
// of the sender
int 0
byte "MyAmountGiven"
app_local_get
gtxn 1 Amount
-
gtxn 1 Fee
-
store 5
int 0
byte "MyAmountGiven"
load 5
app_local_put
b finished
Application Delete - Step 5
The original creator of the crowdfunding application should be able to delete the application after the fund close date. The fund should not be deletable unless the escrow account has been closed out as well. If the escrow still has funds in the account, the fund creator can send all the remaining funds to the fund receiver, whether the goal was met or not.
The goal commands for handling the delete operation if the escrow account is not empty requires two transactions. The first one should be a delete application transaction to the stateful smart contract, and the other should be a payment transaction from the escrow to the fund receiver. The amount of the payment transaction should be 0 and the CloseRemainderTo
attribute should be set to the receiver.
# pass in the escrow account to accounts array to check if it is empty
$ goal app delete --app-id {APPID} --from {ACCOUNT} --app-account=F4HJHVIPILZN3BISEVKXL4NSASZB4LRB25H4WCSEENSPCJ5DYW6CKUVZOA --out=unsignedtransaction1.tx
$ goal clerk send --from-program=./crowd_fund_escrow.teal --to={ACCOUNT} --amount=0 -c {ACCOUNT} --out=unsignedtransaction2.tx
$ cat unsignedtransaction1.tx unsignedtransaction2.tx > combinedtransactions.tx
$ goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx
$ goal clerk split -i groupedtransactions.tx -o split.tx
$ goal clerk sign -i split-0.tx -o signout-0.tx
$ cat signout-0.tx split-1.tx > signout.tx
$ goal clerk rawsend -f signout.tx
Any account can delete a smart contract, even if that account did not create the application. This may not be desirable behavior. The code can prevent this operation. The crowdfunding application only allows the creator to delete the smart contract.
The smart contract performs the following operations.
- Verifies this is a delete application call.
- Ensures the creator is trying to delete the application.
- Checks that the fund close date has passed.
- If the escrow is empty, allows the delete operation
- If the escrow is not empty, verifies that this is atomic transfer with two transactions.
- Verifies that the second transaction is a payment transaction.
- Verifies that the second transaction sets
CloseRemainderTo
to the receiver to close out the escrow. - Verifies the second transaction payment amount is set to 0.
- Verifies the escrow account is the sender of the second transaction.
// check if this is deletion transaction
int DeleteApplication
txn OnCompletion
==
bz not_deletion
// To delete the app
// The creator must empty the escrow
// to the fund receiver if the escrow has funds
// only the creator
// can delete the app
byte "Creator"
app_global_get
txn Sender
==
// check that we are past fund close date
global LatestTimestamp
byte "FundCloseDate"
app_global_get
>=
&&
bz failed
// if escrow balance is zero
// let the app be deleted
// escrow account must be passed
// into the call as an argument
int 0
int 1
balance
==
// if the balance is 0 allow the delete
bnz finished
// if the escrow is not empty then
// there must be need two transactions
// in a group
global GroupSize
int 2
==
// second tx is an payment
gtxn 1 TypeEnum
int 1
==
&&
// the second payment transaction should be
// a close out transaction to receiver
byte "Receiver"
app_global_get
gtxn 1 CloseRemainderTo
==
&&
// the amount of the payment transaction
// should be 0
gtxn 1 Amount
int 0
==
&&
// the sender of the payment transaction
// should be the escrow account
byte "Escrow"
app_global_get
gtxn 1 Sender
==
&&
bz failed
int 1
return
Conclusion
The crowdfunding application illustrates the use of many of Algorand’s layer-1 features to implement a complete application. The full source code for the application is available on Github.