Skip to main content

tempo_contracts/precompiles/
tip20.rs

1pub use IRolesAuth::{IRolesAuthErrors as RolesAuthError, IRolesAuthEvents as RolesAuthEvent};
2pub use ITIP20::{ITIP20Errors as TIP20Error, ITIP20Events as TIP20Event};
3use alloy_primitives::{Address, U256};
4use alloy_sol_types::{SolCall, SolType};
5
6/// USD currency string constant.
7pub const USD_CURRENCY: &str = "USD";
8
9/// Full list of ISO 4217 currency codes.
10pub const ISO4217_CODES: &[&str] = &[
11    "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT",
12    "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD",
13    "CAD", "CDF", "CHE", "CHF", "CHW", "CLP", "CLF", "CNY", "COP", "COU", "CRC", "CUP", "CVE",
14    "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL",
15    "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "ILS",
16    "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW",
17    "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD",
18    "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NAD", "NGN",
19    "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR",
20    "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLE", "SOS",
21    "SRD", "SSP", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD",
22    "TWD", "TZS", "UAH", "UGX", "USD", "USN", "UYI", "UYU", "UYW", "UZS", "VED", "VES", "VND",
23    "VUV", "WST", "XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR", "XOF", "XPD",
24    "XPF", "XPT", "XSU", "XTS", "XUA", "XXX", "YER", "ZAR", "ZMW", "ZWL",
25];
26
27/// Returns `true` if the given code is a recognized ISO 4217 currency code.
28pub fn is_iso4217_currency(code: &str) -> bool {
29    ISO4217_CODES.binary_search(&code).is_ok()
30}
31
32crate::sol! {
33    #[derive(Debug, PartialEq, Eq)]
34    #[sol(abi)]
35    interface IRolesAuth {
36        function hasRole(address account, bytes32 role) external view returns (bool);
37        function getRoleAdmin(bytes32 role) external view returns (bytes32);
38        function grantRole(bytes32 role, address account) external;
39        function revokeRole(bytes32 role, address account) external;
40        function renounceRole(bytes32 role) external;
41        function setRoleAdmin(bytes32 role, bytes32 adminRole) external;
42
43        event RoleMembershipUpdated(bytes32 indexed role, address indexed account, address indexed sender, bool hasRole);
44        event RoleAdminUpdated(bytes32 indexed role, bytes32 indexed newAdminRole, address indexed sender);
45
46        error Unauthorized();
47    }
48}
49
50crate::sol! {
51    /// TIP20 token interface providing standard ERC20 functionality with Tempo-specific extensions.
52    ///
53    /// TIP20 tokens extend the ERC20 standard with:
54    /// - Currency denomination support for real-world asset backing
55    /// - Transfer policy enforcement for compliance
56    /// - Supply caps for controlled token issuance
57    /// - Pause/unpause functionality for emergency controls
58    /// - Memo support for transaction context
59    /// The interface supports both standard token operations and administrative functions
60    /// for managing token behavior and compliance requirements.
61    #[derive(Debug, PartialEq, Eq)]
62    #[sol(abi)]
63    #[allow(clippy::too_many_arguments)]
64    interface ITIP20 {
65        // Standard token functions
66        function name() external view returns (string memory);
67        function symbol() external view returns (string memory);
68        function decimals() external view returns (uint8);
69        function totalSupply() external view returns (uint256);
70        function quoteToken() external view returns (address);
71        function nextQuoteToken() external view returns (address);
72        function balanceOf(address account) external view returns (uint256);
73        function transfer(address to, uint256 amount) external returns (bool);
74        function approve(address spender, uint256 amount) external returns (bool);
75        function allowance(address owner, address spender) external view returns (uint256);
76        function transferFrom(address from, address to, uint256 amount) external returns (bool);
77        function mint(address to, uint256 amount) external;
78        function burn(uint256 amount) external;
79
80        // TIP20 Extension
81        function currency() external view returns (string memory);
82        function supplyCap() external view returns (uint256);
83        function paused() external view returns (bool);
84        function transferPolicyId() external view returns (uint64);
85        function burnBlocked(address from, uint256 amount) external;
86        function mintWithMemo(address to, uint256 amount, bytes32 memo) external;
87        function burnWithMemo(uint256 amount, bytes32 memo) external;
88        function transferWithMemo(address to, uint256 amount, bytes32 memo) external;
89        function transferFromWithMemo(address from, address to, uint256 amount, bytes32 memo) external returns (bool);
90
91        // Admin Functions
92        function changeTransferPolicyId(uint64 newPolicyId) external;
93        function setSupplyCap(uint256 newSupplyCap) external;
94        function pause() external;
95        function unpause() external;
96        function setNextQuoteToken(address newQuoteToken) external;
97        function completeQuoteTokenUpdate() external;
98
99        /// @notice Returns the role identifier for pausing the contract
100        /// @return The pause role identifier
101        function PAUSE_ROLE() external view returns (bytes32);
102
103        /// @notice Returns the role identifier for unpausing the contract
104        /// @return The unpause role identifier
105        function UNPAUSE_ROLE() external view returns (bytes32);
106
107        /// @notice Returns the role identifier for issuing tokens
108        /// @return The issuer role identifier
109        function ISSUER_ROLE() external view returns (bytes32);
110
111        /// @notice Returns the role identifier for burning tokens from blocked accounts
112        /// @return The burn blocked role identifier
113        function BURN_BLOCKED_ROLE() external view returns (bytes32);
114
115        // EIP-2612 Permit Functions
116        function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;
117        function nonces(address owner) external view returns (uint256);
118        function DOMAIN_SEPARATOR() external view returns (bytes32);
119
120        struct UserRewardInfo {
121            address rewardRecipient;
122            uint256 rewardPerToken;
123            uint256 rewardBalance;
124        }
125
126        // Reward Functions
127        function distributeReward(uint256 amount) external;
128        function setRewardRecipient(address recipient) external;
129        function claimRewards() external returns (uint256);
130        function optedInSupply() external view returns (uint128);
131        function globalRewardPerToken() external view returns (uint256);
132        function userRewardInfo(address account) external view returns (UserRewardInfo memory);
133        function getPendingRewards(address account) external view returns (uint128);
134
135        // Events
136        event Transfer(address indexed from, address indexed to, uint256 amount);
137        event Approval(address indexed owner, address indexed spender, uint256 amount);
138        event Mint(address indexed to, uint256 amount);
139        event Burn(address indexed from, uint256 amount);
140        event BurnBlocked(address indexed from, uint256 amount);
141        event TransferWithMemo(address indexed from, address indexed to, uint256 amount, bytes32 indexed memo);
142        event TransferPolicyUpdate(address indexed updater, uint64 indexed newPolicyId);
143        event SupplyCapUpdate(address indexed updater, uint256 indexed newSupplyCap);
144        event PauseStateUpdate(address indexed updater, bool isPaused);
145        event NextQuoteTokenSet(address indexed updater, address indexed nextQuoteToken);
146        event QuoteTokenUpdate(address indexed updater, address indexed newQuoteToken);
147        event RewardDistributed(address indexed funder, uint256 amount);
148        event RewardRecipientSet(address indexed holder, address indexed recipient);
149
150        // Errors
151        error InsufficientBalance(uint256 available, uint256 required, address token);
152        error InsufficientAllowance();
153        error SupplyCapExceeded();
154        error InvalidSupplyCap();
155        error InvalidPayload();
156        error StringTooLong();
157        error PolicyForbids();
158        error InvalidRecipient();
159        error ContractPaused();
160        error InvalidCurrency();
161        error InvalidQuoteToken();
162        error TransfersDisabled();
163        error InvalidAmount();
164        error NoOptedInSupply();
165        error Unauthorized();
166        error ProtectedAddress();
167        error InvalidToken();
168        error Uninitialized();
169        error InvalidTransferPolicyId();
170        error PermitExpired();
171        error InvalidSignature();
172    }
173}
174
175impl ITIP20::ITIP20Calls {
176    /// Returns `true` if `input` matches one of the recognized [TIP-20 payment] selectors:
177    /// - `transfer` / `transferWithMemo`
178    /// - `transferFrom` / `transferFromWithMemo`
179    /// - `mint` / `mintWithMemo`
180    /// - `burn` / `burnWithMemo`
181    ///
182    /// # NOTES
183    /// - Only validates calldata; the caller must check the TIP-20 address prefix on `to`.
184    /// - Only selector and exact ABI-encoded length match, no decoding (better performance).
185    ///
186    /// [TIP-20 payment]: <https://docs.tempo.xyz/protocol/tip20/overview#get-predictable-payment-fees>
187    pub fn is_payment(input: &[u8]) -> bool {
188        fn is_call<C: SolCall>(input: &[u8]) -> bool {
189            input.first_chunk::<4>() == Some(&C::SELECTOR)
190                && input.len()
191                    == 4 + <C::Parameters<'_> as SolType>::ENCODED_SIZE.unwrap_or_default()
192        }
193
194        is_call::<ITIP20::transferCall>(input)
195            || is_call::<ITIP20::transferWithMemoCall>(input)
196            || is_call::<ITIP20::transferFromCall>(input)
197            || is_call::<ITIP20::transferFromWithMemoCall>(input)
198            || is_call::<ITIP20::approveCall>(input)
199            || is_call::<ITIP20::mintCall>(input)
200            || is_call::<ITIP20::mintWithMemoCall>(input)
201            || is_call::<ITIP20::burnCall>(input)
202            || is_call::<ITIP20::burnWithMemoCall>(input)
203    }
204}
205
206impl RolesAuthError {
207    /// Creates an error for unauthorized access.
208    pub const fn unauthorized() -> Self {
209        Self::Unauthorized(IRolesAuth::Unauthorized {})
210    }
211}
212
213impl TIP20Error {
214    /// Creates an error for insufficient token balance.
215    pub const fn insufficient_balance(available: U256, required: U256, token: Address) -> Self {
216        Self::InsufficientBalance(ITIP20::InsufficientBalance {
217            available,
218            required,
219            token,
220        })
221    }
222
223    /// Creates an error for insufficient spending allowance.
224    pub const fn insufficient_allowance() -> Self {
225        Self::InsufficientAllowance(ITIP20::InsufficientAllowance {})
226    }
227
228    /// Creates an error for unauthorized callers
229    pub const fn unauthorized() -> Self {
230        Self::Unauthorized(ITIP20::Unauthorized {})
231    }
232
233    /// Creates an error when minting would set a supply cap that is too large, or invalid.
234    pub const fn invalid_supply_cap() -> Self {
235        Self::InvalidSupplyCap(ITIP20::InvalidSupplyCap {})
236    }
237
238    /// Creates an error when minting would exceed supply cap.
239    pub const fn supply_cap_exceeded() -> Self {
240        Self::SupplyCapExceeded(ITIP20::SupplyCapExceeded {})
241    }
242
243    /// Creates an error for invalid payload data.
244    pub const fn invalid_payload() -> Self {
245        Self::InvalidPayload(ITIP20::InvalidPayload {})
246    }
247
248    /// Creates an error for invalid quote token.
249    pub const fn invalid_quote_token() -> Self {
250        Self::InvalidQuoteToken(ITIP20::InvalidQuoteToken {})
251    }
252
253    /// Creates an error when string parameter exceeds maximum length.
254    pub const fn string_too_long() -> Self {
255        Self::StringTooLong(ITIP20::StringTooLong {})
256    }
257
258    /// Creates an error when transfer is forbidden by policy.
259    pub const fn policy_forbids() -> Self {
260        Self::PolicyForbids(ITIP20::PolicyForbids {})
261    }
262
263    /// Creates an error for invalid recipient address.
264    pub const fn invalid_recipient() -> Self {
265        Self::InvalidRecipient(ITIP20::InvalidRecipient {})
266    }
267
268    /// Creates an error when contract is paused.
269    pub const fn contract_paused() -> Self {
270        Self::ContractPaused(ITIP20::ContractPaused {})
271    }
272
273    /// Creates an error for invalid currency.
274    pub const fn invalid_currency() -> Self {
275        Self::InvalidCurrency(ITIP20::InvalidCurrency {})
276    }
277
278    /// Creates an error for transfers being disabled.
279    pub const fn transfers_disabled() -> Self {
280        Self::TransfersDisabled(ITIP20::TransfersDisabled {})
281    }
282
283    /// Creates an error for invalid amount.
284    pub const fn invalid_amount() -> Self {
285        Self::InvalidAmount(ITIP20::InvalidAmount {})
286    }
287
288    /// Error for when opted in supply is 0
289    pub const fn no_opted_in_supply() -> Self {
290        Self::NoOptedInSupply(ITIP20::NoOptedInSupply {})
291    }
292
293    /// Error for operations on protected addresses (like burning `FeeManager` tokens)
294    pub const fn protected_address() -> Self {
295        Self::ProtectedAddress(ITIP20::ProtectedAddress {})
296    }
297
298    /// Error when an address is not a valid TIP20 token
299    pub const fn invalid_token() -> Self {
300        Self::InvalidToken(ITIP20::InvalidToken {})
301    }
302
303    /// Error when transfer policy ID does not exist
304    pub const fn invalid_transfer_policy_id() -> Self {
305        Self::InvalidTransferPolicyId(ITIP20::InvalidTransferPolicyId {})
306    }
307
308    /// Error when token is uninitialized (has no bytecode)
309    pub const fn uninitialized() -> Self {
310        Self::Uninitialized(ITIP20::Uninitialized {})
311    }
312
313    /// Error when permit signature has expired (block.timestamp > deadline)
314    pub const fn permit_expired() -> Self {
315        Self::PermitExpired(ITIP20::PermitExpired {})
316    }
317
318    /// Error when permit signature is invalid
319    pub const fn invalid_signature() -> Self {
320        Self::InvalidSignature(ITIP20::InvalidSignature {})
321    }
322}
323
324#[cfg(test)]
325mod test {
326    use super::*;
327    use alloc::vec::Vec;
328    use alloy_primitives::{Address, B256, U256};
329
330    #[rustfmt::skip]
331    /// Returns valid ABI-encoded calldata for every recognized TIP-20 payment selector.
332    fn payment_calldatas() -> [Vec<u8>; 9] {
333        let (to, from, amount, memo) = (Address::random(), Address::random(), U256::random(), B256::random());
334
335        [
336            ITIP20::transferCall { to, amount }.abi_encode(),
337            ITIP20::transferWithMemoCall { to, amount, memo }.abi_encode(),
338            ITIP20::transferFromCall { from, to, amount }.abi_encode(),
339            ITIP20::transferFromWithMemoCall { from, to, amount, memo }.abi_encode(),
340            ITIP20::approveCall { spender: to, amount }.abi_encode(),
341            ITIP20::mintCall { to, amount }.abi_encode(),
342            ITIP20::mintWithMemoCall { to, amount, memo }.abi_encode(),
343            ITIP20::burnCall { amount }.abi_encode(),
344            ITIP20::burnWithMemoCall { amount, memo }.abi_encode(),
345        ]
346    }
347
348    #[rustfmt::skip]
349    /// Returns ABI-encoded calldata for TIP-20 selectors NOT recognized as payments.
350    fn non_payment_calldatas() -> [Vec<u8>; 3] {
351        let mut data = ITIP20::transferCall { to: Address::random(), amount: U256::random() }.abi_encode();
352        data[..4].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
353
354        [
355            // non-payment TIP20 calls with known selectors
356            ITIP20::claimRewardsCall {}.abi_encode(),
357            ITIP20::permitCall {
358                owner: Address::random(), spender: Address::random(), value: U256::random(), deadline: U256::random(),
359                v: u8::MAX, r: B256::random(), s: B256::random() }.abi_encode(),
360            // non-payment TIP20 calls with unknown selectors
361            data,
362        ]
363    }
364
365    #[test]
366    fn test_is_payment() {
367        for calldata in payment_calldatas() {
368            assert!(ITIP20::ITIP20Calls::is_payment(&calldata))
369        }
370
371        for calldata in non_payment_calldatas() {
372            assert!(!ITIP20::ITIP20Calls::is_payment(&calldata))
373        }
374    }
375}