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