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
59 changes: 59 additions & 0 deletions crates/precompiles/src/ztip20.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,22 @@ impl<P: PolicyCheck> ZoneTip20Token<P> {
if let Some(revert) = self.reject_crossed_mint_caller(caller) {
return Some(revert);
}
// Skip TIP-403 enforcement for deposit mints from ZoneInbox.
// The sequencer already checks the recipient, and failed-deposit
// refunds to the sender must always succeed to prevent zone lockup.
if caller == ZONE_INBOX_ADDRESS {
return None;
}
let call = decode_or_revert!(ITIP20::mintCall, args);
self.enforce_mint(address, call.to)
}
ITIP20::mintWithMemoCall::SELECTOR => {
if let Some(revert) = self.reject_crossed_mint_caller(caller) {
return Some(revert);
}
if caller == ZONE_INBOX_ADDRESS {
return None;
}
let call = decode_or_revert!(ITIP20::mintWithMemoCall, args);
self.enforce_mint(address, call.to)
}
Expand Down Expand Up @@ -1140,6 +1149,56 @@ mod tests {
Ok(())
}

#[test]
fn inbox_mint_skips_tip403_for_blocked_recipient() -> TestResult {
// Simulates a failed encrypted deposit refund to a TIP-403 blocked sender.
// The zone must not enforce TIP-403 on deposit mints from ZoneInbox,
// otherwise the zone locks up.
let mut harness = PrecompileHarness::new(MockPolicyProvider {
mint_authorized: false,
transfer_authorized: false,
policy_id: 1,
fail_policy_id_resolution: false,
})?;

// Mint from ZoneInbox to a blocked address must succeed.
let inbox_mint = harness.call(
ZONE_INBOX_ADDRESS,
ITIP20::mintCall {
to: harness.bob,
amount: U256::from(99_000u64),
}
.abi_encode()
.into(),
100_000,
false,
)?;
assert!(
!inbox_mint.reverted,
"ZoneInbox deposit mint must not be blocked by TIP-403"
);
assert_eq!(harness.balance_of(harness.bob)?, U256::from(99_000u64));

// Mint from the issuer to the same blocked address must still be rejected.
let issuer_mint = harness.call(
harness.issuer,
ITIP20::mintCall {
to: harness.bob,
amount: U256::from(1_000u64),
}
.abi_encode()
.into(),
100_000,
false,
)?;
assert!(
issuer_mint.reverted,
"issuer mint to blocked address must still be rejected"
);

Ok(())
}

#[test]
fn has_role_enforces_account_or_sequencer_access() -> TestResult {
let mut harness = PrecompileHarness::new(MockPolicyProvider::allow_all())?;
Expand Down
30 changes: 28 additions & 2 deletions crates/tempo-zone/src/l1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1196,7 +1196,7 @@ impl L1BlockDeposits {
// Check TIP-403 policy via the provider (cache-first, RPC fallback).
// Errors are propagated so the engine retries rather than allowing
// unauthorized deposits through.
let authorized = policy_provider
let recipient_authorized = policy_provider
.is_authorized_async(
d.token,
dec.to,
Expand All @@ -1205,7 +1205,7 @@ impl L1BlockDeposits {
)
.await?;

let recipient = if authorized {
let recipient = if recipient_authorized {
debug!(
target: "zone::engine",
recipient = %dec.to,
Expand All @@ -1214,12 +1214,38 @@ impl L1BlockDeposits {
);
dec.to
} else {
// Best-effort check whether the sender is also
// blocked. The zone precompile skips TIP-403 on
// deposit mints so this won't cause a lockup; we
// only log it for observability.
let sender_blocked = match policy_provider
.is_authorized_async(
d.token,
d.sender,
l1_block_number,
crate::l1_state::AuthRole::MintRecipient,
)
.await
{
Ok(authorized) => !authorized,
Err(error) => {
warn!(
target: "zone::engine",
sender = %d.sender,
%error,
"Failed to check sender TIP-403 status, continuing"
);
false
}
};

warn!(
target: "zone::engine",
sender = %d.sender,
recipient = %dec.to,
token = %d.token,
amount = %d.amount,
sender_blocked,
"Encrypted deposit recipient unauthorized, redirecting to sender"
);
d.sender
Expand Down
Loading