Bounded values
A common situation developers find themselves in is their contract needs to take a value that must been within a certain bound.
For example, a fee rate should be within the range of 0~1. It doesn’t make sense to charge more than 100% fee. Whenever a fee rate is provided, the contract needs to verify it’s within the bounds, throwing error if not:
#![allow(unused)]
fn main() {
#[grug::derive(Serde)]
struct InstantiateMsg {
pub fee_rate: Udec256,
}
#[grug::export]
fn instantiate(ctx: MutableCtx, msg: InstantiateMsg) -> anyhow::Result<Response> {
ensure!(
Udec256::ZERO <= fee_rate && fee_rate < Udec256::ONE,
"fee rate is out of bounds"
);
Ok(Response::new())
}
}
We call this an imperative approach for working with bounded values.
The problem with this is that the declaration and validation of fee_rate are in two places, often in two separate files. Sometimes developers simply forget to do the validation.
Instead, Grug encourages a declarative approach. We declare the valid range of a value at the time we define it, utilizing the Bounded type and Bounds trait:
#![allow(unused)]
fn main() {
use grug::{Bounded, Bounds};
use std::ops::Bound;
struct FeeRateBounds;
impl Bounds<Udec256> for FeeRateBounds {
const MIN: Bound<Udec256> = Bound::Inclusive(Udec256::ZERO);
const MAX: Bound<Udec256> = Bound::Exclusive(Udec256::ONE);
}
type FeeRate = Bounded<Udec256, FeeRateBounds>;
#[grug::derive(Serde)]
struct InstantiateMsg {
pub fee_rate: FeeRate,
}
#[grug::export]
fn instantiate(ctx: MutableCtx, msg: InstantiateMsg) -> anyhow::Result<Response> {
// No need to validate the fee rate here.
// Its bounds are already verified when `msg` is deserialized!
Ok(Response::new())
}
}