tempo_contracts/precompiles/
tip20_channel_reserve.rs1pub use ITIP20ChannelReserve::{
2 ITIP20ChannelReserveErrors as TIP20ChannelReserveError,
3 ITIP20ChannelReserveEvents as TIP20ChannelReserveEvent,
4};
5use alloy_primitives::{Address, address};
6use alloy_sol_types::{SolCall, SolType};
7
8pub const TIP20_CHANNEL_RESERVE_ADDRESS: Address =
10 address!("0x4D50500000000000000000000000000000000000");
11
12crate::sol! {
13 #[derive(Debug, PartialEq, Eq)]
18 #[sol(abi)]
19 #[allow(clippy::too_many_arguments)]
20 interface ITIP20ChannelReserve {
21 struct ChannelDescriptor {
23 address payer;
25 address payee;
27 address operator;
29 address token;
31 bytes32 salt;
33 address authorizedSigner;
35 bytes32 expiringNonceHash;
37 }
38
39 struct ChannelState {
41 uint96 settled;
43 uint96 deposit;
45 uint32 closeRequestedAt;
47 }
48
49 struct Channel {
51 ChannelDescriptor descriptor;
53 ChannelState state;
55 }
56
57 function CLOSE_GRACE_PERIOD() external view returns (uint64);
59 function VOUCHER_TYPEHASH() external view returns (bytes32);
61
62 function open(
64 address payee,
65 address operator,
66 address token,
67 uint96 deposit,
68 bytes32 salt,
69 address authorizedSigner
70 )
71 external
72 returns (bytes32 channelId);
73
74 function settle(
76 ChannelDescriptor calldata descriptor,
77 uint96 cumulativeAmount,
78 bytes calldata signature
79 )
80 external;
81
82 function topUp(
84 ChannelDescriptor calldata descriptor,
85 uint96 additionalDeposit
86 )
87 external;
88
89 function close(
91 ChannelDescriptor calldata descriptor,
92 uint96 cumulativeAmount,
93 uint96 captureAmount,
94 bytes calldata signature
95 )
96 external;
97
98 function requestClose(ChannelDescriptor calldata descriptor) external;
100
101 function withdraw(ChannelDescriptor calldata descriptor) external;
103
104 function getChannel(ChannelDescriptor calldata descriptor)
106 external
107 view
108 returns (Channel memory);
109
110 function getChannelState(bytes32 channelId) external view returns (ChannelState memory);
112
113 function getChannelStatesBatch(bytes32[] calldata channelIds)
115 external
116 view
117 returns (ChannelState[] memory);
118
119 function computeChannelId(
121 address payer,
122 address payee,
123 address operator,
124 address token,
125 bytes32 salt,
126 address authorizedSigner,
127 bytes32 expiringNonceHash
128 )
129 external
130 view
131 returns (bytes32);
132
133 function getVoucherDigest(bytes32 channelId, uint96 cumulativeAmount)
135 external
136 view
137 returns (bytes32);
138
139 function domainSeparator() external view returns (bytes32);
141
142 event ChannelOpened(
144 bytes32 indexed channelId,
145 address indexed payer,
146 address indexed payee,
147 address operator,
148 address token,
149 address authorizedSigner,
150 bytes32 salt,
151 bytes32 expiringNonceHash,
152 uint96 deposit
153 );
154
155 event Settled(
157 bytes32 indexed channelId,
158 address indexed payer,
159 address indexed payee,
160 uint96 cumulativeAmount,
161 uint96 deltaPaid,
162 uint96 newSettled
163 );
164
165 event TopUp(
167 bytes32 indexed channelId,
168 address indexed payer,
169 address indexed payee,
170 uint96 additionalDeposit,
171 uint96 newDeposit
172 );
173
174 event CloseRequested(
176 bytes32 indexed channelId,
177 address indexed payer,
178 address indexed payee,
179 uint256 closeGraceEnd
180 );
181
182 event ChannelClosed(
184 bytes32 indexed channelId,
185 address indexed payer,
186 address indexed payee,
187 uint96 settledToPayee,
188 uint96 refundedToPayer
189 );
190
191 event CloseRequestCancelled(
193 bytes32 indexed channelId,
194 address indexed payer,
195 address indexed payee
196 );
197
198 error ChannelAlreadyExists();
200 error ChannelNotFound();
202 error NotPayer();
204 error NotPayeeOrOperator();
206 error InvalidPayee();
208 error ZeroDeposit();
210 error ExpiringNonceHashNotSet();
212 error InvalidSignature();
214 error AmountExceedsDeposit();
216 error AmountNotIncreasing();
218 error CaptureAmountInvalid();
220 error CloseNotReady();
222 error DepositOverflow();
224 }
225}
226
227pub const MAX_PAYMENT_CALLDATA_LEN: usize = 2048;
229
230impl ITIP20ChannelReserve::ITIP20ChannelReserveCalls {
231 pub fn is_payment_with_valid_signature(
242 input: &[u8],
243 validate_signature: impl Fn(&[u8]) -> bool,
244 ) -> bool {
245 fn is_static_call<C: SolCall>(input: &[u8]) -> bool {
246 input.first_chunk::<4>() == Some(&C::SELECTOR)
247 && <C::Parameters<'_> as SolType>::ENCODED_SIZE
248 .is_some_and(|canonical_size| input.len() == 4 + canonical_size)
249 }
250
251 fn decode_dynamic_call<C: SolCall>(input: &[u8]) -> Option<C> {
252 if input.first_chunk::<4>() != Some(&C::SELECTOR)
253 || input.len() > MAX_PAYMENT_CALLDATA_LEN
254 {
255 return None;
256 }
257
258 C::abi_decode_validate(input).ok()
259 }
260
261 is_static_call::<ITIP20ChannelReserve::openCall>(input)
262 || is_static_call::<ITIP20ChannelReserve::topUpCall>(input)
263 || decode_dynamic_call::<ITIP20ChannelReserve::closeCall>(input)
264 .is_some_and(|call| validate_signature(call.signature.as_ref()))
265 || decode_dynamic_call::<ITIP20ChannelReserve::settleCall>(input)
266 .is_some_and(|call| validate_signature(call.signature.as_ref()))
267 || is_static_call::<ITIP20ChannelReserve::requestCloseCall>(input)
268 || is_static_call::<ITIP20ChannelReserve::withdrawCall>(input)
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use alloc::{vec, vec::Vec};
276 use alloy_primitives::{B256, aliases::U96};
277
278 impl ITIP20ChannelReserve::ITIP20ChannelReserveCalls {
279 fn is_payment(input: &[u8]) -> bool {
282 Self::is_payment_with_valid_signature(input, |_| true)
283 }
284 }
285
286 fn descriptor() -> ITIP20ChannelReserve::ChannelDescriptor {
287 ITIP20ChannelReserve::ChannelDescriptor {
288 payer: Address::random(),
289 payee: Address::random(),
290 operator: Address::random(),
291 token: Address::random(),
292 salt: B256::random(),
293 authorizedSigner: Address::random(),
294 expiringNonceHash: B256::random(),
295 }
296 }
297
298 #[rustfmt::skip]
299 fn payment_calldatas() -> [Vec<u8>; 6] {
300 let descriptor = descriptor();
301 [
302 ITIP20ChannelReserve::openCall { payee: Address::random(), operator: Address::random(), token: Address::random(), deposit: U96::from(1), salt: B256::random(), authorizedSigner: Address::random() }.abi_encode(),
303 ITIP20ChannelReserve::topUpCall { descriptor: descriptor.clone(), additionalDeposit: U96::ONE }.abi_encode(),
304 ITIP20ChannelReserve::settleCall { descriptor: descriptor.clone(), cumulativeAmount: U96::ONE, signature: vec![1, 2, 3].into() }.abi_encode(),
305 ITIP20ChannelReserve::closeCall { descriptor: descriptor.clone(), cumulativeAmount: U96::ONE, captureAmount: U96::ONE, signature: vec![1, 2, 3].into() }.abi_encode(),
306 ITIP20ChannelReserve::requestCloseCall { descriptor: descriptor.clone() }.abi_encode(),
307 ITIP20ChannelReserve::withdrawCall { descriptor }.abi_encode(),
308 ]
309 }
310
311 #[test]
312 fn test_is_payment() {
313 for calldata in payment_calldatas() {
314 assert!(ITIP20ChannelReserve::ITIP20ChannelReserveCalls::is_payment(
315 &calldata
316 ));
317 }
318
319 let mut unknown = payment_calldatas()[0].clone();
320 unknown[..4].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
321 assert!(!ITIP20ChannelReserve::ITIP20ChannelReserveCalls::is_payment(&unknown));
322 }
323
324 #[test]
325 fn test_is_payment_rejects_malformed_dynamic_calldata() {
326 let mut calldata = ITIP20ChannelReserve::settleCall {
327 descriptor: descriptor(),
328 cumulativeAmount: U96::from(1),
329 signature: vec![1, 2, 3].into(),
330 }
331 .abi_encode();
332 calldata[4 + 8 * 32 + 31] = 0;
334 assert!(!ITIP20ChannelReserve::ITIP20ChannelReserveCalls::is_payment(&calldata));
335
336 let mut oversized = ITIP20ChannelReserve::settleCall {
337 descriptor: descriptor(),
338 cumulativeAmount: U96::from(1),
339 signature: vec![0; 2048].into(),
340 }
341 .abi_encode();
342 assert!(oversized.len() > 2048);
343 assert!(!ITIP20ChannelReserve::ITIP20ChannelReserveCalls::is_payment(&oversized));
344
345 oversized.truncate(4);
346 assert!(!ITIP20ChannelReserve::ITIP20ChannelReserveCalls::is_payment(&oversized));
347 }
348}