#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct IcpRefillPolicyInput {
pub requested_amount_e8s: u64,
pub observed_xdr_permyriad_per_icp: Option<u64>,
pub active_for_key: bool,
pub cycles_funding_enabled: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IcpRefillPolicyRules {
pub max_refill_e8s_per_call: u64,
pub min_xdr_permyriad_per_icp: Option<u64>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IcpRefillPolicyViolation {
NotConfigured,
CyclesFundingDisabled,
AmountZero,
MaxRefillPerCall {
requested_e8s: u64,
max_e8s: u64,
},
RateUnavailable {
min_xdr_permyriad_per_icp: u64,
},
RateGateDenied {
observed_xdr_permyriad_per_icp: u64,
min_xdr_permyriad_per_icp: u64,
},
ConcurrentRefill,
}
pub const fn evaluate_manual_refill(
policy: Option<&IcpRefillPolicyRules>,
input: IcpRefillPolicyInput,
) -> Result<(), IcpRefillPolicyViolation> {
let Some(policy) = policy else {
return Err(IcpRefillPolicyViolation::NotConfigured);
};
evaluate_common(policy, input)
}
const fn evaluate_common(
policy: &IcpRefillPolicyRules,
input: IcpRefillPolicyInput,
) -> Result<(), IcpRefillPolicyViolation> {
if !input.cycles_funding_enabled {
return Err(IcpRefillPolicyViolation::CyclesFundingDisabled);
}
if input.requested_amount_e8s == 0 {
return Err(IcpRefillPolicyViolation::AmountZero);
}
if input.requested_amount_e8s > policy.max_refill_e8s_per_call {
return Err(IcpRefillPolicyViolation::MaxRefillPerCall {
requested_e8s: input.requested_amount_e8s,
max_e8s: policy.max_refill_e8s_per_call,
});
}
if input.active_for_key {
return Err(IcpRefillPolicyViolation::ConcurrentRefill);
}
if let Some(min_xdr_permyriad_per_icp) = policy.min_xdr_permyriad_per_icp {
match input.observed_xdr_permyriad_per_icp {
Some(observed_xdr_permyriad_per_icp)
if observed_xdr_permyriad_per_icp >= min_xdr_permyriad_per_icp => {}
Some(observed_xdr_permyriad_per_icp) => {
return Err(IcpRefillPolicyViolation::RateGateDenied {
observed_xdr_permyriad_per_icp,
min_xdr_permyriad_per_icp,
});
}
None => {
return Err(IcpRefillPolicyViolation::RateUnavailable {
min_xdr_permyriad_per_icp,
});
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn policy() -> IcpRefillPolicyRules {
IcpRefillPolicyRules {
max_refill_e8s_per_call: 100_000_000,
min_xdr_permyriad_per_icp: Some(40_000),
}
}
fn input() -> IcpRefillPolicyInput {
IcpRefillPolicyInput {
requested_amount_e8s: 50_000_000,
observed_xdr_permyriad_per_icp: Some(45_000),
active_for_key: false,
cycles_funding_enabled: true,
}
}
#[test]
fn manual_refill_allows_configured_request() {
evaluate_manual_refill(Some(&policy()), input()).expect("manual refill");
}
#[test]
fn refill_denies_amount_above_cap() {
let mut input = input();
input.requested_amount_e8s = 100_000_001;
let err = evaluate_manual_refill(Some(&policy()), input).expect_err("cap violation");
assert_eq!(
err,
IcpRefillPolicyViolation::MaxRefillPerCall {
requested_e8s: 100_000_001,
max_e8s: 100_000_000,
}
);
}
#[test]
fn refill_denies_missing_rate_when_gate_configured() {
let mut input = input();
input.observed_xdr_permyriad_per_icp = None;
let err = evaluate_manual_refill(Some(&policy()), input).expect_err("rate required");
assert_eq!(
err,
IcpRefillPolicyViolation::RateUnavailable {
min_xdr_permyriad_per_icp: 40_000,
}
);
}
#[test]
fn refill_denies_low_rate() {
let mut input = input();
input.observed_xdr_permyriad_per_icp = Some(39_999);
let err = evaluate_manual_refill(Some(&policy()), input).expect_err("rate too low");
assert_eq!(
err,
IcpRefillPolicyViolation::RateGateDenied {
observed_xdr_permyriad_per_icp: 39_999,
min_xdr_permyriad_per_icp: 40_000,
}
);
}
#[test]
fn refill_denies_concurrent_key() {
let mut input = input();
input.active_for_key = true;
let err = evaluate_manual_refill(Some(&policy()), input).expect_err("concurrent refill");
assert_eq!(err, IcpRefillPolicyViolation::ConcurrentRefill);
}
#[test]
fn manual_refill_denies_when_cycles_funding_disabled() {
let mut input = input();
input.cycles_funding_enabled = false;
let err = evaluate_manual_refill(Some(&policy()), input).expect_err("kill switch");
assert_eq!(err, IcpRefillPolicyViolation::CyclesFundingDisabled);
}
}