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