#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::unused_unit)]
pub mod attestations;
pub mod default_weights;
pub mod migrations;
#[cfg(any(feature = "mock", test))]
pub mod mock;
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
#[cfg(any(feature = "try-runtime", test))]
mod try_state;
mod access_control;
pub mod authorized_by;
#[cfg(test)]
mod tests;
pub use crate::{
access_control::AttestationAccessControl, attestations::AttestationDetails, default_weights::WeightInfo, pallet::*,
};
#[frame_support::pallet]
pub mod pallet {
use super::*;
use authorized_by::AuthorizedBy;
use frame_support::{
dispatch::{DispatchResult, DispatchResultWithPostInfo},
pallet_prelude::*,
traits::{
fungible::{Inspect, MutateHold},
Get, StorageVersion,
},
};
use frame_system::pallet_prelude::*;
use ctype::CtypeHashOf;
use kilt_support::{
traits::{BalanceMigrationManager, CallSources, StorageDepositCollector},
Deposit,
};
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
pub type ClaimHashOf<T> = <T as frame_system::Config>::Hash;
pub type AttesterOf<T> = <T as Config>::AttesterId;
pub(crate) type AuthorizationIdOf<T> = <T as Config>::AuthorizationId;
pub(crate) type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
pub(crate) type BalanceOf<T> = <<T as Config>::Currency as Inspect<AccountIdOf<T>>>::Balance;
pub(crate) type CurrencyOf<T> = <T as Config>::Currency;
pub(crate) type HoldReasonOf<T> = <T as Config>::RuntimeHoldReason;
pub(crate) type BalanceMigrationManagerOf<T> = <T as Config>::BalanceMigrationManager;
pub(crate) type AuthorizedByOf<T> = authorized_by::AuthorizedBy<AccountIdOf<T>, AttesterOf<T>>;
pub type AttestationDetailsOf<T> =
AttestationDetails<CtypeHashOf<T>, AttesterOf<T>, AuthorizationIdOf<T>, AccountIdOf<T>, BalanceOf<T>>;
#[pallet::composite_enum]
pub enum HoldReason {
Deposit,
}
#[pallet::config]
pub trait Config: frame_system::Config + ctype::Config {
type EnsureOrigin: EnsureOrigin<
<Self as frame_system::Config>::RuntimeOrigin,
Success = <Self as Config>::OriginSuccess,
>;
type OriginSuccess: CallSources<AccountIdOf<Self>, AttesterOf<Self>>;
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type WeightInfo: WeightInfo;
type RuntimeHoldReason: From<HoldReason>;
type Currency: MutateHold<AccountIdOf<Self>, Reason = HoldReasonOf<Self>>;
#[pallet::constant]
type Deposit: Get<BalanceOf<Self>>;
#[pallet::constant]
type MaxDelegatedAttestations: Get<u32>;
type AttesterId: Parameter + MaxEncodedLen;
type AuthorizationId: Parameter + MaxEncodedLen;
type AccessControl: Parameter
+ AttestationAccessControl<Self::AttesterId, Self::AuthorizationId, CtypeHashOf<Self>, ClaimHashOf<Self>>;
type BalanceMigrationManager: BalanceMigrationManager<AccountIdOf<Self>, BalanceOf<Self>>;
}
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
#[cfg(feature = "try-runtime")]
fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
crate::try_state::do_try_state::<T>()
}
}
#[pallet::storage]
#[pallet::getter(fn attestations)]
pub type Attestations<T> = StorageMap<_, Blake2_128Concat, ClaimHashOf<T>, AttestationDetailsOf<T>>;
#[pallet::storage]
#[pallet::getter(fn external_attestations)]
pub type ExternalAttestations<T> =
StorageDoubleMap<_, Twox64Concat, AuthorizationIdOf<T>, Blake2_128Concat, ClaimHashOf<T>, bool, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
AttestationCreated {
attester: AttesterOf<T>,
claim_hash: ClaimHashOf<T>,
ctype_hash: CtypeHashOf<T>,
authorization: Option<AuthorizationIdOf<T>>,
},
AttestationRevoked {
authorized_by: AuthorizedByOf<T>,
attester: AttesterOf<T>,
ctype_hash: CtypeHashOf<T>,
claim_hash: ClaimHashOf<T>,
},
AttestationRemoved {
authorized_by: AuthorizedByOf<T>,
attester: AttesterOf<T>,
ctype_hash: CtypeHashOf<T>,
claim_hash: ClaimHashOf<T>,
},
}
#[pallet::error]
pub enum Error<T> {
AlreadyAttested,
AlreadyRevoked,
NotFound,
CTypeMismatch,
NotAuthorized,
MaxDelegatedAttestationsExceeded,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(
<T as pallet::Config>::WeightInfo::add()
.saturating_add(authorization.as_ref().map(|ac| ac.can_attest_weight()).unwrap_or(Weight::zero()))
)]
pub fn add(
origin: OriginFor<T>,
claim_hash: ClaimHashOf<T>,
ctype_hash: CtypeHashOf<T>,
authorization: Option<T::AccessControl>,
) -> DispatchResult {
let source = <T as Config>::EnsureOrigin::ensure_origin(origin)?;
let payer = source.sender();
let who = source.subject();
let deposit_amount = <T as Config>::Deposit::get();
ensure!(
ctype::Ctypes::<T>::contains_key(ctype_hash),
ctype::Error::<T>::NotFound
);
ensure!(
!Attestations::<T>::contains_key(claim_hash),
Error::<T>::AlreadyAttested
);
authorization
.as_ref()
.map(|ac| ac.can_attest(&who, &ctype_hash, &claim_hash))
.transpose()?;
let authorization_id = authorization.as_ref().map(|ac| ac.authorization_id());
let deposit = AttestationStorageDepositCollector::<T>::create_deposit(payer, deposit_amount)?;
<T as Config>::BalanceMigrationManager::exclude_key_from_migration(&Attestations::<T>::hashed_key_for(
claim_hash,
));
log::debug!("insert Attestation");
Attestations::<T>::insert(
claim_hash,
AttestationDetails {
ctype_hash,
attester: who.clone(),
authorization_id: authorization_id.clone(),
revoked: false,
deposit,
},
);
if let Some(authorization_id) = &authorization_id {
ExternalAttestations::<T>::insert(authorization_id, claim_hash, true);
}
Self::deposit_event(Event::AttestationCreated {
attester: who,
claim_hash,
ctype_hash,
authorization: authorization_id,
});
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(
<T as pallet::Config>::WeightInfo::revoke()
.saturating_add(authorization.as_ref().map(|ac| ac.can_revoke_weight()).unwrap_or(Weight::zero()))
)]
pub fn revoke(
origin: OriginFor<T>,
claim_hash: ClaimHashOf<T>,
authorization: Option<T::AccessControl>,
) -> DispatchResultWithPostInfo {
let source = <T as Config>::EnsureOrigin::ensure_origin(origin)?;
let who = source.subject();
let attestation = Attestations::<T>::get(claim_hash).ok_or(Error::<T>::NotFound)?;
let attester = attestation.attester.clone();
ensure!(!attestation.revoked, Error::<T>::AlreadyRevoked);
let authorized_by = if attester != who {
let attestation_auth_id = attestation.authorization_id.as_ref().ok_or(Error::<T>::NotAuthorized)?;
authorization.ok_or(Error::<T>::NotAuthorized)?.can_revoke(
&who,
&attestation.ctype_hash,
&claim_hash,
attestation_auth_id,
)?;
AuthorizedBy::Authorization(who)
} else {
AuthorizedBy::Attester(who)
};
log::debug!("revoking Attestation");
Attestations::<T>::insert(
claim_hash,
AttestationDetails {
revoked: true,
..attestation
},
);
Self::deposit_event(Event::AttestationRevoked {
attester,
authorized_by,
ctype_hash: attestation.ctype_hash,
claim_hash,
});
Ok(Some(<T as pallet::Config>::WeightInfo::revoke()).into())
}
#[pallet::call_index(2)]
#[pallet::weight(
<T as pallet::Config>::WeightInfo::remove()
.saturating_add(authorization.as_ref().map(|ac| ac.can_remove_weight()).unwrap_or(Weight::zero()))
)]
pub fn remove(
origin: OriginFor<T>,
claim_hash: ClaimHashOf<T>,
authorization: Option<T::AccessControl>,
) -> DispatchResultWithPostInfo {
let source = <T as Config>::EnsureOrigin::ensure_origin(origin)?;
let who = source.subject();
let attestation = Attestations::<T>::get(claim_hash).ok_or(Error::<T>::NotFound)?;
let authorized_by = if attestation.attester != who {
let attestation_auth_id = attestation.authorization_id.as_ref().ok_or(Error::<T>::NotAuthorized)?;
authorization.ok_or(Error::<T>::NotAuthorized)?.can_remove(
&who,
&attestation.ctype_hash,
&claim_hash,
attestation_auth_id,
)?;
AuthorizedBy::Authorization(who)
} else {
AuthorizedBy::Attester(who)
};
log::debug!("removing Attestation");
Self::remove_attestation(authorized_by, attestation, claim_hash)?;
Ok(Some(<T as pallet::Config>::WeightInfo::remove()).into())
}
#[pallet::call_index(3)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::reclaim_deposit())]
pub fn reclaim_deposit(origin: OriginFor<T>, claim_hash: ClaimHashOf<T>) -> DispatchResult {
let who = ensure_signed(origin)?;
let attestation = Attestations::<T>::get(claim_hash).ok_or(Error::<T>::NotFound)?;
ensure!(attestation.deposit.owner == who, Error::<T>::NotAuthorized);
log::debug!("removing Attestation");
Self::remove_attestation(AuthorizedBy::DepositOwner(who), attestation, claim_hash)?;
Ok(())
}
#[pallet::call_index(4)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::change_deposit_owner())]
pub fn change_deposit_owner(origin: OriginFor<T>, claim_hash: ClaimHashOf<T>) -> DispatchResult {
let source = <T as Config>::EnsureOrigin::ensure_origin(origin)?;
let subject = source.subject();
let sender = source.sender();
let attestation = Attestations::<T>::get(claim_hash).ok_or(Error::<T>::NotFound)?;
ensure!(attestation.attester == subject, Error::<T>::NotAuthorized);
AttestationStorageDepositCollector::<T>::change_deposit_owner::<BalanceMigrationManagerOf<T>>(
&claim_hash,
sender,
)?;
Ok(())
}
#[pallet::call_index(5)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::update_deposit())]
pub fn update_deposit(origin: OriginFor<T>, claim_hash: ClaimHashOf<T>) -> DispatchResult {
let sender = ensure_signed(origin)?;
let attestation = Attestations::<T>::get(claim_hash).ok_or(Error::<T>::NotFound)?;
ensure!(attestation.deposit.owner == sender, Error::<T>::NotAuthorized);
AttestationStorageDepositCollector::<T>::update_deposit::<BalanceMigrationManagerOf<T>>(&claim_hash)?;
Ok(())
}
}
impl<T: Config> Pallet<T> {
fn remove_attestation(
authorized_by: AuthorizedByOf<T>,
attestation: AttestationDetailsOf<T>,
claim_hash: ClaimHashOf<T>,
) -> DispatchResult {
let is_key_migrated =
<T as Config>::BalanceMigrationManager::is_key_migrated(&Attestations::<T>::hashed_key_for(claim_hash));
if is_key_migrated {
AttestationStorageDepositCollector::<T>::free_deposit(attestation.deposit)?;
} else {
<T as Config>::BalanceMigrationManager::release_reserved_deposit(
&attestation.deposit.owner,
&attestation.deposit.amount,
)
}
Attestations::<T>::remove(claim_hash);
if let Some(authorization_id) = &attestation.authorization_id {
ExternalAttestations::<T>::remove(authorization_id, claim_hash);
}
if !attestation.revoked {
Self::deposit_event(Event::AttestationRevoked {
attester: attestation.attester.clone(),
authorized_by: authorized_by.clone(),
claim_hash,
ctype_hash: attestation.ctype_hash,
});
}
Self::deposit_event(Event::AttestationRemoved {
attester: attestation.attester,
authorized_by,
claim_hash,
ctype_hash: attestation.ctype_hash,
});
Ok(())
}
}
pub(crate) struct AttestationStorageDepositCollector<T: Config>(PhantomData<T>);
impl<T: Config> StorageDepositCollector<AccountIdOf<T>, ClaimHashOf<T>, T::RuntimeHoldReason>
for AttestationStorageDepositCollector<T>
{
type Currency = <T as Config>::Currency;
type Reason = HoldReason;
fn reason() -> Self::Reason {
HoldReason::Deposit
}
fn get_hashed_key(key: &ClaimHashOf<T>) -> Result<sp_std::vec::Vec<u8>, DispatchError> {
Ok(Attestations::<T>::hashed_key_for(key))
}
fn deposit(
key: &ClaimHashOf<T>,
) -> Result<Deposit<AccountIdOf<T>, <Self::Currency as Inspect<AccountIdOf<T>>>::Balance>, DispatchError> {
let attestation = Attestations::<T>::get(key).ok_or(Error::<T>::NotFound)?;
Ok(attestation.deposit)
}
fn deposit_amount(_key: &ClaimHashOf<T>) -> <Self::Currency as Inspect<AccountIdOf<T>>>::Balance {
T::Deposit::get()
}
fn store_deposit(
key: &ClaimHashOf<T>,
deposit: Deposit<AccountIdOf<T>, <Self::Currency as Inspect<AccountIdOf<T>>>::Balance>,
) -> Result<(), DispatchError> {
let attestation = Attestations::<T>::get(key).ok_or(Error::<T>::NotFound)?;
Attestations::<T>::insert(key, AttestationDetails { deposit, ..attestation });
Ok(())
}
}
}