This tutorial provides a step-by-step guide on how to implement secure, robust, and efficient smart contracts on the Vara Network using Rust. By the end of this tutorial, you will be familiar with the basics of creating and handling custom events and errors, and managing state transitions and validations in your smart contracts.
Define custom events and errors that your smart contract will use:
#[derive(Encode, Decode, TypeInfo, Debug)]
#[codec(crate = "gstd::codec")]
#[scale_info(crate = "gstd::scale_info")]
pub enum Event {
FirstCustomEvent {
first_field: Id,
second_field: ActorId,
third_field: u128,
},
SecondCustomEvent {
first_field: Id,
second_field: ActorId,
},
}
#[derive(Debug, Clone, Encode, Decode, TypeInfo)]
#[codec(crate = "gstd::codec")]
#[scale_info(crate = "gstd::scale_info")]
pub enum Error {
Unauthorized,
UnexpectedFTEvent,
MessageSendError,
NotFound,
IdNotFoundInAddress,
IdNotFound,
NotAdmin,
NotEnoughBalance
}Implement the Handle function using InOut<Action, Result<Event, Error>> to manage inputs and outputs of actions in your smart contract. The InOut structure facilitates the management of interactions, allowing a clear handling of the flow of actions and their possible outcomes.
The code below illustrates how to set up the program metadata, specifying the types used for initialization, handling actions, and other relevant aspects of the contract:
pub struct ProgramMetadata;
impl Metadata for ProgramMetadata {
// Type used for contract initialization
type Init = In<Init>;
// Define 'Handle' to manage actions and results
type Handle = InOut<Action, Result<Event, Error>>; // Handle the output with `Result<Event, Error>`
// Other types required by the contract, defined as empty if not used
type Others = ();
type Reply = ();
type Signal = ();
type State = ();
}The Handle function plays a crucial role in managing the behavior of your smart contract by processing incoming actions and generating appropriate outputs. Below is a practical example of how to implement the Handle function to handle outputs efficiently:
let action: Action = msg::load().expect("Could not load Action");
let state = unsafe {
STATE.as_mut().expect("Unexpected error in state")
};
let result = match action {
Action::FirstAction { input } => {
state.first_method(input).await
},
Action::SecondAction { input } => {
state.second_method(input)
}
};
msg::reply(result, 0).expect("Failed to encode or reply with `Result<Event, Error>`");In this step, you will define specific functions within your smart contract to handle actions. These functions will perform validations, update the state, and potentially generate events. Below is an example of how to implement a function that handles a specific action and returns a Result<Event, Error>:
impl State {
fn first_method(&mut self, amount: u128) -> Result<Event, Error> { } // the output with `Result<Event, Error>`
// More methods...
}Implementing robust input validations is crucial for maintaining the integrity and security of your smart contract. These validations ensure that only valid and authorized actions are processed. Below is an example of adding input validations within a specific function implementation that returns Result<Event, Error>:
fn first_method(&mut self, amount: u128) -> Result<Event, Error> {
// Validations
let source = msg::source();
if self.balances.get(&source).unwrap_or(&0) < &amount {
return Err(Error::NotEnoughBalance);
}
// Additional logic and actions follow...
}In this step, you will learn how to implement a consistent framework for handling validations, state transitions, and events in your smart contract. Consistency in these areas ensures that the contract functions reliably and securely. Below is an example of how to structure these elements within a function:
fn first_method(&mut self, amount: u128) -> Result<Event, Error> {
// Validations
let source = msg::source();
if self.balances.get(&source).unwrap_or(&0) < &amount {
return Err(Error::NotEnoughBalance);
}
// Transition State in Main State
self.balances
.entry(source)
.and_modify(|balance| *balance -= amount);
self.current_supply -= amount;
self.total_supply -= amount;
// Generate event
Ok(Event::FirstCustomEvent {
first_field: info_first_field,
second_field: info_second_field,
third_field: info_third_field,
})
}Admin validations are a crucial security feature for functions in smart contracts that should be restricted to authorized users only. This step involves adding checks to ensure that only admins can execute certain actions. Below is an example of how to integrate admin validations into a function:
fn first_method(&mut self, amount: u128) -> Result<Event, Error> {
// Admin Validations
let source = msg::source();
if !self.admins.contains(&source) {
return Err(Error::NotAdmin);
}
// Additional implementation logic follows...
}This step involves implementing a system to query and reply to the state in your smart contract. This is essential for interactions that require state inspection without modifying it.
Start by defining enums for the types of queries your contract can handle and their corresponding replies:
#[derive(Encode, Decode, TypeInfo)]
#[codec(crate = "gstd::codec")]
#[scale_info(crate = "gstd::scale_info")]
pub enum Query {
ExampleQuery,
}
#[derive(Encode, Decode, TypeInfo)]
#[codec(crate = "gstd::codec")]
#[scale_info(crate = "gstd::scale_info")]
pub enum QueryReply {
ExampleQueryReply(String),
}Incorporate query handling into your smart contract by defining InOut<Query, QueryReply> for the State in your ProgramMetadata. This allows your contract to process queries and return the corresponding results efficiently.
Modify the ProgramMetadata structure to include the State type, which will handle both incoming queries and their replies:
pub struct ProgramMetadata;
impl Metadata for ProgramMetadata {
type Init = In<Init>;
type Handle = InOut<Action, Result<Event, Error>>; // Handle actions and their outcomes with `Result<Event, Error>`
type Others = ();
type Reply = ();
type Signal = ();
type State = InOut<Query, QueryReply>; // Manage queries and their replies
}Integrate query handling functionality to manage and respond to state inquiries within your smart contract. This involves setting up a function that can receive queries, process them, and send back appropriate replies.
The function state() is designated to handle queries. Here’s how it is implemented:
#[no_mangle]
extern "C" fn state() {
let state = unsafe {
STATE
.as_ref()
.expect("Unexpected: Error in getting contract state")
};
let query: Query = msg::load().expect("Unable to decode the query");
let reply = match query {
Query::ExampleQuery => QueryReply::ExampleQueryReply(state.field.clone()),
};
msg::reply(reply, 0).expect("Error on sharing state");
}This tutorial has guided you through the essential steps for building secure smart contracts with Rust on the Vara Network. We've explored everything from initializing enums to adding complex validations and admin controls. These foundations not only enhance your smart contract's functionality but also fortify its security.
As you continue to develop in the blockchain space, remember to stay updated with the latest practices and always test thoroughly. Keep building, learning, and contributing to a safer and more robust blockchain ecosystem.