Skip to main content

cast/cmd/
tip403.rs

1use crate::{
2    cmd::{rpc_provider, tip20::send_tip20_transaction},
3    tempo::print_payload,
4    tx::{SendTxOpts, TxParams},
5};
6use alloy_ens::NameOrAddress;
7use alloy_primitives::Address;
8use alloy_sol_types::SolCall;
9use clap::{Parser, ValueEnum};
10use eyre::Result;
11use foundry_cli::opts::RpcOpts;
12use serde_json::json;
13use std::str::FromStr;
14use tempo_contracts::precompiles::{ITIP403Registry, TIP403_REGISTRY_ADDRESS};
15use tempo_primitives::TempoAddressExt;
16
17/// TIP-403 policy registry operations (Tempo).
18///
19/// Policies created here are referenced by ID from `cast receive-policy set` (sender policy and
20/// token filter) and by TIP-20 token compliance configuration.
21#[derive(Debug, Parser, Clone)]
22pub enum Tip403Subcommand {
23    /// Create a new simple (whitelist or blacklist) policy.
24    Create {
25        /// Policy type to create.
26        #[arg(value_enum)]
27        policy_type: PolicyKind,
28
29        /// Address authorized to modify the policy.
30        #[arg(long, value_parser = NameOrAddress::from_str)]
31        admin: NameOrAddress,
32
33        /// Initial member(s) to seed the policy with. Can be specified multiple times.
34        #[arg(long = "member", value_name = "ADDRESS", value_parser = NameOrAddress::from_str)]
35        accounts: Vec<NameOrAddress>,
36
37        #[command(flatten)]
38        send_tx: SendTxOpts,
39
40        #[command(flatten)]
41        tx: TxParams,
42    },
43
44    /// Add or remove an account from a whitelist policy.
45    Whitelist {
46        #[command(flatten)]
47        args: MembershipArgs,
48    },
49
50    /// Add or remove an account from a blacklist policy.
51    Blacklist {
52        #[command(flatten)]
53        args: MembershipArgs,
54    },
55
56    /// Show a policy's type and admin.
57    Info {
58        /// Policy ID to inspect.
59        policy_id: u64,
60
61        #[command(flatten)]
62        rpc: RpcOpts,
63    },
64
65    /// Check whether an address is authorized by a policy.
66    Check {
67        /// Policy ID to evaluate.
68        policy_id: u64,
69
70        /// Address to check.
71        #[arg(value_parser = NameOrAddress::from_str)]
72        address: NameOrAddress,
73
74        /// Role to evaluate (defaults to the transfer check). Role variants require T2+.
75        #[arg(long, value_enum)]
76        role: Option<PolicyRole>,
77
78        #[command(flatten)]
79        rpc: RpcOpts,
80    },
81}
82
83#[derive(Debug, Clone, clap::Args)]
84pub struct MembershipArgs {
85    /// Whether to add or remove the account.
86    #[arg(value_enum)]
87    pub action: MembershipAction,
88
89    /// Policy ID to modify.
90    pub policy_id: u64,
91
92    /// Account to add or remove.
93    #[arg(value_parser = NameOrAddress::from_str)]
94    pub account: NameOrAddress,
95
96    #[command(flatten)]
97    pub send_tx: SendTxOpts,
98
99    #[command(flatten)]
100    pub tx: TxParams,
101}
102
103#[derive(Debug, Clone, Copy, ValueEnum)]
104pub enum PolicyKind {
105    Whitelist,
106    Blacklist,
107}
108
109#[derive(Debug, Clone, Copy, ValueEnum)]
110pub enum MembershipAction {
111    Add,
112    Remove,
113}
114
115#[derive(Debug, Clone, Copy, ValueEnum)]
116pub enum PolicyRole {
117    Sender,
118    Recipient,
119    MintRecipient,
120}
121
122impl Tip403Subcommand {
123    pub async fn run(self) -> Result<()> {
124        match self {
125            Self::Create { policy_type, admin, accounts, send_tx, tx } => {
126                create(policy_type, admin, accounts, send_tx, tx).await
127            }
128            Self::Whitelist { args } => modify(PolicyKind::Whitelist, args).await,
129            Self::Blacklist { args } => modify(PolicyKind::Blacklist, args).await,
130            Self::Info { policy_id, rpc } => info(policy_id, rpc).await,
131            Self::Check { policy_id, address, role, rpc } => {
132                check(policy_id, address, role, rpc).await
133            }
134        }
135    }
136}
137
138async fn create(
139    policy_type: PolicyKind,
140    admin: NameOrAddress,
141    accounts: Vec<NameOrAddress>,
142    send_tx: SendTxOpts,
143    tx: TxParams,
144) -> Result<()> {
145    let provider = rpc_provider(&send_tx.eth.rpc)?;
146    let admin = admin.resolve(&provider).await?;
147
148    let mut members = Vec::with_capacity(accounts.len());
149    for account in accounts {
150        let account = account.resolve(&provider).await?;
151        warn_if_virtual(account)?;
152        members.push(account);
153    }
154
155    // Preview the policy ID the registry would assign. This is the next counter value, so it is
156    // only accurate if no other policy is created before this transaction lands.
157    let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, &provider);
158    let policy_type = policy_type.to_sol();
159    let (expected_id, data) = if members.is_empty() {
160        let call = registry.createPolicy(admin, policy_type);
161        (call.call().await?, call.calldata().to_vec())
162    } else {
163        let call = registry.createPolicyWithAccounts(admin, policy_type, members);
164        (call.call().await?, call.calldata().to_vec())
165    };
166    sh_status!(
167        "Expected policy ID: {expected_id} (only if this tx is mined before any other policy \
168         creation; read the PolicyCreated event for the authoritative ID)"
169    )?;
170
171    send_tip20_transaction(TIP403_REGISTRY_ADDRESS, data, send_tx, tx).await
172}
173
174async fn modify(kind: PolicyKind, args: MembershipArgs) -> Result<()> {
175    let MembershipArgs { action, policy_id, account, send_tx, tx } = args;
176    let provider = rpc_provider(&send_tx.eth.rpc)?;
177    let account = account.resolve(&provider).await?;
178    warn_if_virtual(account)?;
179
180    let flag = matches!(action, MembershipAction::Add);
181    let data = match kind {
182        PolicyKind::Whitelist => ITIP403Registry::modifyPolicyWhitelistCall {
183            policyId: policy_id,
184            account,
185            allowed: flag,
186        }
187        .abi_encode(),
188        PolicyKind::Blacklist => ITIP403Registry::modifyPolicyBlacklistCall {
189            policyId: policy_id,
190            account,
191            restricted: flag,
192        }
193        .abi_encode(),
194    };
195    send_tip20_transaction(TIP403_REGISTRY_ADDRESS, data, send_tx, tx).await
196}
197
198async fn info(policy_id: u64, rpc: RpcOpts) -> Result<()> {
199    let provider = rpc_provider(&rpc)?;
200    let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, provider);
201    let builtin = match policy_id {
202        0 => Some("reject-all"),
203        1 => Some("allow-all"),
204        _ => None,
205    };
206
207    if !registry.policyExists(policy_id).call().await? {
208        let payload = json!({ "policy_id": policy_id, "exists": false, "builtin": builtin });
209        return print_payload(payload, |_| sh_println!("Policy {policy_id} does not exist"));
210    }
211
212    let data = registry.policyData(policy_id).call().await?;
213    let payload = json!({
214        "policy_id": policy_id,
215        "exists": true,
216        "builtin": builtin,
217        "policy_type": policy_type_label(data.policyType),
218        "admin": format!("{}", data.admin),
219    });
220    print_payload(payload, |payload| {
221        sh_println!(
222            "Policy ID: {}\n\
223             Built-in:  {}\n\
224             Type:      {}\n\
225             Admin:     {}",
226            payload["policy_id"],
227            payload["builtin"].as_str().unwrap_or("no"),
228            payload["policy_type"].as_str().unwrap_or_default(),
229            payload["admin"].as_str().unwrap_or_default(),
230        )
231    })
232}
233
234async fn check(
235    policy_id: u64,
236    address: NameOrAddress,
237    role: Option<PolicyRole>,
238    rpc: RpcOpts,
239) -> Result<()> {
240    let provider = rpc_provider(&rpc)?;
241    let address = address.resolve(&provider).await?;
242    let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, provider);
243    let (role_label, authorized) = match role {
244        None => ("transfer", registry.isAuthorized(policy_id, address).call().await?),
245        Some(PolicyRole::Sender) => {
246            ("sender", registry.isAuthorizedSender(policy_id, address).call().await?)
247        }
248        Some(PolicyRole::Recipient) => {
249            ("recipient", registry.isAuthorizedRecipient(policy_id, address).call().await?)
250        }
251        Some(PolicyRole::MintRecipient) => {
252            ("mint-recipient", registry.isAuthorizedMintRecipient(policy_id, address).call().await?)
253        }
254    };
255
256    let payload = json!({
257        "policy_id": policy_id,
258        "address": format!("{address}"),
259        "role": role_label,
260        "authorized": authorized,
261    });
262    print_payload(payload, |payload| {
263        sh_println!(
264            "Policy ID:  {}\n\
265             Address:    {}\n\
266             Role:       {}\n\
267             Authorized: {}",
268            payload["policy_id"],
269            payload["address"].as_str().unwrap_or_default(),
270            payload["role"].as_str().unwrap_or_default(),
271            payload["authorized"].as_bool().unwrap_or_default(),
272        )
273    })
274}
275
276/// Warn (but don't fail) on virtual members; only T3+ chains reject them on-chain.
277fn warn_if_virtual(account: Address) -> Result<()> {
278    if account.is_virtual() {
279        sh_warn!(
280            "{account} looks like a TIP-1022 virtual address; on T3+ chains it is rejected as a \
281             literal policy member. Resolve it to its master with `cast vaddr resolve {account}`."
282        )?;
283    }
284    Ok(())
285}
286
287impl PolicyKind {
288    const fn to_sol(self) -> ITIP403Registry::PolicyType {
289        match self {
290            Self::Whitelist => ITIP403Registry::PolicyType::WHITELIST,
291            Self::Blacklist => ITIP403Registry::PolicyType::BLACKLIST,
292        }
293    }
294}
295
296pub(super) const fn policy_type_label(policy_type: ITIP403Registry::PolicyType) -> &'static str {
297    match policy_type {
298        ITIP403Registry::PolicyType::WHITELIST => "whitelist",
299        ITIP403Registry::PolicyType::BLACKLIST => "blacklist",
300        ITIP403Registry::PolicyType::COMPOUND => "compound",
301        _ => "unknown",
302    }
303}