evm_oracle_state/pending/
chainlink.rs1use alloy_primitives::{Address, B256, I256, U256, keccak256};
4
5use super::{PendingOracleRoute, PendingOracleUpdateId};
6
7pub const CHAINLINK_TRANSMIT_SELECTOR: [u8; 4] = [0xb1, 0xdc, 0x65, 0xa4];
9pub const CHAINLINK_TRANSMIT_SECONDARY_SELECTOR: [u8; 4] = [0xba, 0x0c, 0xb2, 0x9e];
11pub const CHAINLINK_FORWARD_SELECTOR: [u8; 4] = [0x6f, 0xad, 0xcf, 0x72];
13
14const MINIMUM_PLAUSIBLE_TIMESTAMP: u64 = 1_577_836_800;
15const MAXIMUM_OBSERVATIONS: usize = 256;
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct ChainlinkPendingReport {
20 pub aggregator: Address,
22 pub forwarder: Option<Address>,
24 pub secondary: bool,
26 pub observations_timestamp: u64,
28 pub observations: Vec<I256>,
30 pub median_answer: I256,
32 pub minimum_answer: I256,
34 pub maximum_answer: I256,
36 pub report_hash: B256,
38 pub calldata_hash: B256,
40 pub inner_calldata_hash: B256,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct DecodedChainlinkOcr2 {
47 pub route: PendingOracleRoute,
49 pub report: ChainlinkPendingReport,
51}
52
53impl DecodedChainlinkOcr2 {
54 pub fn update_id(&self) -> PendingOracleUpdateId {
56 let mut identity = Vec::with_capacity(52);
57 identity.extend_from_slice(self.report.aggregator.as_slice());
58 identity.extend_from_slice(self.report.report_hash.as_slice());
59 PendingOracleUpdateId::from_hash(keccak256(identity))
60 }
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
65pub enum PendingOracleDecodeError {
66 #[error("Chainlink pending calldata is ABI-truncated at {field}")]
68 Truncated {
69 field: &'static str,
71 },
72 #[error("Chainlink pending calldata has an invalid ABI offset for {field}")]
74 InvalidOffset {
75 field: &'static str,
77 },
78 #[error("Chainlink report has implausible observations timestamp {timestamp}")]
80 ImplausibleTimestamp {
81 timestamp: u64,
83 },
84 #[error("Chainlink report has implausible observation count {count}")]
86 ImplausibleObservationCount {
87 count: usize,
89 },
90}
91
92pub fn decode_chainlink_ocr2_calldata(
98 to: Address,
99 outer_calldata: &[u8],
100) -> Result<Option<DecodedChainlinkOcr2>, PendingOracleDecodeError> {
101 let Some(outer_selector) = outer_calldata.get(..4) else {
102 return Ok(None);
103 };
104
105 let (aggregator, forwarder, inner_calldata) = if outer_selector == CHAINLINK_FORWARD_SELECTOR {
106 let args = &outer_calldata[4..];
107 let target = abi_word(args, 0, "forward target")?;
108 let aggregator = Address::from_slice(&target[12..]);
109 let inner = dynamic_bytes(args, 32, "forward calldata")?;
110 (aggregator, Some(to), inner)
111 } else {
112 (to, None, outer_calldata)
113 };
114
115 let selector = inner_calldata
116 .get(..4)
117 .ok_or(PendingOracleDecodeError::Truncated {
118 field: "transmit selector",
119 })?;
120 let secondary = if selector == CHAINLINK_TRANSMIT_SELECTOR {
121 false
122 } else if selector == CHAINLINK_TRANSMIT_SECONDARY_SELECTOR {
123 true
124 } else {
125 return Ok(None);
126 };
127
128 let transmit_args = &inner_calldata[4..];
129 let report_bytes = dynamic_bytes(transmit_args, 3 * 32, "OCR report")?;
130 let (observations_timestamp, observations) = decode_report(report_bytes)?;
131 let mut ordered = observations.clone();
132 ordered.sort_unstable();
133 let median_answer = ordered[ordered.len() / 2];
134 let minimum_answer = ordered[0];
135 let maximum_answer = ordered[ordered.len() - 1];
136 let route = match forwarder {
137 Some(forwarder) => PendingOracleRoute::ChainlinkForwarded {
138 forwarder,
139 aggregator,
140 secondary,
141 },
142 None => PendingOracleRoute::ChainlinkDirect {
143 aggregator,
144 secondary,
145 },
146 };
147
148 Ok(Some(DecodedChainlinkOcr2 {
149 route,
150 report: ChainlinkPendingReport {
151 aggregator,
152 forwarder,
153 secondary,
154 observations_timestamp,
155 observations,
156 median_answer,
157 minimum_answer,
158 maximum_answer,
159 report_hash: keccak256(report_bytes),
160 calldata_hash: keccak256(outer_calldata),
161 inner_calldata_hash: keccak256(inner_calldata),
162 },
163 }))
164}
165
166fn decode_report(report: &[u8]) -> Result<(u64, Vec<I256>), PendingOracleDecodeError> {
167 let timestamp = abi_u64(report, 0, "observations timestamp")?;
168 if timestamp < MINIMUM_PLAUSIBLE_TIMESTAMP {
169 return Err(PendingOracleDecodeError::ImplausibleTimestamp { timestamp });
170 }
171 let observations_offset = abi_usize(report, 64, "observations offset")?;
172 let count = abi_usize(report, observations_offset, "observation count")?;
173 if count == 0 || count > MAXIMUM_OBSERVATIONS {
174 return Err(PendingOracleDecodeError::ImplausibleObservationCount { count });
175 }
176
177 let mut observations = Vec::with_capacity(count);
178 for index in 0..count {
179 let offset = observations_offset
180 .checked_add(32)
181 .and_then(|offset| offset.checked_add(index * 32))
182 .ok_or(PendingOracleDecodeError::InvalidOffset {
183 field: "observation",
184 })?;
185 let value = U256::from_be_slice(abi_word(report, offset, "observation")?);
186 observations.push(I256::from_raw(value));
187 }
188 Ok((timestamp, observations))
189}
190
191fn dynamic_bytes<'a>(
192 args: &'a [u8],
193 offset_position: usize,
194 field: &'static str,
195) -> Result<&'a [u8], PendingOracleDecodeError> {
196 let offset = abi_usize(args, offset_position, field)?;
197 let length = abi_usize(args, offset, field)?;
198 let start = offset
199 .checked_add(32)
200 .ok_or(PendingOracleDecodeError::InvalidOffset { field })?;
201 let end = start
202 .checked_add(length)
203 .ok_or(PendingOracleDecodeError::InvalidOffset { field })?;
204 args.get(start..end)
205 .ok_or(PendingOracleDecodeError::Truncated { field })
206}
207
208fn abi_u64(
209 data: &[u8],
210 offset: usize,
211 field: &'static str,
212) -> Result<u64, PendingOracleDecodeError> {
213 let word = abi_word(data, offset, field)?;
214 if word[..24].iter().any(|byte| *byte != 0) {
215 return Err(PendingOracleDecodeError::InvalidOffset { field });
216 }
217 Ok(u64::from_be_bytes(
218 word[24..].try_into().expect("eight bytes"),
219 ))
220}
221
222fn abi_usize(
223 data: &[u8],
224 offset: usize,
225 field: &'static str,
226) -> Result<usize, PendingOracleDecodeError> {
227 let value = abi_u64(data, offset, field)?;
228 usize::try_from(value).map_err(|_| PendingOracleDecodeError::InvalidOffset { field })
229}
230
231fn abi_word<'a>(
232 data: &'a [u8],
233 offset: usize,
234 field: &'static str,
235) -> Result<&'a [u8], PendingOracleDecodeError> {
236 let end = offset
237 .checked_add(32)
238 .ok_or(PendingOracleDecodeError::InvalidOffset { field })?;
239 data.get(offset..end)
240 .ok_or(PendingOracleDecodeError::Truncated { field })
241}