The home for Hyperlane core contracts, sdk packages, and other infrastructure
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
hyperlane-monorepo/rust/sealevel/programs/hyperlane-sealevel-igp/src/accounts.rs

342 lines
11 KiB

//! Interchain gas paymaster accounts.
use std::{cmp::Ordering, collections::HashMap};
use access_control::AccessControl;
use account_utils::{AccountData, DiscriminatorData, DiscriminatorPrefixed, SizedData};
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{clock::Slot, program_error::ProgramError, pubkey::Pubkey};
use hyperlane_core::{H256, U256};
use crate::error::Error;
/// The scale for token exchange rates, i.e. a token exchange rate of 1.0 is
/// represented as 10^19.
pub const TOKEN_EXCHANGE_RATE_SCALE: u128 = 10u128.pow(19);
/// The number of decimals for the native SOL token.
pub const SOL_DECIMALS: u8 = 9;
#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "camelCase"))]
/// Types of IGPs that exist.
pub enum InterchainGasPaymasterType {
/// An IGP with gas oracles and that receives lamports as payment.
Igp(Pubkey),
/// An overhead IGP that points to an inner IGP and imposes a gas overhead for each destination domain.
OverheadIgp(Pubkey),
}
impl InterchainGasPaymasterType {
/// Returns the key for the IGP.
pub fn key(&self) -> &Pubkey {
match self {
InterchainGasPaymasterType::Igp(key) => key,
InterchainGasPaymasterType::OverheadIgp(key) => key,
}
}
}
/// A gas oracle that provides gas data for a remote chain.
#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "camelCase"))]
pub enum GasOracle {
/// Remote gas data stored directly in the variant data.
RemoteGasData(RemoteGasData),
// Future gas oracle variants could include a Pyth type, generalized CPI type, etc.
}
impl Default for GasOracle {
fn default() -> Self {
GasOracle::RemoteGasData(RemoteGasData::default())
}
}
/// The account for the program's global data.
pub type ProgramDataAccount = AccountData<DiscriminatorPrefixed<ProgramData>>;
impl DiscriminatorData for ProgramData {
const DISCRIMINATOR: [u8; 8] = *b"PRGMDATA";
}
/// A singleton account that stores the program's global data.
#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Default)]
pub struct ProgramData {
/// The bump seed for the program data PDA.
pub bump_seed: u8,
/// The number of gas payments made by in the program.
pub payment_count: u64,
}
impl SizedData for ProgramData {
fn size(&self) -> usize {
// 1 for bump_seed
// 8 for payment_count
1 + 8
}
}
/// An overhead IGP account.
pub type OverheadIgpAccount = AccountData<DiscriminatorPrefixed<OverheadIgp>>;
impl DiscriminatorData for OverheadIgp {
const DISCRIMINATOR: [u8; 8] = *b"OVRHDIGP";
}
/// Overhead IGP account data, intended to be configured with gas overheads
/// to impose on application-specified gas payment amounts.
#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Default)]
pub struct OverheadIgp {
/// The bump seed for the overhead IGP PDA.
pub bump_seed: u8,
/// The salt used to derive the overhead IGP PDA.
pub salt: H256,
/// The owner of the overhead IGP.
pub owner: Option<Pubkey>,
/// The inner IGP account.
pub inner: Pubkey,
/// The gas overheads to impose on gas payments to each destination domain.
pub gas_overheads: HashMap<u32, u64>,
}
impl OverheadIgp {
/// Returns the gas overhead to impose on gas payments to the given
/// destination domain. Defaults to 0 if a gas overhead is not set for the domain.
pub fn gas_overhead(&self, destination_domain: u32) -> u64 {
self.gas_overheads
.get(&destination_domain)
.copied()
.unwrap_or(0)
}
/// Quotes a gas payment, considering the gas overhead if one is present.
#[allow(unused)]
pub fn quote_gas_payment(
&self,
destination_domain: u32,
gas_amount: u64,
inner_igp: &Igp,
) -> Result<u64, Error> {
let total_gas_amount = self.gas_overhead(destination_domain) + gas_amount;
inner_igp.quote_gas_payment(destination_domain, total_gas_amount)
}
}
impl AccessControl for OverheadIgp {
fn owner(&self) -> Option<&Pubkey> {
self.owner.as_ref()
}
fn set_owner(&mut self, new_owner: Option<Pubkey>) -> Result<(), ProgramError> {
self.owner = new_owner;
Ok(())
}
}
impl SizedData for OverheadIgp {
fn size(&self) -> usize {
// 1 for bump_seed
// 32 for salt
// 33 for owner (1 byte Option, 32 bytes for pubkey)
// 32 for inner
// 4 for gas_overheads.len()
// N * (4 + 8) for gas_overhead contents
1 + 32 + 33 + 32 + 4 + (self.gas_overheads.len() * (4 + 8))
}
}
/// An IGP account.
pub type IgpAccount = AccountData<DiscriminatorPrefixed<Igp>>;
impl DiscriminatorData for Igp {
const DISCRIMINATOR: [u8; 8] = *b"IGP_____";
}
/// IGP account data.
#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Default)]
pub struct Igp {
/// The bump seed for the IGP PDA.
pub bump_seed: u8,
/// The salt used to derive the IGP PDA.
pub salt: H256,
/// The owner of the IGP.
pub owner: Option<Pubkey>,
/// The beneficiary of the IGP.
pub beneficiary: Pubkey,
/// The gas oracles for each destination domain.
pub gas_oracles: HashMap<u32, GasOracle>,
}
impl SizedData for Igp {
fn size(&self) -> usize {
// 1 for bump_seed
// 32 for salt
// 33 for owner (1 byte Option, 32 bytes for pubkey)
// 32 for beneficiary
// 4 for gas_oracles.len()
// M * (4 + (1 + 257)) for gas_oracles contents
1 + 32 + 33 + 32 + 4 + (self.gas_oracles.len() * (1 + 257))
}
}
impl Igp {
/// Quotes a gas payment.
/// Returns an error if a gas oracle is not set for the destination domain.
pub fn quote_gas_payment(
&self,
destination_domain: u32,
gas_amount: u64,
) -> Result<u64, Error> {
let oracle = self
.gas_oracles
.get(&destination_domain)
.ok_or(Error::NoGasOracleSetForDestinationDomain)?;
let GasOracle::RemoteGasData(RemoteGasData {
token_exchange_rate,
gas_price,
token_decimals,
}) = oracle;
// Arithmetic is done using U256 to avoid overflows.
// The total cost quoted in the destination chain's native token.
let destination_gas_cost = U256::from(gas_amount) * U256::from(*gas_price);
// Convert to the local native token (decimals not yet accounted for).
let origin_cost = (destination_gas_cost * U256::from(*token_exchange_rate))
/ U256::from(TOKEN_EXCHANGE_RATE_SCALE);
// Convert from the remote token's decimals to the local token's decimals.
let origin_cost = convert_decimals(origin_cost, *token_decimals, SOL_DECIMALS);
// Panics if an overflow occurs.
Ok(origin_cost.as_u64())
}
}
impl AccessControl for Igp {
fn owner(&self) -> Option<&Pubkey> {
self.owner.as_ref()
}
fn set_owner(&mut self, new_owner: Option<Pubkey>) -> Result<(), ProgramError> {
self.owner = new_owner;
Ok(())
}
}
/// Remote gas data.
#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct RemoteGasData {
/// The token exchange rate for the remote token, adjusted by the
/// TOKEN_EXCHANGE_RATE_SCALE.
/// If this e.g. 0.2, then one local token would give you 5 remote tokens.
#[cfg_attr(feature = "serde", serde(with = "hyperlane_core::utils::serde_u128"))]
pub token_exchange_rate: u128,
/// The gas price for the remote chain.
#[cfg_attr(feature = "serde", serde(with = "hyperlane_core::utils::serde_u128"))]
pub gas_price: u128,
/// The number of decimals for the remote token.
pub token_decimals: u8,
}
/// A discriminator used to easily identify gas payment accounts.
/// This is the first 8 bytes of the account data.
pub const GAS_PAYMENT_DISCRIMINATOR: &[u8; 8] = b"GASPAYMT";
/// A gas payment account, relating to a single gas payment.
pub type GasPaymentAccount = AccountData<GasPayment>;
/// Gas payment account data, prefixed with a discriminator.
pub type GasPayment = DiscriminatorPrefixed<GasPaymentData>;
impl DiscriminatorData for GasPaymentData {
const DISCRIMINATOR: [u8; 8] = *GAS_PAYMENT_DISCRIMINATOR;
}
/// Gas payment account data.
#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Default)]
pub struct GasPaymentData {
/// The sequence number of the gas payment.
pub sequence_number: u64,
/// The IGP that the gas payment is for.
pub igp: Pubkey,
/// The destination domain of the gas payment.
pub destination_domain: u32,
/// The message ID of the gas payment.
pub message_id: H256,
/// The amount of gas provided.
pub gas_amount: u64,
/// The amount of lamports quoted and paid.
pub payment: u64,
/// The unique gas payment pubkey.
pub unique_gas_payment_pubkey: Pubkey,
/// The slot of the gas payment.
pub slot: Slot,
}
impl SizedData for GasPaymentData {
fn size(&self) -> usize {
// 8 for sequence_number
// 32 for igp
// 4 for destination_domain
// 32 for message_id
// 8 for gas_amount
// 32 for unique_gas_payment_pubkey
// 8 for slot
8 + 32 + 4 + 32 + 8 + 8 + 32 + 8
}
}
/// Converts `num` from `from_decimals` to `to_decimals`.
fn convert_decimals(num: U256, from_decimals: u8, to_decimals: u8) -> U256 {
match from_decimals.cmp(&to_decimals) {
Ordering::Greater => num / U256::from(10u64).pow(U256::from(from_decimals - to_decimals)),
Ordering::Less => num * U256::from(10u64).pow(U256::from(to_decimals - from_decimals)),
Ordering::Equal => num,
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_convert_decimals() {
let num = U256::from(1000000u128);
let from_decimals = 9;
let to_decimals = 9;
let result = convert_decimals(num, from_decimals, to_decimals);
assert_eq!(result, num);
let num = U256::from(1000000000000000u128);
let from_decimals = 18;
let to_decimals = 9;
let result = convert_decimals(num, from_decimals, to_decimals);
assert_eq!(result, U256::from(1000000u128));
let num = U256::from(1000000u128);
let from_decimals = 4;
let to_decimals = 9;
let result = convert_decimals(num, from_decimals, to_decimals);
assert_eq!(result, U256::from(100000000000u128));
// Some loss of precision
let num = U256::from(9999999u128);
let from_decimals = 9;
let to_decimals = 4;
let result = convert_decimals(num, from_decimals, to_decimals);
assert_eq!(result, U256::from(99u128));
// Total loss of precision
let num = U256::from(999u128);
let from_decimals = 9;
let to_decimals = 4;
let result = convert_decimals(num, from_decimals, to_decimals);
assert_eq!(result, U256::from(0u128));
}
}