Develop, build and deploy a Smart Contract on the Elrond Network
Prerequisites for your System (you can follow the links if you didn't install those tools yet):
Let's start by creating a new contract on the command line in a folder of your choice:
erdpy contract new crowdfunding --template empty
We go through every part of that first command.
Here is the official explanation of the erdpy CLI purpose:
Elrond Python Command Line Tools and SDK for interacting with the Elrond Network (in general) and Smart Contracts (in particular).
The command group contract is for building, deploying, upgrading, and interacting with Smart Contracts.
With new, we create a new Smart Contract based on a template.
After this, we specify the name of our new Smart Contract crowdfunding.
The last is an optional argument template with which we specify the template to use. We use empty which creates a Smart Contract with just the necessary base structure.
Please open up the newly created project in VS Code. You should see one folder in your Explorer crowdfunding that holds everything we need for creating our contract.
We will do some housekeeping first, and change the path value in the Cargo.toml file from “src/empty.rs” to “src/crowdfunding.rs” to declare that this file will contain the code for our Smart Contract. Now we need to rename the file in the src folder from empty.rs to crowdfunding.rs and replace the contents of the file with the following:
#![no_std]
elrond_wasm::imports!();
#[elrond_wasm::contract]
pub trait Crowdfunding {
#[init]
fn init(&self) {}
}
Let's go through does few lines.
Rust is not specially written for just Smart Contract development like Solidity. In the first line, we say that we don't wanna load the standard libraries to make our Smart Contract very lightweight.
The next line imports the Elrond Framework to simplify your Smart Contract.
With the next line, we say that the following is a Smart Contract. After that line, our Smart Contract starts with pub trait Crowdfunding
.
For what does the keyword trait stand for in Rust?
A trait defines functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way. We can use trait bounds to specify that a generic type can be any type that has certain behavior.
Note: Traits are similar to a feature often called interfaces in other languages, although with some differences.
The next two lines define the constructor method, which will only run once when you deploy the Smart Contract to the network. It must be annotated with #[init]
What are Smart Contract annotations?
Annotations (also known as Rust “attributes”) are the bread and butter of the
elrond-wasm
smart contract development framework.
You can dive deeper into the annotations Elrond has to offer here.
Let's build our new contract.
cd crowdfunding
erdpy contract build
This might take a while. If the command was successful you now see a new subfolder in the crowdfunding folder > output.
Storage
We define values that should be stored on-chain. We can achieve that by annotations. All methods defined for storage handling must not have an implementation.
The first annotation view defines a public endpoint to access the storage value. With storage_mapper we define a storage setter and getter and SingleValueMapper is the simplest storage mapper that manages only one storage key.
We need three on-chain storage keys first a configured desired target amount, and a deadline expressed as a block timestamp after which the contract cannot be funded, the deposit is the received funds it has an extra argument for the donor address.
The values for target and deposit are a sum of EGLD and will be stored as 1 EGLD = 10¹⁸ EGLD-wei, all payments are expressed in wei.
#[view(getTarget)]
#[storage_mapper("target")]
fn target(&self) -> SingleValueMapper<BigUint>;
#[view(getDeadline)]
#[storage_mapper("deadline")]
fn deadline(&self) -> SingleValueMapper<u64>;
#[view(getDeposit)]
#[storage_mapper("deposit")]
fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper<BigUint>;
Functions
Next, we will add two more arguments to the constructor of two of our storage fields and two checks that the target amount needs to be higher than 0 and the deadline must be in the future.
#[init]
fn init(&self, target: BigUint, deadline: u64) {
require!(target > 0, "Target must be more than 0");
self.target().set(target);
require!(
deadline > self.get_current_time(),
"Deadline can't be in the past"
);
self.deadline().set(deadline);
}fn get_current_time(&self) -> u64 {
self.blockchain().get_block_timestamp()
}
Next, we define a publicly accessible endpoint to keep track of who donated how much, it's a payable function that will receive EGLD.
#[endpoint]
#[payable("EGLD")]
fn fund(&self) {
let payment = self.call_value().egld_value();
let caller = self.blockchain().get_caller();
self.deposit(&caller).update(|deposit| *deposit += payment);
}
We add two more lines to our fund function, the new lines are marked bold. To prevent funding after the deadline has passed we reject all payments that come in after the defined deadline.
#[endpoint]
#[payable("EGLD")]
fn fund(&self) {
let payment = self.call_value().egld_value();
let current_time = self.blockchain().get_block_timstamp();
require!(current_time < self.deadline().get(), "cannot fund after deadline");
let caller = self.blockchain().get_caller();
self.deposit(&caller).update(|deposit| *deposit += payment);
}
We need to define an endpoint to read the actual status of the funding. These lines must be added outside of the contract.
The
#[derive]
keyword in Rust allows you to automatically implement certain traits for your type.
#[derive(TopEncode, TopDecode, TypeAbi, PartialEq, Clone, Copy)]
pub enum Status {
FundingPeriod,
Successful,
Failed,
}
We need to add another import to our file underneath the first import:
elrond_wasm::derive_imports!();
We add two more methods into our contract trait to receive the actual status and the current funding with getCurrentFunds.
#[view]
fn status(&self) -> Status {
if self.blockchain().get_block_timestamp() <= self.deadline().get() {
Status::FundingPeriod
} else if self.get_current_funds() >= self.target().get() {
Status::Successful
} else {
Status::Failed
}
}#[view(getCurrentFunds)]
fn get_current_funds(&self) -> BigUint {
self.blockchain().get_sc_balance(&TokenIdentifier::egld(), 0)
}
Finally, we need a function to claim the funding. In this function, we prevent claiming before the deadline is reached. If the funding was successful, the owner receives the current balance. If the funding failed only donors can claim their invested amounts.
#[endpoint]
fn claim(&self) {
match self.status() {
Status::FundingPeriod => sc_panic!("cannot claim before deadline"),
Status::Successful => {
let caller = self.blockchain().get_caller();
require!(
caller == self.blockchain().get_owner_address(),
"only owner can claim successful funding"
);
let sc_balance = self.get_current_funds();
self.send().direct_egld(&caller, &sc_balance);
},
Status::Failed => {
let caller = self.blockchain().get_caller();
let deposit = self.deposit(&caller).get();
if deposit > 0u32 {
self.deposit(&caller).clear();
self.send().direct_egld(&caller, &deposit);
}
},
}
}
You can see the final contract code here.
Deployment
First, we need to create an owner wallet on the Devnet here. Click on “Create Wallet”, and write down the security phrase (24 words) that can help us retrieve the wallet and the password for the JSON Keystore.
Save the security phrase (24 words) in a safe place.
Login into your Devnet Wallet and click on Faucet to get some funds. For sending transactions we need some xEGLD for the gas fees.
We generate a private key PEM file so we don't need to type in our password every time to confirm transactions.
cd crowdfunding
erdpy --verbose wallet derive ./wallet-owner.pem --mnemonic
You need to type in the just received security phrase of 24 words separated by whitespace.
Create a file in the crowdfunding folder erdpy.json and put the following content into it:
{
"configurations": {
"default": {
"proxy": "https://devnet-api.elrond.com",
"chainID": "D"
}
},
"contract": {
"deploy": {
"verbose": true,
"bytecode": "./output/crowdfunding.wasm",
"recall-nonce": true,
"pem": "./wallet-owner.pem",
"gas-limit": 60000000,
"arguments": [100000000000000000000, 130000046930],
"send": true,
"outfile": "deploy-testnet.interaction.json"
}
}
}
Build & Deploy the contract with the following commands:
erdpy contract build
erdpy contract deploy
Somewhere in the command line output, you will see a line like the following:
INFO:cli.contracts:Contract address: erd1qqqqqqqqqqqqqpgqfrxgju4fvxjrygagw52xamtmzmptex54rfyq40ns5r
Copy and paste the contract address into the search on the Devnet Explorer and see your contract details etc.
In an upcoming article, we will build the frontend for this Smart Contract. You can see a sneak preview below.