aurora_engine_precompiles/
xcc.rs1use aurora_engine_sdk::io::IO;
6use aurora_engine_types::{
7 Cow, H160, H256, U256, Vec,
8 account_id::AccountId,
9 borsh::{self, BorshDeserialize},
10 format,
11 parameters::{CrossContractCallArgs, PromiseCreateArgs},
12 str,
13 types::{Address, EthGas, NearGas, balance::ZERO_YOCTO},
14 vec,
15};
16use aurora_evm::ExitError;
17use aurora_evm::backend::Log;
18use aurora_evm::executor::stack::{PrecompileFailure, PrecompileHandle};
19
20use crate::{HandleBasedPrecompile, PrecompileOutput, utils};
21
22pub mod costs {
23 use crate::prelude::types::{EthGas, NearGas};
24
25 pub const CROSS_CONTRACT_CALL_BASE: EthGas = EthGas::new(343_650);
37 pub const CROSS_CONTRACT_CALL_BYTE: EthGas = EthGas::new(4);
40 pub const CROSS_CONTRACT_CALL_NEAR_GAS: u64 = 175_000_000;
46
47 pub const ROUTER_EXEC_BASE: NearGas = NearGas::new(7_000_000_000_000);
48 pub const ROUTER_EXEC_PER_CALLBACK: NearGas = NearGas::new(12_000_000_000_000);
49 pub const ROUTER_SCHEDULE: NearGas = NearGas::new(5_000_000_000_000);
50}
51
52mod consts {
53 pub(super) const ERR_INVALID_INPUT: &str = "ERR_INVALID_XCC_INPUT";
54 pub(super) const ERR_SERIALIZE: &str = "ERR_XCC_CALL_SERIALIZE";
55 pub(super) const ERR_STATIC: &str = "ERR_INVALID_IN_STATIC";
56 pub(super) const ERR_DELEGATE: &str = "ERR_INVALID_IN_DELEGATE";
57 pub(super) const ERR_XCC_ACCOUNT_ID: &str = "ERR_FAILED_TO_CREATE_XCC_ACCOUNT_ID";
58 pub(super) const ROUTER_EXEC_NAME: &str = "execute";
59 pub(super) const ROUTER_SCHEDULE_NAME: &str = "schedule";
60 pub(super) const TRANSFER_FROM_SELECTOR: [u8; 4] = [0x23, 0xb8, 0x72, 0xdd];
63}
64
65pub struct CrossContractCall<I> {
66 io: I,
67 engine_account_id: AccountId,
68}
69
70impl<I> CrossContractCall<I> {
71 pub const fn new(engine_account_id: AccountId, io: I) -> Self {
72 Self {
73 io,
74 engine_account_id,
75 }
76 }
77}
78
79pub mod cross_contract_call {
80 use aurora_engine_types::{
81 H256,
82 types::{Address, make_address},
83 };
84
85 pub const ADDRESS: Address = make_address(0x516cded1, 0xd16af10cad47d6d49128e2eb7d27b372);
90
91 pub const AMOUNT_TOPIC: H256 = crate::make_h256(
94 0x0072657175697265645f6e656172,
95 0x0072657175697265645f6e656172,
96 );
97}
98
99impl<I: IO> HandleBasedPrecompile for CrossContractCall<I> {
100 #[allow(clippy::too_many_lines)]
101 fn run_with_handle(
102 &self,
103 handle: &mut impl PrecompileHandle,
104 ) -> Result<PrecompileOutput, PrecompileFailure> {
105 let input = handle.input();
106 let target_gas = handle.gas_limit().map(EthGas::new);
107 let context = handle.context();
108 utils::validate_no_value_attached_to_precompile(context.apparent_value)?;
109 let is_static = handle.is_static();
110
111 let input_len = u64::try_from(input.len()).map_err(utils::err_usize_conv)?;
114 let mut cost =
115 costs::CROSS_CONTRACT_CALL_BASE + costs::CROSS_CONTRACT_CALL_BYTE * input_len;
116 let check_cost = |cost: EthGas| -> Result<(), PrecompileFailure> {
117 if let Some(target_gas) = target_gas {
118 if cost > target_gas {
119 return Err(PrecompileFailure::Error {
120 exit_status: ExitError::OutOfGas,
121 });
122 }
123 }
124 Ok(())
125 };
126 check_cost(cost)?;
127
128 if is_static {
130 return Err(revert_with_message(consts::ERR_STATIC));
131 } else if context.address != cross_contract_call::ADDRESS.raw() {
132 return Err(revert_with_message(consts::ERR_DELEGATE));
133 }
134
135 let sender = context.caller;
136 let target_account_id = create_target_account_id(sender, self.engine_account_id.as_ref())?;
137 let args = CrossContractCallArgs::try_from_slice(input)
138 .map_err(|_| ExitError::Other(Cow::from(consts::ERR_INVALID_INPUT)))?;
139 let (promise, attached_near) = match args {
140 CrossContractCallArgs::Eager(call) => {
141 let call_gas = call.total_gas();
142 let attached_near = call.total_near();
143 let callback_count = call
144 .promise_count()
145 .checked_sub(1)
146 .ok_or_else(|| ExitError::Other(Cow::from(consts::ERR_INVALID_INPUT)))?;
147 let router_exec_cost = costs::ROUTER_EXEC_BASE
148 + NearGas::new(callback_count * costs::ROUTER_EXEC_PER_CALLBACK.as_u64());
149 let promise = PromiseCreateArgs {
150 target_account_id,
151 method: consts::ROUTER_EXEC_NAME.into(),
152 args: borsh::to_vec(&call)
153 .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?,
154 attached_balance: ZERO_YOCTO,
155 attached_gas: router_exec_cost.saturating_add(call_gas),
156 };
157 (promise, attached_near)
158 }
159 CrossContractCallArgs::Delayed(call) => {
160 let attached_near = call.total_near();
161 let promise = PromiseCreateArgs {
162 target_account_id,
163 method: consts::ROUTER_SCHEDULE_NAME.into(),
164 args: borsh::to_vec(&call)
165 .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?,
166 attached_balance: ZERO_YOCTO,
167 attached_gas: costs::ROUTER_SCHEDULE,
170 };
171 (promise, attached_near)
172 }
173 };
174 cost += EthGas::new(promise.attached_gas.as_u64() / costs::CROSS_CONTRACT_CALL_NEAR_GAS);
175 check_cost(cost)?;
176
177 let required_near =
178 match state::get_code_version_of_address(&self.io, &Address::new(sender)) {
179 None => attached_near + state::STORAGE_AMOUNT,
181 Some(_) => attached_near,
182 };
183 if required_near != ZERO_YOCTO {
185 let engine_implicit_address = aurora_engine_sdk::types::near_account_to_evm_address(
186 self.engine_account_id.as_bytes(),
187 );
188 let tx_data = transfer_from_args(
189 sender.0.into(),
190 engine_implicit_address.raw().0.into(),
191 required_near.as_u128().into(),
192 );
193 let wnear_address = state::get_wnear_address(&self.io);
194 let context = aurora_evm::Context {
195 address: wnear_address.raw(),
196 caller: cross_contract_call::ADDRESS.raw(),
197 apparent_value: U256::zero(),
198 };
199 let (exit_reason, return_value) =
200 handle.call(wnear_address.raw(), None, tx_data, None, false, &context);
201 match exit_reason {
202 aurora_evm::ExitReason::Succeed(_) => (),
204 aurora_evm::ExitReason::Revert(r) => {
205 return Err(PrecompileFailure::Revert {
206 exit_status: r,
207 output: return_value,
208 });
209 }
210 aurora_evm::ExitReason::Error(e) => {
211 return Err(PrecompileFailure::Error { exit_status: e });
212 }
213 aurora_evm::ExitReason::Fatal(f) => {
214 return Err(PrecompileFailure::Fatal { exit_status: f });
215 }
216 }
217 }
218
219 let topics = vec![
220 cross_contract_call::AMOUNT_TOPIC,
221 H256(aurora_engine_types::types::u256_to_arr(&U256::from(
222 required_near.as_u128(),
223 ))),
224 ];
225
226 let promise_log = Log {
227 address: cross_contract_call::ADDRESS.raw(),
228 topics,
229 data: borsh::to_vec(&promise)
230 .map_err(|_| ExitError::Other(Cow::from(consts::ERR_SERIALIZE)))?,
231 };
232
233 Ok(PrecompileOutput {
234 logs: vec![promise_log],
235 cost,
236 ..Default::default()
237 })
238 }
239}
240
241pub mod state {
242 use aurora_engine_sdk::error::ReadU32Error;
245 use aurora_engine_sdk::io::{IO, StorageIntermediate};
246 use aurora_engine_types::parameters::xcc::CodeVersion;
247 use aurora_engine_types::storage::{self, KeyPrefix};
248 use aurora_engine_types::types::{Address, Yocto};
249
250 pub const ERR_CORRUPTED_STORAGE: &str = "ERR_CORRUPTED_XCC_STORAGE";
251 pub const ERR_MISSING_WNEAR_ADDRESS: &str = "ERR_MISSING_WNEAR_ADDRESS";
252 pub const VERSION_KEY: &[u8] = b"version";
253 pub const WNEAR_KEY: &[u8] = b"wnear";
254 pub const STORAGE_AMOUNT: Yocto = Yocto::new(2_000_000_000_000_000_000_000_000);
256
257 pub fn get_wnear_address<I: IO>(io: &I) -> Address {
263 let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, WNEAR_KEY);
264 io.read_storage(&key).map_or_else(
265 || panic!("{ERR_MISSING_WNEAR_ADDRESS}"),
266 |bytes| Address::try_from_slice(&bytes.to_vec()).expect(ERR_CORRUPTED_STORAGE),
267 )
268 }
269
270 pub fn get_latest_code_version<I: IO>(io: &I) -> CodeVersion {
272 let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, VERSION_KEY);
273 read_version(io, &key).unwrap_or_default()
274 }
275
276 pub fn get_code_version_of_address<I: IO>(io: &I, address: &Address) -> Option<CodeVersion> {
278 let key = storage::bytes_to_key(KeyPrefix::CrossContractCall, address.as_bytes());
279 read_version(io, &key)
280 }
281
282 fn read_version<I: IO>(io: &I, key: &[u8]) -> Option<CodeVersion> {
284 match io.read_u32(key) {
285 Ok(value) => Some(CodeVersion(value)),
286 Err(ReadU32Error::MissingValue) => None,
287 Err(ReadU32Error::InvalidU32) => panic!("{}", ERR_CORRUPTED_STORAGE),
288 }
289 }
290}
291
292fn transfer_from_args(from: ethabi::Address, to: ethabi::Address, amount: ethabi::Uint) -> Vec<u8> {
293 let args = ethabi::encode(&[
294 ethabi::Token::Address(from),
295 ethabi::Token::Address(to),
296 ethabi::Token::Uint(amount),
297 ]);
298 [&consts::TRANSFER_FROM_SELECTOR, args.as_slice()].concat()
299}
300
301fn create_target_account_id(
302 sender: H160,
303 engine_account_id: &str,
304) -> Result<AccountId, PrecompileFailure> {
305 let mut buffer = [0; 40];
306 hex::encode_to_slice(sender.as_bytes(), &mut buffer)
307 .map_err(|_| revert_with_message(consts::ERR_XCC_ACCOUNT_ID))?;
308 let sender_in_hex =
309 str::from_utf8(&buffer).map_err(|_| revert_with_message(consts::ERR_XCC_ACCOUNT_ID))?;
310
311 AccountId::try_from(format!("{sender_in_hex}.{engine_account_id}"))
312 .map_err(|_| revert_with_message(consts::ERR_XCC_ACCOUNT_ID))
313}
314
315fn revert_with_message(message: &str) -> PrecompileFailure {
316 PrecompileFailure::Revert {
317 exit_status: aurora_evm::ExitRevert::Reverted,
318 output: message.as_bytes().to_vec(),
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use crate::prelude::sdk::types::near_account_to_evm_address;
325 use crate::xcc::cross_contract_call;
326 use aurora_engine_types::vec;
327 use rand::Rng;
328
329 #[test]
330 fn test_precompile_id() {
331 assert_eq!(
332 cross_contract_call::ADDRESS,
333 near_account_to_evm_address(b"nearCrossContractCall")
334 );
335 }
336
337 #[test]
338 fn test_transfer_from_encoding() {
339 let mut rng = rand::rng();
340
341 let from = rng.random::<[u8; 20]>().into();
342 let to = rng.random::<[u8; 20]>().into();
343 let amount = rng.random::<[u8; 32]>().into();
344
345 #[allow(deprecated)]
346 let transfer_from_function = ethabi::Function {
347 name: "transferFrom".into(),
348 inputs: vec![
349 ethabi::Param {
350 name: "from".into(),
351 kind: ethabi::ParamType::Address,
352 internal_type: None,
353 },
354 ethabi::Param {
355 name: "to".into(),
356 kind: ethabi::ParamType::Address,
357 internal_type: None,
358 },
359 ethabi::Param {
360 name: "amount".into(),
361 kind: ethabi::ParamType::Uint(256),
362 internal_type: None,
363 },
364 ],
365 outputs: vec![ethabi::Param {
366 name: String::new(),
367 kind: ethabi::ParamType::Bool,
368 internal_type: None,
369 }],
370 constant: None,
371 state_mutability: ethabi::StateMutability::NonPayable,
372 };
373
374 let expected_tx_data = transfer_from_function
375 .encode_input(&[
376 ethabi::Token::Address(from),
377 ethabi::Token::Address(to),
378 ethabi::Token::Uint(amount),
379 ])
380 .unwrap();
381
382 assert_eq!(
383 super::transfer_from_args(from, to, amount),
384 expected_tx_data
385 );
386 }
387}