Skip to content

Commit

Permalink
Fix(xcc): Method in router contract for upgradability (#866)
Browse files Browse the repository at this point in the history
## Description

The initial XCC implementation was meant to allow the contracts deployed
to the sub-accounts created by the Engine as part of the XCC flow to be
upgradable. However, that upgrade code did not work as intended. The
problem is that Near enforces [only an account
itself](https://github.com/near/nearcore/blob/83fe943e4e270db87a89535037d2b3a8909a2c6d/runtime/runtime/src/actions.rs#L852)
can use the `DeployContract` action after the account has been created
(if the account is being created during that receipt then
`DeployContract` is allowed to be part of the batch). But the initial
implementation attempted to push the `DeployContract` action directly,
causing an `ActorNoPermission` error.

The correct way to implement an upgrade mechanism is to have the
deployed contract contain a method which accepts new code then creates a
receipt to itself with the `DeployContract` action. This PR changes the
XCC Router contract to include such a method.

Unfortunately since V1 XCC Routers did not have any upgrade method, and
they have no access keys associated with them, they can never be
upgraded. After releasing the V2 Router contract with upgradability we
may encourage existing XCC users (for example the fast bridge) to
migrate to a new XCC account.

## Performance / NEAR gas cost considerations

N/A

## Testing

A new integration test for upgrading XCC contracts is included in this
PR.
  • Loading branch information
birchmd authored Nov 16, 2023
1 parent 1e327df commit f72aa84
Show file tree
Hide file tree
Showing 10 changed files with 357 additions and 60 deletions.
Binary file not shown.
Binary file added engine-tests/src/tests/res/aurora_v3.3.1.wasm
Binary file not shown.
Binary file added engine-tests/src/tests/res/xcc_router_v1.wasm
Binary file not shown.
234 changes: 215 additions & 19 deletions engine-tests/src/tests/xcc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ use std::path::Path;
const WNEAR_AMOUNT: NearToken = NearToken::from_near(500);
const STORAGE_AMOUNT: NearToken = NearToken::from_near(50);

const XCC_ROUTER_BASE_PATH: &str = "../etc/xcc-router";
const XCC_ROUTER_VERSION_RELATIVE_PATH: &str = "src/VERSION";

#[test]
#[allow(clippy::too_many_lines)]
fn test_xcc_eth_gas_cost() {
Expand Down Expand Up @@ -356,12 +359,25 @@ fn approve_erc20(
}

pub fn contract_bytes() -> Vec<u8> {
let base_path = Path::new("../etc").join("xcc-router");
let base_path = Path::new(XCC_ROUTER_BASE_PATH);
let output_path = base_path.join("target/wasm32-unknown-unknown/release/xcc_router.wasm");
utils::rust::compile(base_path);
fs::read(output_path).unwrap()
}

pub fn router_version() -> u32 {
let base_path = Path::new(XCC_ROUTER_BASE_PATH);
let file_path = base_path.join(XCC_ROUTER_VERSION_RELATIVE_PATH);
let version = fs::read_to_string(file_path).unwrap();
version.trim().parse().unwrap()
}

pub fn change_router_version(version: u32) {
let base_path = Path::new(XCC_ROUTER_BASE_PATH);
let file_path = base_path.join(XCC_ROUTER_VERSION_RELATIVE_PATH);
fs::write(file_path, format!("{version}\n")).unwrap();
}

fn make_fib_promise(n: usize, account_id: &AccountId) -> NearPromise {
if n == 0 {
NearPromise::Simple(SimpleNearPromise::Create(PromiseCreateArgs {
Expand Down Expand Up @@ -390,9 +406,10 @@ fn make_fib_promise(n: usize, account_id: &AccountId) -> NearPromise {
pub mod workspace {
use crate::tests::xcc::{check_fib_result, WNEAR_AMOUNT};
use crate::utils;
use crate::utils::solidity::erc20::{ERC20Constructor, ERC20};
use crate::utils::workspace::{
create_sub_account, deploy_engine, deploy_erc20_from_nep_141, deploy_nep_141,
nep_141_balance_of, transfer_nep_141_to_erc_20,
create_sub_account, deploy_engine, deploy_engine_v331, deploy_erc20_from_nep_141,
deploy_nep_141, get_xcc_router_version, nep_141_balance_of, transfer_nep_141_to_erc_20,
};
use aurora_engine_precompiles::xcc::cross_contract_call;
use aurora_engine_transactions::legacy::TransactionLegacy;
Expand Down Expand Up @@ -614,6 +631,152 @@ pub mod workspace {
check_fib_result(&output, usize::try_from(n).unwrap());
}

#[allow(clippy::too_many_lines)]
#[tokio::test]
async fn test_xcc_upgrade() {
const DEPOSIT_AMOUNT: u128 = 1;

// Deploy v3.3.1 Engine with the XCC router contract it had at the time.
let v1_bytes = std::fs::read("src/tests/res/xcc_router_v1.wasm").unwrap();
let XccTestContext {
aurora,
mut signer,
signer_address,
chain_id,
wnear_account,
} = inner_init_xcc(v1_bytes, true).await.unwrap();

let router_account_id = create_router_account_id(&signer_address, &aurora);

// Do XCC interaction to create router account
let promise = PromiseCreateArgs {
target_account_id: wnear_account.id(),
method: "near_deposit".into(),
args: b"{}".to_vec(),
attached_balance: Yocto::new(1),
attached_gas: NearGas::new(5_000_000_000_000),
};
let promise_args = PromiseArgs::Create(promise);
let xcc_args = CrossContractCallArgs::Eager(promise_args);
submit_xcc_transaction(&xcc_args, &aurora, &mut signer, chain_id)
.await
.unwrap();
assert_eq!(
nep_141_balance_of(&wnear_account, &router_account_id).await,
DEPOSIT_AMOUNT,
);

// Upgrade to latest engine code
aurora
.stage_upgrade(utils::AuroraRunner::get_engine_code())
.max_gas()
.transact()
.await
.unwrap();
aurora.deploy_upgrade().max_gas().transact().await.unwrap();

// Upgrade to Engine to have latest XCC
let current_xcc_version = super::router_version();
aurora
.factory_update(super::contract_bytes())
.max_gas()
.transact()
.await
.unwrap();

// Confirm that XCC v1 router account still works
submit_xcc_transaction(&xcc_args, &aurora, &mut signer, chain_id)
.await
.unwrap();
assert_eq!(
nep_141_balance_of(&wnear_account, &router_account_id).await,
2 * DEPOSIT_AMOUNT,
);

// Create new account on Aurora
let mut v2_signer = utils::Signer::random();
let v2_signer_address = utils::address_from_secret_key(&v2_signer.secret_key);
let wnear_address = aurora.factory_get_wnear_address().await.unwrap().result;
let wnear_erc20 = {
let constructor = ERC20Constructor::load();
let contract = constructor.0.deployed_at(wnear_address);
ERC20(contract)
};
transfer_nep_141_to_erc_20(
&wnear_account,
&wnear_erc20,
&aurora.root(),
v2_signer_address,
WNEAR_AMOUNT.as_yoctonear(),
&aurora,
)
.await
.unwrap();
approve_xcc_precompile(&wnear_erc20, &aurora, chain_id, &mut v2_signer)
.await
.unwrap();

// Use XCC to create account with v2 router contract
let v2_router_account_id = create_router_account_id(&v2_signer_address, &aurora);
submit_xcc_transaction(&xcc_args, &aurora, &mut v2_signer, chain_id)
.await
.unwrap();
assert_eq!(
nep_141_balance_of(&wnear_account, &v2_router_account_id).await,
DEPOSIT_AMOUNT,
);
assert_eq!(
get_xcc_router_version(&aurora, &v2_router_account_id).await,
current_xcc_version,
);

// Upgrade to Engine to have fake XCC v3
super::change_router_version(current_xcc_version + 1);
aurora
.factory_update(super::contract_bytes())
.max_gas()
.transact()
.await
.unwrap();

// Use v2 XCC router account and confirm it is upgraded to v3
submit_xcc_transaction(&xcc_args, &aurora, &mut v2_signer, chain_id)
.await
.unwrap();
assert_eq!(
nep_141_balance_of(&wnear_account, &v2_router_account_id).await,
2 * DEPOSIT_AMOUNT,
);
assert_eq!(
get_xcc_router_version(&aurora, &v2_router_account_id).await,
current_xcc_version + 1,
);

// Upgrade Engine to have fake XCC v4
super::change_router_version(current_xcc_version + 2);
aurora
.factory_update(super::contract_bytes())
.max_gas()
.transact()
.await
.unwrap();

// Use XCC direct funding and confirm upgrade still happens
aurora
.fund_xcc_sub_account(v2_signer_address, Some(wnear_account.id()))
.max_gas()
.transact()
.await
.unwrap();
assert_eq!(
get_xcc_router_version(&aurora, &v2_router_account_id).await,
current_xcc_version + 2,
);

// Restore XCC router version to not leave the git repo dirty
super::change_router_version(current_xcc_version);
}

#[allow(clippy::too_many_lines, clippy::future_not_send)]
async fn test_xcc_precompile_common(is_scheduled: bool) {
let XccTestContext {
Expand Down Expand Up @@ -760,12 +923,23 @@ pub mod workspace {
);
}

/// Default XCC initialization (latest Aurora Engine code + latest XCC router code).
async fn init_xcc() -> anyhow::Result<XccTestContext> {
inner_init_xcc(super::contract_bytes(), false).await
}

/// Deploys the EVM, sets xcc router code, deploys wnear contract, bridges wnear into EVM,
/// and calls `factory_set_wnear_address`
async fn init_xcc() -> anyhow::Result<XccTestContext> {
let aurora = deploy_engine().await;
async fn inner_init_xcc(
xcc_wasm_bytes: Vec<u8>,
use_v331: bool,
) -> anyhow::Result<XccTestContext> {
let aurora = if use_v331 {
deploy_engine_v331().await
} else {
deploy_engine().await
};
let chain_id = aurora.get_chain_id().await?.result.as_u64();
let xcc_wasm_bytes = super::contract_bytes();
let result = aurora.factory_update(xcc_wasm_bytes).transact().await?;
assert!(result.is_success());

Expand Down Expand Up @@ -795,18 +969,7 @@ pub mod workspace {
let wnear_address = aurora.factory_get_wnear_address().await.unwrap().result;
assert_eq!(wnear_address, wnear_erc20.0.address);

let approve_tx = wnear_erc20.approve(
cross_contract_call::ADDRESS,
WNEAR_AMOUNT.as_yoctonear().into(),
signer.use_nonce().into(),
);
let signed_transaction =
utils::sign_transaction(approve_tx, Some(chain_id), &signer.secret_key);
let result = aurora
.submit(rlp::encode(&signed_transaction).to_vec())
.transact()
.await?;
assert!(result.is_success());
approve_xcc_precompile(&wnear_erc20, &aurora, chain_id, &mut signer).await?;

Ok(XccTestContext {
aurora,
Expand All @@ -825,6 +988,39 @@ pub mod workspace {
pub wnear_account: RawContract,
}

fn create_router_account_id(signer_address: &Address, aurora: &EngineContract) -> AccountId {
let router_account = format!(
"{}.{}",
hex::encode(signer_address.as_bytes()),
aurora.id().as_ref()
);
router_account.parse().unwrap()
}

/// The signer approves the XCC precompile to spend its wrapped NEAR
async fn approve_xcc_precompile(
wnear_erc20: &ERC20,
aurora: &EngineContract,
chain_id: u64,
signer: &mut utils::Signer,
) -> anyhow::Result<()> {
let approve_tx = wnear_erc20.approve(
cross_contract_call::ADDRESS,
WNEAR_AMOUNT.as_yoctonear().into(),
signer.use_nonce().into(),
);
let signed_transaction =
utils::sign_transaction(approve_tx, Some(chain_id), &signer.secret_key);
let result = aurora
.submit(rlp::encode(&signed_transaction).to_vec())
.transact()
.await?;
if !result.is_success() {
return Err(anyhow::Error::msg("Failed Approve transaction"));
};
Ok(())
}

async fn submit_xcc_transaction(
xcc_args: &CrossContractCallArgs,
aurora: &EngineContract,
Expand Down Expand Up @@ -898,7 +1094,7 @@ pub mod workspace {
let result = aurora
.root()
.call(&wrap_account.id(), "near_deposit")
.deposit(WNEAR_AMOUNT)
.deposit(WNEAR_AMOUNT.saturating_mul(3))
.transact()
.await?;
assert!(result.is_success(), "{result:?}");
Expand Down
9 changes: 9 additions & 0 deletions engine-tests/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,15 @@ impl AuroraRunner {
std::fs::read(path).unwrap()
}

pub fn get_engine_v331_code() -> Vec<u8> {
let path = if cfg!(feature = "ext-connector") {
"src/tests/res/aurora_silo_v3.3.1.wasm"
} else {
"src/tests/res/aurora_v3.3.1.wasm"
};
std::fs::read(path).unwrap()
}

pub const fn get_default_chain_id() -> u64 {
DEFAULT_CHAIN_ID
}
Expand Down
21 changes: 19 additions & 2 deletions engine-tests/src/utils/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,16 @@ pub async fn deploy_engine_with_code(code: Vec<u8>) -> EngineContract {
.unwrap()
}

#[allow(clippy::let_and_return)]
pub async fn deploy_engine() -> EngineContract {
let code = AuroraRunner::get_engine_code();
inner_deploy_engine(AuroraRunner::get_engine_code()).await
}

pub async fn deploy_engine_v331() -> EngineContract {
inner_deploy_engine(AuroraRunner::get_engine_v331_code()).await
}

#[allow(clippy::let_and_return)]
async fn inner_deploy_engine(code: Vec<u8>) -> EngineContract {
let contract = deploy_engine_with_code(code).await;

#[cfg(feature = "ext-connector")]
Expand Down Expand Up @@ -84,6 +91,16 @@ async fn init_eth_connector(aurora: &EngineContract) -> anyhow::Result<()> {
Ok(())
}

pub async fn get_xcc_router_version(aurora: &EngineContract, xcc_account: &AccountId) -> u32 {
aurora
.root()
.view(xcc_account, "get_version")
.await
.unwrap()
.json::<u32>()
.unwrap()
}

pub async fn create_sub_account(
master_account: &Account,
account: &str,
Expand Down
2 changes: 2 additions & 0 deletions engine-types/src/parameters/xcc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ pub struct WithdrawWnearToRouterArgs {
pub struct CodeVersion(pub u32);

impl CodeVersion {
pub const ONE: Self = Self(1);

#[must_use]
pub const fn increment(self) -> Self {
Self(self.0 + 1)
Expand Down
Loading

0 comments on commit f72aa84

Please sign in to comment.