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;
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 logoURI() external view returns (string memory);
89        function setLogoURI(string calldata newLogoURI) external;
90        function burnBlocked(address from, uint256 amount) external;
91        function mintWithMemo(address to, uint256 amount, bytes32 memo) external;
92        function burnWithMemo(uint256 amount, bytes32 memo) external;
93        function transferWithMemo(address to, uint256 amount, bytes32 memo) external;
94        function transferFromWithMemo(address from, address to, uint256 amount, bytes32 memo) external returns (bool);
95
96        // Admin Functions
97        function changeTransferPolicyId(uint64 newPolicyId) external;
98        function setSupplyCap(uint256 newSupplyCap) external;
99        function pause() external;
100        function unpause() external;
101        function setNextQuoteToken(address newQuoteToken) external;
102        function completeQuoteTokenUpdate() external;
103
104        /// @notice Returns the role identifier for pausing the contract
105        /// @return The pause role identifier
106        function PAUSE_ROLE() external view returns (bytes32);
107
108        /// @notice Returns the role identifier for unpausing the contract
109        /// @return The unpause role identifier
110        function UNPAUSE_ROLE() external view returns (bytes32);
111
112        /// @notice Returns the role identifier for issuing tokens
113        /// @return The issuer role identifier
114        function ISSUER_ROLE() external view returns (bytes32);
115
116        /// @notice Returns the role identifier for burning tokens from blocked accounts
117        /// @return The burn blocked role identifier
118        function BURN_BLOCKED_ROLE() external view returns (bytes32);
119
120        // EIP-2612 Permit Functions
121        function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;
122        function nonces(address owner) external view returns (uint256);
123        function DOMAIN_SEPARATOR() external view returns (bytes32);
124
125        struct UserRewardInfo {
126            address rewardRecipient;
127            uint256 rewardPerToken;
128            uint256 rewardBalance;
129        }
130
131        // Reward Functions
132        function distributeReward(uint256 amount) external;
133        function setRewardRecipient(address recipient) external;
134        function claimRewards() external returns (uint256);
135        function optedInSupply() external view returns (uint128);
136        function globalRewardPerToken() external view returns (uint256);
137        function userRewardInfo(address account) external view returns (UserRewardInfo memory);
138        function getPendingRewards(address account) external view returns (uint128);
139
140        // Events
141        event Transfer(address indexed from, address indexed to, uint256 amount);
142        event Approval(address indexed owner, address indexed spender, uint256 amount);
143        event Mint(address indexed to, uint256 amount);
144        event Burn(address indexed from, uint256 amount);
145        event BurnBlocked(address indexed from, uint256 amount);
146        event TransferWithMemo(address indexed from, address indexed to, uint256 amount, bytes32 indexed memo);
147        event TransferPolicyUpdate(address indexed updater, uint64 indexed newPolicyId);
148        event SupplyCapUpdate(address indexed updater, uint256 indexed newSupplyCap);
149        event PauseStateUpdate(address indexed updater, bool isPaused);
150        event NextQuoteTokenSet(address indexed updater, address indexed nextQuoteToken);
151        event QuoteTokenUpdate(address indexed updater, address indexed newQuoteToken);
152        event RewardDistributed(address indexed funder, uint256 amount);
153        event RewardRecipientSet(address indexed holder, address indexed recipient);
154        event LogoURIUpdated(address indexed updater, string newLogoURI);
155
156        // Errors
157        error InsufficientBalance(uint256 available, uint256 required, address token);
158        error InsufficientAllowance();
159        error SupplyCapExceeded();
160        error InvalidSupplyCap();
161        error InvalidPayload();
162        error PolicyForbids();
163        error InvalidRecipient();
164        error ContractPaused();
165        error InvalidCurrency();
166        error InvalidQuoteToken();
167        error InvalidAmount();
168        error NoOptedInSupply();
169        error Unauthorized();
170        error ProtectedAddress();
171        error InvalidToken();
172        error Uninitialized();
173        error InvalidTransferPolicyId();
174        error PermitExpired();
175        error InvalidSignature();
176        error LogoURITooLong();
177        error InvalidLogoURI();
178    }
179}
180
181impl ITIP20::ITIP20Calls {
182    /// Returns the recipient address for the TIP-20 call, if one exists.
183    pub fn to(&self) -> Option<Address> {
184        Some(match self {
185            Self::transfer(c) => c.to,
186            Self::transferWithMemo(c) => c.to,
187            Self::transferFrom(c) => c.to,
188            Self::transferFromWithMemo(c) => c.to,
189            Self::mint(c) => c.to,
190            Self::mintWithMemo(c) => c.to,
191            _ => return None,
192        })
193    }
194
195    /// Returns `true` if `input` matches one of the recognized [TIP-20 payment] selectors:
196    /// - `transfer` / `transferWithMemo`
197    /// - `transferFrom` / `transferFromWithMemo`
198    /// - `approve`
199    /// - `mint` / `mintWithMemo`
200    /// - `burn` / `burnWithMemo`
201    ///
202    /// # NOTES
203    /// - Only validates calldata; the caller must check the TIP-20 address prefix on `to`.
204    /// - Only selector and exact ABI-encoded length match, no decoding (better performance).
205    ///
206    /// [TIP-20 payment]: <https://docs.tempo.xyz/protocol/tip20/overview#get-predictable-payment-fees>
207    pub fn is_payment(input: &[u8]) -> bool {
208        fn is_call<C: SolCall>(input: &[u8]) -> bool {
209            let Some(encoded_size) = <C::Parameters<'_> as SolType>::ENCODED_SIZE else {
210                return false;
211            };
212
213            input.first_chunk::<4>() == Some(&C::SELECTOR) && input.len() == 4 + encoded_size
214        }
215
216        is_call::<ITIP20::transferCall>(input)
217            || is_call::<ITIP20::transferWithMemoCall>(input)
218            || is_call::<ITIP20::transferFromCall>(input)
219            || is_call::<ITIP20::transferFromWithMemoCall>(input)
220            || is_call::<ITIP20::approveCall>(input)
221            || is_call::<ITIP20::mintCall>(input)
222            || is_call::<ITIP20::mintWithMemoCall>(input)
223            || is_call::<ITIP20::burnCall>(input)
224            || is_call::<ITIP20::burnWithMemoCall>(input)
225    }
226
227    /// Returns addresses whose balance slots are accessed by this call.
228    ///
229    /// For transfers: `[to]` or `[from, to]`. For mints: `[to]`.
230    /// For burns, approves, and view calls: empty.
231    pub fn balance_addresses(&self) -> [Option<Address>; 2] {
232        match self {
233            Self::transfer(c) => [Some(c.to), None],
234            Self::transferWithMemo(c) => [Some(c.to), None],
235            Self::transferFrom(c) => [Some(c.from), Some(c.to)],
236            Self::transferFromWithMemo(c) => [Some(c.from), Some(c.to)],
237            Self::mint(c) => [Some(c.to), None],
238            Self::mintWithMemo(c) => [Some(c.to), None],
239            _ => [None, None],
240        }
241    }
242
243    /// Returns addresses whose rewards slots are accessed by this call.
244    pub fn reward_addresses(&self, sender: Address) -> [Option<Address>; 2] {
245        match self {
246            Self::transfer(c) => [Some(sender), Some(c.to)],
247            Self::transferWithMemo(c) => [Some(sender), Some(c.to)],
248            Self::transferFrom(c) => [Some(c.from), Some(c.to)],
249            Self::transferFromWithMemo(c) => [Some(c.from), Some(c.to)],
250            Self::mint(c) => [Some(c.to), None],
251            Self::mintWithMemo(c) => [Some(c.to), None],
252            Self::burn(_) | Self::burnWithMemo(_) => [Some(sender), Some(Address::ZERO)],
253            _ => [None, None],
254        }
255    }
256}
257
258#[cfg(test)]
259mod test {
260    use super::*;
261    use alloc::vec::Vec;
262    use alloy_primitives::{Address, B256, U256};
263
264    #[rustfmt::skip]
265    /// Returns valid ABI-encoded calldata for every recognized TIP-20 payment selector.
266    fn payment_calldatas() -> [Vec<u8>; 9] {
267        let (to, from, amount, memo) = (Address::random(), Address::random(), U256::random(), B256::random());
268
269        [
270            ITIP20::transferCall { to, amount }.abi_encode(),
271            ITIP20::transferWithMemoCall { to, amount, memo }.abi_encode(),
272            ITIP20::transferFromCall { from, to, amount }.abi_encode(),
273            ITIP20::transferFromWithMemoCall { from, to, amount, memo }.abi_encode(),
274            ITIP20::approveCall { spender: to, amount }.abi_encode(),
275            ITIP20::mintCall { to, amount }.abi_encode(),
276            ITIP20::mintWithMemoCall { to, amount, memo }.abi_encode(),
277            ITIP20::burnCall { amount }.abi_encode(),
278            ITIP20::burnWithMemoCall { amount, memo }.abi_encode(),
279        ]
280    }
281
282    #[rustfmt::skip]
283    /// Returns ABI-encoded calldata for TIP-20 selectors NOT recognized as payments.
284    fn non_payment_calldatas() -> [Vec<u8>; 3] {
285        let mut data = ITIP20::transferCall { to: Address::random(), amount: U256::random() }.abi_encode();
286        data[..4].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
287
288        [
289            // non-payment TIP20 calls with known selectors
290            ITIP20::claimRewardsCall {}.abi_encode(),
291            ITIP20::permitCall {
292                owner: Address::random(), spender: Address::random(), value: U256::random(), deadline: U256::random(),
293                v: u8::MAX, r: B256::random(), s: B256::random() }.abi_encode(),
294            // non-payment TIP20 calls with unknown selectors
295            data,
296        ]
297    }
298
299    #[test]
300    fn test_is_payment() {
301        for calldata in payment_calldatas() {
302            assert!(ITIP20::ITIP20Calls::is_payment(&calldata))
303        }
304
305        for calldata in non_payment_calldatas() {
306            assert!(!ITIP20::ITIP20Calls::is_payment(&calldata))
307        }
308    }
309}