Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions crates/precompiles/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,54 @@

use alloy_primitives::Address;
use revm::precompile::PrecompileError;
use tempo_contracts::precompiles::ITIP403Registry::{BlockedReason, PolicyType};
use zone_primitives::policy::AuthRole;

/// Cached TIP-1028 receive-policy configuration for one account.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReceivePolicy {
pub has_receive_policy: bool,
pub sender_policy_id: u64,
pub sender_policy_type: PolicyType,
pub token_filter_id: u64,
pub token_filter_type: PolicyType,
pub recovery_authority: Address,
}

impl ReceivePolicy {
/// Default L1 receive-policy view for an account with no configured policy.
pub const fn none() -> Self {
Self {
has_receive_policy: false,
sender_policy_id: 0,
sender_policy_type: PolicyType::WHITELIST,
token_filter_id: 0,
token_filter_type: PolicyType::WHITELIST,
recovery_authority: Address::ZERO,
}
}
}

/// Result of applying an account receive policy to an inbound transfer or mint.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReceivePolicyDecision {
Authorized,
Blocked {
reason: BlockedReason,
recovery_authority: Address,
},
}

impl ReceivePolicyDecision {
/// Returns the `validateReceivePolicy` ABI fields.
pub const fn as_validate_return(self) -> (bool, BlockedReason) {
match self {
Self::Authorized => (true, BlockedReason::NONE),
Self::Blocked { reason, .. } => (false, reason),
}
}
}

/// Authorization provider used by the TIP-403 proxy and zone TIP-20 precompiles.
///
/// Implementors resolve policy queries — either from an in-memory cache with
Expand Down Expand Up @@ -42,4 +88,15 @@ pub trait PolicyCheck {

/// Return the highest known policy ID counter.
fn policy_id_counter(&self) -> u64;

/// Return an account's TIP-1028 receive-policy configuration.
fn receive_policy(&self, account: Address) -> Result<ReceivePolicy, PrecompileError>;

/// Validate an inbound transfer or mint against the receiver's TIP-1028 policy.
fn validate_receive_policy(
&self,
token: Address,
sender: Address,
receiver: Address,
) -> Result<ReceivePolicyDecision, PrecompileError>;
}
70 changes: 68 additions & 2 deletions crates/precompiles/src/tip403_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
//! queries from the zone's [`PolicyCheck`] provider (cache-first, L1 RPC fallback).
//!
//! **Read-only calls** (`isAuthorized`, `isAuthorizedSender`, `isAuthorizedRecipient`,
//! `isAuthorizedMintRecipient`, `policyData`, `compoundPolicyData`, `policyExists`)
//! `isAuthorizedMintRecipient`, `policyData`, `compoundPolicyData`, `policyExists`,
//! `receivePolicy`, `validateReceivePolicy`)
//! are resolved via the [`PolicyCheck`] trait.
//!
//! **Mutating calls** (`createPolicy`, `modifyPolicyWhitelist`, etc.) are reverted —
Expand All @@ -23,7 +24,7 @@ use tempo_precompiles::tip403_registry::{ALLOW_ALL_POLICY_ID, REJECT_ALL_POLICY_
use tracing::{debug, warn};
use zone_primitives::policy::AuthRole;

use crate::policy::PolicyCheck;
use crate::policy::{PolicyCheck, ReceivePolicyDecision};

/// The precompile address — same as the L1 TIP403Registry.
pub const ZONE_TIP403_PROXY_ADDRESS: Address = TIP403_REGISTRY_ADDRESS;
Expand Down Expand Up @@ -93,6 +94,17 @@ impl<P: PolicyCheck> ZoneTip403ProxyRegistry<P> {
}
self.is_authorized(policy_id, to, AuthRole::Recipient)
}

/// Validate `receiver`'s TIP-1028 receive policy for an inbound transfer or mint.
pub fn validate_receive_policy(
&self,
token: Address,
sender: Address,
receiver: Address,
) -> Result<ReceivePolicyDecision, PrecompileError> {
self.provider
.validate_receive_policy(token, sender, receiver)
}
}

impl<P: PolicyCheck + Clone + Send + Sync + 'static> ZoneTip403ProxyRegistry<P> {
Expand Down Expand Up @@ -154,12 +166,19 @@ impl<P: PolicyCheck> ZoneTip403ProxyRegistry<P> {
if selector == ITIP403Registry::policyIdCounterCall::SELECTOR {
return self.handle_policy_id_counter(reservoir);
}
if selector == ITIP403Registry::receivePolicyCall::SELECTOR {
return self.handle_receive_policy(data, reservoir);
}
if selector == ITIP403Registry::validateReceivePolicyCall::SELECTOR {
return self.handle_validate_receive_policy(data, reservoir);
}

// Mutating functions — all reverted on zone
if selector == ITIP403Registry::createPolicyCall::SELECTOR
|| selector == ITIP403Registry::createPolicyWithAccountsCall::SELECTOR
|| selector == ITIP403Registry::createCompoundPolicyCall::SELECTOR
|| selector == ITIP403Registry::setPolicyAdminCall::SELECTOR
|| selector == ITIP403Registry::setReceivePolicyCall::SELECTOR
|| selector == ITIP403Registry::modifyPolicyWhitelistCall::SELECTOR
|| selector == ITIP403Registry::modifyPolicyBlacklistCall::SELECTOR
{
Expand Down Expand Up @@ -299,4 +318,51 @@ impl<P: PolicyCheck> ZoneTip403ProxyRegistry<P> {
reservoir,
))
}

/// Handle `receivePolicy(account)`.
fn handle_receive_policy(&self, data: &[u8], reservoir: u64) -> PrecompileResult {
let call = match ITIP403Registry::receivePolicyCall::abi_decode(data) {
Ok(call) => call,
Err(_) => return Ok(PrecompileOutput::revert(0, Bytes::new(), reservoir)),
};

let policy = self.provider.receive_policy(call.account)?;
let ret = ITIP403Registry::receivePolicyReturn {
hasReceivePolicy: policy.has_receive_policy,
senderPolicyId: policy.sender_policy_id,
senderPolicyType: policy.sender_policy_type,
tokenFilterId: policy.token_filter_id,
tokenFilterType: policy.token_filter_type,
recoveryAuthority: policy.recovery_authority,
};
let encoded = ITIP403Registry::receivePolicyCall::abi_encode_returns(&ret);
Ok(PrecompileOutput::new(
POLICY_DATA_GAS,
encoded.into(),
reservoir,
))
}

/// Handle `validateReceivePolicy(token, sender, receiver)`.
fn handle_validate_receive_policy(&self, data: &[u8], reservoir: u64) -> PrecompileResult {
let call = match ITIP403Registry::validateReceivePolicyCall::abi_decode(data) {
Ok(call) => call,
Err(_) => return Ok(PrecompileOutput::revert(0, Bytes::new(), reservoir)),
};

let (authorized, blocked_reason) = self
.provider
.validate_receive_policy(call.token, call.sender, call.receiver)?
.as_validate_return();
let ret = ITIP403Registry::validateReceivePolicyReturn {
authorized,
blockedReason: blocked_reason,
};
let encoded = ITIP403Registry::validateReceivePolicyCall::abi_encode_returns(&ret);
Ok(PrecompileOutput::new(
AUTH_CHECK_GAS,
encoded.into(),
reservoir,
))
}
}
Loading
Loading