diff --git a/crates/replace/src/lib.rs b/crates/replace/src/lib.rs index 8f534c2cda..f527f78e5e 100644 --- a/crates/replace/src/lib.rs +++ b/crates/replace/src/lib.rs @@ -380,6 +380,20 @@ impl Pallet { Ok(()) } + fn accept_replace_tokens( + old_vault_id: &DefaultVaultId, + new_vault_id: &DefaultVaultId, + redeemable_tokens: &Amount, + ) -> DispatchResult { + // increase old-vault's to-be-redeemed tokens - this should never fail + ext::vault_registry::try_increase_to_be_redeemed_tokens::(old_vault_id, redeemable_tokens)?; + + // increase new-vault's to-be-issued tokens - this will fail if there is insufficient collateral + ext::vault_registry::try_increase_to_be_issued_tokens::(new_vault_id, redeemable_tokens)?; + + Ok(()) + } + fn _accept_replace( old_vault_id: DefaultVaultId, new_vault_id: DefaultVaultId, @@ -424,11 +438,7 @@ impl Pallet { ext::vault_registry::try_deposit_collateral::(&new_vault_id, &actual_new_vault_collateral)?; - // increase old-vault's to-be-redeemed tokens - this should never fail - ext::vault_registry::try_increase_to_be_redeemed_tokens::(&old_vault_id, &redeemable_tokens)?; - - // increase new-vault's to-be-issued tokens - this will fail if there is insufficient collateral - ext::vault_registry::try_increase_to_be_issued_tokens::(&new_vault_id, &redeemable_tokens)?; + Self::accept_replace_tokens(&old_vault_id, &new_vault_id, &redeemable_tokens)?; ext::vault_registry::transfer_funds( CurrencySource::AvailableReplaceCollateral(old_vault_id.clone()), @@ -466,9 +476,10 @@ impl Pallet { Ok(()) } - fn _execute_replace(replace_id: H256, raw_merkle_proof: Vec, raw_tx: Vec) -> Result<(), DispatchError> { - // Retrieve the ReplaceRequest as per the replaceId parameter from Vaults in the VaultRegistry - let replace = Self::get_open_replace_request(&replace_id)?; + fn _execute_replace(replace_id: H256, raw_merkle_proof: Vec, raw_tx: Vec) -> DispatchResult { + // retrieve the replace request using the id parameter + // we can still execute cancelled requests + let replace = Self::get_open_or_cancelled_replace_request(&replace_id)?; let griefing_collateral: Amount = replace.griefing_collateral(); let amount = replace.amount(); @@ -489,17 +500,35 @@ impl Pallet { replace_id, )?; + // only return griefing collateral if not already slashed + let collateral = match replace.status { + ReplaceRequestStatus::Pending => { + // give old-vault the griefing collateral + ext::vault_registry::transfer_funds( + CurrencySource::ActiveReplaceCollateral(old_vault_id.clone()), + CurrencySource::FreeBalance(old_vault_id.account_id.clone()), + &griefing_collateral, + )?; + // NOTE: this is just the additional collateral already locked on accept + // it is only used in the ReplaceTokens event + collateral + } + ReplaceRequestStatus::Cancelled => { + // we need to re-accept first, this will check that the vault is over the secure threshold + Self::accept_replace_tokens(&old_vault_id, &new_vault_id, &amount)?; + // no additional collateral locked for this + Amount::zero(collateral.currency()) + } + ReplaceRequestStatus::Completed => { + // we never enter this branch as completed requests are filtered + return Err(Error::::ReplaceCompleted.into()); + } + }; + // decrease old-vault's issued & to-be-redeemed tokens, and // change new-vault's to-be-issued tokens to issued tokens ext::vault_registry::replace_tokens::(&old_vault_id, &new_vault_id, &amount, &collateral)?; - // Give oldvault back its griefing collateral - ext::vault_registry::transfer_funds( - CurrencySource::ActiveReplaceCollateral(old_vault_id.clone()), - CurrencySource::FreeBalance(old_vault_id.account_id.clone()), - &griefing_collateral, - )?; - // Emit ExecuteReplace event. Self::deposit_event(Event::::ExecuteReplace { replace_id: replace_id, @@ -547,7 +576,7 @@ impl Pallet { )?; // if the new_vault locked additional collateral especially for this replace, - // release it if it does not cause him to be undercollateralized + // release it if it does not cause them to be undercollateralized if !ext::vault_registry::is_vault_liquidated::(&new_vault_id)? && ext::vault_registry::is_allowed_to_withdraw_collateral::(&new_vault_id, &collateral)? { @@ -612,6 +641,15 @@ impl Pallet { } } + /// Get a open or cancelled replace request by id. Completed requests are not returned. + pub fn get_open_or_cancelled_replace_request(id: &H256) -> Result, DispatchError> { + let request = >::get(id).ok_or(Error::::ReplaceIdNotFound)?; + match request.status { + ReplaceRequestStatus::Pending | ReplaceRequestStatus::Cancelled => Ok(request), + ReplaceRequestStatus::Completed => Err(Error::::ReplaceCompleted.into()), + } + } + fn insert_replace_request(key: &H256, value: &DefaultReplaceRequest) { >::insert(key, value) } diff --git a/crates/replace/src/tests.rs b/crates/replace/src/tests.rs index 846cfd12ef..a5211266e9 100644 --- a/crates/replace/src/tests.rs +++ b/crates/replace/src/tests.rs @@ -1,4 +1,7 @@ -use crate::{ext, mock::*, ReplaceRequest, ReplaceRequestStatus}; +use crate::{ + mock::{CurrencyId, *}, + *, +}; use bitcoin::types::{MerkleProof, Transaction}; use btc_relay::BtcAddress; @@ -142,11 +145,11 @@ mod accept_replace_tests { BtcAddress::default() )); assert_event_matches!(Event::AcceptReplace{ - replace_id: _, + replace_id: _, old_vault_id: OLD_VAULT, new_vault_id: NEW_VAULT, - amount: 5, - collateral: 10, + amount: 5, + collateral: 10, btc_address: addr} if addr == BtcAddress::default()); }) } @@ -171,7 +174,7 @@ mod accept_replace_tests { old_vault_id: OLD_VAULT, new_vault_id: NEW_VAULT, amount: 4, - collateral: 8, + collateral: 8, btc_address: addr} if addr == BtcAddress::default()); }) } @@ -196,11 +199,11 @@ mod execute_replace_test { use super::*; fn setup_mocks() { - Replace::get_open_replace_request.mock_safe(move |_| { + ReplaceRequests::::insert(H256::zero(), { let mut replace = test_request(); replace.old_vault = OLD_VAULT; replace.new_vault = NEW_VAULT; - MockResult::Return(Ok(replace)) + replace }); Replace::replace_period.mock_safe(|| MockResult::Return(20)); @@ -212,6 +215,9 @@ mod execute_replace_test { ext::vault_registry::replace_tokens::.mock_safe(|_, _, _, _| MockResult::Return(Ok(()))); Amount::::unlock_on.mock_safe(|_, _| MockResult::Return(Ok(()))); ext::vault_registry::transfer_funds::.mock_safe(|_, _, _| MockResult::Return(Ok(()))); + + ext::vault_registry::try_increase_to_be_redeemed_tokens::.mock_safe(|_, _| MockResult::Return(Ok(()))); + ext::vault_registry::try_increase_to_be_issued_tokens::.mock_safe(|_, _| MockResult::Return(Ok(()))); } #[test] @@ -226,6 +232,28 @@ mod execute_replace_test { }); }) } + + #[test] + fn should_execute_cancelled_request() { + run_test(|| { + setup_mocks(); + + ReplaceRequests::::insert(H256::zero(), { + let mut replace = test_request(); + replace.old_vault = OLD_VAULT; + replace.new_vault = NEW_VAULT; + replace.status = ReplaceRequestStatus::Cancelled; + replace + }); + + assert_ok!(Replace::_execute_replace(H256::zero(), Vec::new(), Vec::new())); + assert_event_matches!(Event::ExecuteReplace { + replace_id: _, + old_vault_id: OLD_VAULT, + new_vault_id: NEW_VAULT + }); + }) + } } mod cancel_replace_tests { diff --git a/standalone/runtime/tests/mock/mod.rs b/standalone/runtime/tests/mock/mod.rs index a4b15809ad..6adf1ae74c 100644 --- a/standalone/runtime/tests/mock/mod.rs +++ b/standalone/runtime/tests/mock/mod.rs @@ -324,6 +324,16 @@ pub struct UserData { pub balances: BTreeMap, } +pub trait Collateral { + fn collateral(&self, amount: Balance) -> Amount; +} + +impl Collateral for VaultId { + fn collateral(&self, amount: Balance) -> Amount { + Amount::new(amount, self.collateral_currency()) + } +} + pub trait Wrapped { fn wrapped(&self, amount: Balance) -> Amount; } diff --git a/standalone/runtime/tests/mock/replace_testing_utils.rs b/standalone/runtime/tests/mock/replace_testing_utils.rs index 1fe4913e71..3d4ae2881a 100644 --- a/standalone/runtime/tests/mock/replace_testing_utils.rs +++ b/standalone/runtime/tests/mock/replace_testing_utils.rs @@ -14,6 +14,15 @@ pub fn setup_replace( old_vault_id: &VaultId, new_vault_id: &VaultId, issued_tokens: Amount, +) -> (ReplaceRequest, H256) { + setup_replace_with_collateral(old_vault_id, new_vault_id, issued_tokens, old_vault_id.collateral(0)) +} + +pub fn setup_replace_with_collateral( + old_vault_id: &VaultId, + new_vault_id: &VaultId, + issued_tokens: Amount, + collateral: Amount, ) -> (ReplaceRequest, H256) { let new_vault_btc_address = BtcAddress::P2PKH(H160([2; 20])); @@ -23,7 +32,7 @@ pub fn setup_replace( &old_vault_id, &new_vault_id, issued_tokens, - griefing(0), + collateral, new_vault_btc_address, ) .unwrap(); diff --git a/standalone/runtime/tests/test_replace.rs b/standalone/runtime/tests/test_replace.rs index af5d41f884..811cd79d17 100644 --- a/standalone/runtime/tests/test_replace.rs +++ b/standalone/runtime/tests/test_replace.rs @@ -744,12 +744,12 @@ fn integration_test_replace_cancel_replace() { // liquidation tests fn execute_replace(replace_id: H256) -> DispatchResultWithPostInfo { - let replace = ReplacePallet::get_open_replace_request(&replace_id).unwrap(); + let replace = ReplacePallet::get_open_or_cancelled_replace_request(&replace_id).unwrap(); execute_replace_with_amount(replace_id, replace.amount()) } fn execute_replace_with_amount(replace_id: H256, amount: Amount) -> DispatchResultWithPostInfo { - let replace = ReplacePallet::get_open_replace_request(&replace_id).unwrap(); + let replace = ReplacePallet::get_open_or_cancelled_replace_request(&replace_id).unwrap(); // send the btc from the old_vault to the new_vault let (_tx_id, _tx_block_height, merkle_proof, raw_tx, _) = generate_transaction_and_mine( @@ -918,6 +918,66 @@ fn integration_test_replace_execute_replace_both_vaults_liquidated() { }); } +#[test] +fn integration_test_replace_execute_replace_with_cancelled() { + test_with(|old_vault_id, new_vault_id| { + let issued_tokens = old_vault_id.wrapped(1000); + let (replace, replace_id) = setup_replace(&old_vault_id, &new_vault_id, issued_tokens); + + let before = ParachainTwoVaultState::get(&old_vault_id, &new_vault_id); + cancel_replace(replace_id); + assert_ok!(execute_replace(replace_id)); + + assert_eq!( + ParachainTwoVaultState::get(&old_vault_id, &new_vault_id), + before.with_changes(|old_vault, new_vault, _| { + new_vault.to_be_issued -= issued_tokens; + new_vault.issued += issued_tokens; + old_vault.to_be_redeemed -= issued_tokens; + old_vault.issued -= issued_tokens; + + old_vault.griefing_collateral -= replace.griefing_collateral(); + *new_vault.free_balance.get_mut(&DEFAULT_GRIEFING_CURRENCY).unwrap() += replace.griefing_collateral(); + }) + ); + }); +} + +#[test] +fn integration_test_replace_execute_replace_with_additional_and_cancelled() { + test_with(|old_vault_id, new_vault_id| { + let issued_tokens = old_vault_id.wrapped(1000); + let collateral = old_vault_id.collateral(100000); + let (replace, replace_id) = + setup_replace_with_collateral(&old_vault_id, &new_vault_id, issued_tokens, collateral); + + let before = ParachainTwoVaultState::get(&old_vault_id, &new_vault_id); + cancel_replace(replace_id); + assert_ok!(execute_replace(replace_id)); + + assert_eq!( + ParachainTwoVaultState::get(&old_vault_id, &new_vault_id), + before.with_changes(|old_vault, new_vault, _| { + new_vault.to_be_issued -= issued_tokens; + new_vault.issued += issued_tokens; + old_vault.to_be_redeemed -= issued_tokens; + old_vault.issued -= issued_tokens; + + // griefing collateral is slashed + old_vault.griefing_collateral -= replace.griefing_collateral(); + *new_vault.free_balance.get_mut(&DEFAULT_GRIEFING_CURRENCY).unwrap() += replace.griefing_collateral(); + + // backing collateral is returned + new_vault.backing_collateral -= replace.collateral().unwrap(); + *new_vault + .free_balance + .get_mut(&new_vault_id.collateral_currency()) + .unwrap() += replace.collateral().unwrap(); + }) + ); + }); +} + #[test] fn integration_test_replace_cancel_replace_success() { test_with(|old_vault_id, new_vault_id| {