Skip to content

Commit

Permalink
Unwrap NEAR on Aurora->NEAR transfers (#750)
Browse files Browse the repository at this point in the history
<!--
Thanks for submitting a pull request! Here are some helpful tips:

* Always create branches on and target the `develop` branch.
* Run all the tests locally and ensure that they are passing.
* Run `make format` to ensure that the code is formatted.
* Run `make check` to ensure that all checks passed successfully.
* Small commits and contributions that attempt one single goal is
preferable.
* If the idea changes or adds anything functional which will affect
users, an
AIP discussion is required first on the Aurora forum: 

https://forum.aurora.dev/discussions/AIPs%20(Aurora%20Improvement%20Proposals).
* Avoid breaking the public API (namely in engine/src/lib.rs) unless
required.
* If your PR is a WIP, ensure that you enable "draft" mode.
* Your first PRs won't use the CI automatically unless a maintainer
starts.
If this is not your first PR, please do NOT abuse the CI resources.

Checklist:
- [ ] I have performed a self-review of my code
- [ ] I have documented my code, particularly in the hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] I have added tests to prove my fix or new feature is effective and
works
- [ ] Any dependent changes have been merged
- [ ] The PR is targeting the `develop` branch and not `master`
- [ ] I have pre-squashed my commits into a single commit and rebased.
-->

## Description

NEAR instead of wNEAR on AuroraStoring native NEAR tokens on aurora
account on NEAR - issues:
* This creates issues for accounting and monitoring - need to verify
which part of `aurora` balance is used for storage, which is used for
bridged NEAR, and which is free and available.
* Could create a lack of NEAR collateral for the bridged wNEAR - some
users may not be able to withdraw their wNEAR from Aurora.

The solution:
* Keep holding wNEAR on `aurora` balance on NEAR side.
* This way, each time the user bridges NEAR to Aurora, it's being
wrapped to wNEAR and sent to Aurora.
* We preserve the same ERC-20 address of wNEAR on Aurora without any
additional steps.
* When withdrawing wNEAR from Aurora, make unwrap in wNEAR (call
`wrap.near near_withdraw()`) and then send native NEAR tokens for users.
* Use optional suffix `{}:unwrap` so it won't break the cross-chain
swaps for [Ref.Finance](http://ref.finance/).

Implementation:
Modify the `ExitToNear` precompile:
* Call `near_withdraw` instead of `ft_transfer` if the `erc20_address`
is a `wnear_address` and the `recipient` end with the suffix
`{}:unwrap`.
* Rename `refund_on_error` callback to
`exit_to_near_precompile_callback`.
* Pass to it args with optional `TransferNearCallArgs` and an already
existing `RefundCallArgs` but make it optional.
* In the `exit_to_near_precompile_callback` transfer near on successful
promise result and refund on the failed result.

## Performance / NEAR gas cost considerations
The gas consumption increased a little bit due to a callback call.
<!--
Performance regressions are not ideal, though we welcome performance 
improvements. Any PR must be completely mindful of any gas cost
increases. The
CI will fail if the gas costs change at all. Do update these tests to 
accommodate for the new gas changes. It is good to explain 
this change, if necessary.
-->

## Testing
Integration tests are added
<!--
Please describe the tests that you ran to verify your changes.
-->

## How should this be reviewed
The reviewer should focus on the changed components that are described
in the `implementation` section, and verify that the implementation is
backward compatible and doesn't break any other components like `XCC`
and `refund` logic.

---------

Co-authored-by: Oleksandr Anyshchenko <oleksandr.anyshchenko@aurora.dev>
Co-authored-by: Michael Birch <michael.birch@aurora.dev>
  • Loading branch information
3 people authored Sep 22, 2023
1 parent 0731841 commit 00f41ee
Show file tree
Hide file tree
Showing 14 changed files with 427 additions and 93 deletions.
200 changes: 148 additions & 52 deletions engine-precompiles/src/native.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
use super::{EvmPrecompileResult, Precompile};
use crate::prelude::{
format,
parameters::{PromiseArgs, PromiseCreateArgs, WithdrawCallArgs},
sdk::io::{StorageIntermediate, IO},
storage::{bytes_to_key, KeyPrefix},
types::{Address, Yocto},
vec, BorshSerialize, Cow, String, ToString, Vec, U256,
};
#[cfg(feature = "error_refund")]
use crate::prelude::{
parameters::{PromiseWithCallbackArgs, RefundCallArgs},
types,
use crate::prelude::{parameters::RefundCallArgs, types};
use crate::{
prelude::{
format,
parameters::{PromiseArgs, PromiseCreateArgs, WithdrawCallArgs},
sdk::io::{StorageIntermediate, IO},
storage::{bytes_to_key, KeyPrefix},
str,
types::{Address, Yocto},
vec, BorshSerialize, Cow, String, ToString, Vec, U256,
},
xcc::state::get_wnear_address,
};

use crate::prelude::types::EthGas;
use crate::PrecompileOutput;
use aurora_engine_types::{account_id::AccountId, types::NEP141Wei};
use aurora_engine_types::{
account_id::AccountId,
parameters::{
ExitToNearPrecompileCallbackCallArgs, PromiseWithCallbackArgs, TransferNearCallArgs,
},
types::NEP141Wei,
};
use evm::backend::Log;
use evm::{Context, ExitError};

const ERR_TARGET_TOKEN_NOT_FOUND: &str = "Target token not found";
const UNWRAP_WNEAR_MSG: &str = "unwrap";

mod costs {
use crate::prelude::types::{EthGas, NearGas};
Expand All @@ -35,9 +43,7 @@ mod costs {
pub(super) const FT_TRANSFER_GAS: NearGas = NearGas::new(10_000_000_000_000);

/// Value determined experimentally based on tests.
/// (No mainnet data available since this feature is not enabled)
#[cfg(feature = "error_refund")]
pub(super) const REFUND_ON_ERROR_GAS: NearGas = NearGas::new(5_000_000_000_000);
pub(super) const EXIT_TO_NEAR_CALLBACK_GAS: NearGas = NearGas::new(10_000_000_000_000);

// TODO(#332): Determine the correct amount of gas
pub(super) const WITHDRAWAL_GAS: NearGas = NearGas::new(100_000_000_000_000);
Expand Down Expand Up @@ -242,6 +248,28 @@ fn validate_amount(amount: U256) -> Result<(), ExitError> {
Ok(())
}

#[derive(Debug, PartialEq)]
struct Recipient<'a> {
receiver_account_id: AccountId,
message: Option<&'a str>,
}

fn parse_recipient(recipient: &[u8]) -> Result<Recipient<'_>, ExitError> {
let recipient = str::from_utf8(recipient)
.map_err(|_| ExitError::Other(Cow::from("ERR_INVALID_RECEIVER_ACCOUNT_ID")))?;
let (receiver_account_id, message) = recipient.split_once(':').map_or_else(
|| (recipient, None),
|(recipient, msg)| (recipient, Some(msg)),
);

Ok(Recipient {
receiver_account_id: receiver_account_id
.parse()
.map_err(|_| ExitError::Other(Cow::from("ERR_INVALID_RECEIVER_ACCOUNT_ID")))?,
message,
})
}

impl<I: IO> Precompile for ExitToNear<I> {
fn required_gas(_input: &[u8]) -> Result<EthGas, ExitError> {
Ok(costs::EXIT_TO_NEAR_GAS)
Expand Down Expand Up @@ -300,10 +328,8 @@ impl<I: IO> Precompile for ExitToNear<I> {
#[cfg(not(feature = "error_refund"))]
let mut input = parse_input(input)?;
let current_account_id = self.current_account_id.clone();
#[cfg(feature = "error_refund")]
let refund_on_error_target = current_account_id.clone();

let (nep141_address, args, exit_event) = match flag {
let (nep141_address, args, exit_event, method, transfer_near_args) = match flag {
0x0 => {
// ETH transfer
//
Expand All @@ -326,6 +352,8 @@ impl<I: IO> Precompile for ExitToNear<I> {
dest: dest_account.to_string(),
amount: context.apparent_value,
},
"ft_transfer",
None,
)
} else {
return Err(ExitError::Other(Cow::from(
Expand Down Expand Up @@ -355,29 +383,46 @@ impl<I: IO> Precompile for ExitToNear<I> {
input = &input[32..];

validate_amount(amount)?;
let recipient = parse_recipient(input)?;

if let Ok(receiver_account_id) = AccountId::try_from(input) {
let (args, method, transfer_near_args) = if recipient.message
== Some(UNWRAP_WNEAR_MSG)
&& erc20_address == get_wnear_address(&self.io).raw()
{
(
format!(r#"{{"amount": "{}"}}"#, amount.as_u128()),
"near_withdraw",
Some(TransferNearCallArgs {
target_account_id: recipient.receiver_account_id.clone(),
amount: amount.as_u128(),
}),
)
} else {
// There is no way to inject json, given the encoding of both arguments
// as decimal and valid account id respectively.
(
nep141_address,
// There is no way to inject json, given the encoding of both arguments
// as decimal and valid account id respectively.
format!(
r#"{{"receiver_id": "{}", "amount": "{}", "memo": null}}"#,
receiver_account_id,
recipient.receiver_account_id,
amount.as_u128()
),
events::ExitToNear {
sender: Address::new(erc20_address),
erc20_address: Address::new(erc20_address),
dest: receiver_account_id.to_string(),
amount,
},
"ft_transfer",
None,
)
} else {
return Err(ExitError::Other(Cow::from(
"ERR_INVALID_RECEIVER_ACCOUNT_ID",
)));
}
};

(
nep141_address,
args,
events::ExitToNear {
sender: Address::new(erc20_address),
erc20_address: Address::new(erc20_address),
dest: recipient.receiver_account_id.to_string(),
amount,
},
method,
transfer_near_args,
)
}
_ => return Err(ExitError::Other(Cow::from("ERR_INVALID_FLAG"))),
};
Expand All @@ -394,30 +439,37 @@ impl<I: IO> Precompile for ExitToNear<I> {
erc20_address,
amount: types::u256_to_arr(&exit_event.amount),
};
#[cfg(feature = "error_refund")]
let refund_promise = PromiseCreateArgs {
target_account_id: refund_on_error_target,
method: "refund_on_error".to_string(),
args: refund_args.try_to_vec().unwrap(),
attached_balance: Yocto::new(0),
attached_gas: costs::REFUND_ON_ERROR_GAS,

let callback_args = ExitToNearPrecompileCallbackCallArgs {
#[cfg(feature = "error_refund")]
refund: Some(refund_args),
#[cfg(not(feature = "error_refund"))]
refund: None,
transfer_near: transfer_near_args,
};

let transfer_promise = PromiseCreateArgs {
target_account_id: nep141_address,
method: "ft_transfer".to_string(),
method: method.to_string(),
args: args.as_bytes().to_vec(),
attached_balance: Yocto::new(1),
attached_gas: costs::FT_TRANSFER_GAS,
};

#[cfg(feature = "error_refund")]
let promise = PromiseArgs::Callback(PromiseWithCallbackArgs {
base: transfer_promise,
callback: refund_promise,
});
#[cfg(not(feature = "error_refund"))]
let promise = PromiseArgs::Create(transfer_promise);

let promise = if callback_args == ExitToNearPrecompileCallbackCallArgs::default() {
PromiseArgs::Create(transfer_promise)
} else {
PromiseArgs::Callback(PromiseWithCallbackArgs {
base: transfer_promise,
callback: PromiseCreateArgs {
target_account_id: self.current_account_id.clone(),
method: "exit_to_near_precompile_callback".to_string(),
args: callback_args.try_to_vec().unwrap(),
attached_balance: Yocto::new(0),
attached_gas: costs::EXIT_TO_NEAR_CALLBACK_GAS,
},
})
};
let promise_log = Log {
address: exit_to_near::ADDRESS.raw(),
topics: Vec::new(),
Expand Down Expand Up @@ -620,8 +672,10 @@ impl<I: IO> Precompile for ExitToEthereum<I> {

#[cfg(test)]
mod tests {
use super::{exit_to_ethereum, exit_to_near, validate_amount, validate_input_size};
use crate::prelude::sdk::types::near_account_to_evm_address;
use super::{
exit_to_ethereum, exit_to_near, parse_recipient, validate_amount, validate_input_size,
};
use crate::{native::Recipient, prelude::sdk::types::near_account_to_evm_address};
use aurora_engine_types::U256;

#[test]
Expand Down Expand Up @@ -687,4 +741,46 @@ mod tests {
fn test_exit_with_valid_amount() {
validate_amount(U256::from(u128::MAX)).unwrap();
}

#[test]
fn test_parse_recipient() {
assert_eq!(
parse_recipient(b"test.near").unwrap(),
Recipient {
receiver_account_id: "test.near".parse().unwrap(),
message: None
}
);

assert_eq!(
parse_recipient(b"test.near:unwrap").unwrap(),
Recipient {
receiver_account_id: "test.near".parse().unwrap(),
message: Some("unwrap")
}
);

assert_eq!(
parse_recipient(b"test.near:some_msg:with_extra_colon").unwrap(),
Recipient {
receiver_account_id: "test.near".parse().unwrap(),
message: Some("some_msg:with_extra_colon")
}
);

assert_eq!(
parse_recipient(b"test.near:").unwrap(),
Recipient {
receiver_account_id: "test.near".parse().unwrap(),
message: Some("")
}
);
}

#[test]
fn test_parse_invalid_recipient() {
assert!(parse_recipient(b"test@.near").is_err());
assert!(parse_recipient(b"test@.near:msg").is_err());
assert!(parse_recipient(&[0xc2]).is_err());
}
}
16 changes: 10 additions & 6 deletions engine-standalone-storage/src/sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,12 @@ pub fn parse_transaction_kind(
})?;
TransactionKind::RegisterRelayer(address)
}
TransactionKindTag::RefundOnError => match promise_data.first().and_then(Option::as_ref) {
None => TransactionKind::RefundOnError(None),
TransactionKindTag::ExitToNear => match promise_data.first().and_then(Option::as_ref) {
None => TransactionKind::ExitToNear(None),
Some(_) => {
let args = aurora_engine_types::parameters::RefundCallArgs::try_from_slice(&bytes)
let args = aurora_engine_types::parameters::ExitToNearPrecompileCallbackCallArgs::try_from_slice(&bytes)
.map_err(f)?;
TransactionKind::RefundOnError(Some(args))
TransactionKind::ExitToNear(Some(args))
}
},
TransactionKindTag::SetConnectorData => {
Expand Down Expand Up @@ -515,9 +515,13 @@ fn non_submit_execute<I: IO + Copy>(
None
}

TransactionKind::RefundOnError(_) => {
TransactionKind::ExitToNear(_) => {
let mut handler = crate::promise::NoScheduler { promise_data };
let maybe_result = contract_methods::connector::refund_on_error(io, env, &mut handler)?;
let maybe_result = contract_methods::connector::exit_to_near_precompile_callback(
io,
env,
&mut handler,
)?;

maybe_result.map(|submit_result| TransactionExecutionResult::Submit(Ok(submit_result)))
}
Expand Down
Loading

0 comments on commit 00f41ee

Please sign in to comment.