Skip to content

Commit

Permalink
Contract: XRPL Token Registration Recovery (#51)
Browse files Browse the repository at this point in the history
# Description
- Add recovery of XRPL token registration that failed the trust set
operation. Contract owner will have to call the recovery that will only
succeed if the token is registered and is in inactive state because the
previous trust set operation has failed.
  • Loading branch information
keyleu committed Nov 27, 2023
1 parent 52558e7 commit 9ea11bc
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 9 deletions.
4 changes: 2 additions & 2 deletions contract/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ cosmwasm-schema = "1.5.0"
cosmwasm-std = { version = "1.5.0", features = ["cosmwasm_1_1"] }
cw-ownable = "0.5.1"
cw-storage-plus = "1.2.0"
cw-utils = "1.0.2"
cw-utils = "1.0.3"
cw2 = "1.1.1"
hex = "0.4.3"
serde_json = "1.0.108"
Expand Down
54 changes: 52 additions & 2 deletions contract/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ pub fn execute(
account_sequence,
number_of_tickets,
),
ExecuteMsg::RecoverXRPLTokenRegistration { issuer, currency } => {
recover_xrpl_token_registration(deps.into_empty(), info.sender, issuer, currency)
}
ExecuteMsg::SaveSignature {
operation_id,
signature,
Expand Down Expand Up @@ -595,6 +598,53 @@ fn recover_tickets(
.add_attribute("account_sequence", account_sequence.to_string()))
}

fn recover_xrpl_token_registration(
deps: DepsMut,
sender: Addr,
issuer: String,
currency: String,
) -> CoreumResult<ContractError> {
assert_owner(deps.storage, &sender)?;

let key = build_xrpl_token_key(issuer.to_owned(), currency.to_owned());

let mut token = XRPL_TOKENS
.load(deps.storage, key.to_owned())
.map_err(|_| ContractError::TokenNotRegistered {})?;

// Check that the token is in inactive state, which means the trust set operation failed.
if token.state.ne(&TokenState::Inactive) {
return Err(ContractError::XRPLTokenNotInactive {});
}

// Put the state back to Processing since we are going to try to activate it again.
token.state = TokenState::Processing;
XRPL_TOKENS.save(deps.storage, key, &token)?;

// Create the pending operation to approve the token again
let config = CONFIG.load(deps.storage)?;
let ticket = allocate_ticket(deps.storage)?;

create_pending_operation(
deps.storage,
Some(ticket),
None,
OperationType::TrustSet {
issuer: issuer.to_owned(),
currency: currency.to_owned(),
trust_set_limit_amount: config.trust_set_limit_amount,
},
)?;

Ok(Response::new()
.add_attribute(
"action",
ContractActions::RecoverXRPLTokenRegistration.as_str(),
)
.add_attribute("issuer", issuer)
.add_attribute("currency", currency))
}

fn save_signature(
deps: DepsMut,
sender: Addr,
Expand Down Expand Up @@ -740,7 +790,7 @@ fn query_xrpl_tokens(
limit: Option<u32>,
) -> StdResult<XRPLTokensResponse> {
let limit = limit.unwrap_or(MAX_PAGE_LIMIT).min(MAX_PAGE_LIMIT);
let offset = offset.unwrap_or(0);
let offset = offset.unwrap_or_default();
let tokens: Vec<XRPLToken> = XRPL_TOKENS
.range(deps.storage, None, None, Order::Ascending)
.skip(offset as usize)
Expand All @@ -758,7 +808,7 @@ fn query_coreum_tokens(
limit: Option<u32>,
) -> StdResult<CoreumTokensResponse> {
let limit = limit.unwrap_or(MAX_PAGE_LIMIT).min(MAX_PAGE_LIMIT);
let offset = offset.unwrap_or(0);
let offset = offset.unwrap_or_default();
let tokens: Vec<CoreumToken> = COREUM_TOKENS
.range(deps.storage, None, None, Order::Ascending)
.skip(offset as usize)
Expand Down
7 changes: 6 additions & 1 deletion contract/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,17 @@ pub enum ContractError {
#[error("XRPLTokenNotEnabled: This token must be enabled to be bridged")]
XRPLTokenNotEnabled {},

#[error("CoreumOriginatedTokenDisabled: This token is currently disabled and can't be bridged")]
#[error(
"CoreumOriginatedTokenDisabled: This token is currently disabled and can't be bridged"
)]
CoreumOriginatedTokenDisabled {},

#[error("XRPLTokenNotInProcessing: This token must be in processing state to be enabled")]
XRPLTokenNotInProcessing {},

#[error("XRPLTokenNotInactive: To recover this token it must be inactive")]
XRPLTokenNotInactive {},

#[error("AmountSentIsZeroAfterTruncation: Amount sent is zero after truncating to sending precision")]
AmountSentIsZeroAfterTruncation {},

Expand Down
5 changes: 5 additions & 0 deletions contract/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ pub enum ExecuteMsg {
account_sequence: u64,
number_of_tickets: Option<u32>,
},
#[serde(rename = "recover_xrpl_token_registration")]
RecoverXRPLTokenRegistration {
issuer: String,
currency: String,
},
SaveSignature {
operation_id: u64,
signature: String,
Expand Down
4 changes: 2 additions & 2 deletions contract/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub fn check_operation_exists(
ticket_sequence: Option<u64>,
) -> Result<u64, ContractError> {
// Get the sequence or ticket number (priority for sequence number)
let operation_id = account_sequence.unwrap_or(ticket_sequence.unwrap_or_default());
let operation_id = account_sequence.unwrap_or_else(|| ticket_sequence.unwrap());

if !PENDING_OPERATIONS.has(storage, operation_id) {
return Err(ContractError::PendingOperationNotFound {});
Expand All @@ -64,7 +64,7 @@ pub fn create_pending_operation(
operation_type,
};

let operation_id = ticket_sequence.unwrap_or(account_sequence.unwrap_or_default());
let operation_id = ticket_sequence.unwrap_or_else(|| account_sequence.unwrap());
if PENDING_OPERATIONS.has(storage, operation_id) {
return Err(ContractError::PendingOperationAlreadyExists {});
}
Expand Down
2 changes: 2 additions & 0 deletions contract/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ pub enum ContractActions {
RegisterXRPLToken,
SendFromXRPLToCoreum,
RecoverTickets,
RecoverXRPLTokenRegistration,
XRPLTransactionResult,
SaveSignature,
SendToXRPL,
Expand All @@ -154,6 +155,7 @@ impl ContractActions {
ContractActions::RegisterXRPLToken => "register_xrpl_token",
ContractActions::SendFromXRPLToCoreum => "send_from_xrpl_to_coreum",
ContractActions::RecoverTickets => "recover_tickets",
ContractActions::RecoverXRPLTokenRegistration => "recover_xrpl_token_registration",
ContractActions::XRPLTransactionResult => "submit_xrpl_transaction_result",
ContractActions::SaveSignature => "save_signature",
ContractActions::SendToXRPL => "send_to_xrpl",
Expand Down
196 changes: 196 additions & 0 deletions contract/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3842,6 +3842,202 @@ mod tests {
assert_eq!(query_available_tickets.tickets, tickets.clone());
}

#[test]
fn xrpl_token_registration_recovery() {
let app = CoreumTestApp::new();
let signer = app
.init_account(&coins(100_000_000_000, FEE_DENOM))
.unwrap();
let wasm = Wasm::new(&app);
let asset_ft = AssetFT::new(&app);

let relayer = Relayer {
coreum_address: Addr::unchecked(signer.address()),
xrpl_address: generate_xrpl_address(),
xrpl_pub_key: generate_xrpl_pub_key(),
};

let token_issuer = generate_xrpl_address();
let token_currency = "BTC".to_string();
let token = XRPLToken {
issuer: token_issuer.to_owned(),
currency: token_currency.to_owned(),
sending_precision: -15,
max_holding_amount: 100,
};

let contract_addr = store_and_instantiate(
&wasm,
&signer,
Addr::unchecked(signer.address()),
vec![relayer.clone()],
1,
2,
Uint128::new(TRUST_SET_LIMIT_AMOUNT),
query_issue_fee(&asset_ft),
generate_xrpl_address(),
);

// We successfully recover 3 tickets to perform operations
wasm.execute::<ExecuteMsg>(
&contract_addr,
&ExecuteMsg::RecoverTickets {
account_sequence: 1,
number_of_tickets: Some(3),
},
&vec![],
&signer,
)
.unwrap();

wasm.execute::<ExecuteMsg>(
&contract_addr,
&ExecuteMsg::SaveEvidence {
evidence: Evidence::XRPLTransactionResult {
tx_hash: Some(generate_hash()),
account_sequence: Some(1),
ticket_sequence: None,
transaction_result: TransactionResult::Accepted,
operation_result: OperationResult::TicketsAllocation {
tickets: Some(vec![1, 2, 3]),
},
},
},
&vec![],
&signer,
)
.unwrap();

// We perform the register token operation, which should put the token to Processing state and create the PendingOperation
wasm.execute::<ExecuteMsg>(
&contract_addr,
&ExecuteMsg::RegisterXRPLToken {
issuer: token.issuer.clone(),
currency: token.currency.clone(),
sending_precision: token.sending_precision,
max_holding_amount: Uint128::new(token.max_holding_amount),
},
&query_issue_fee(&asset_ft),
&signer,
)
.unwrap();

// If we try to recover a token that is not in Inactive state, it should fail.
let recover_error = wasm
.execute::<ExecuteMsg>(
&contract_addr,
&ExecuteMsg::RecoverXRPLTokenRegistration {
issuer: token.issuer.clone(),
currency: token.currency.clone(),
},
&vec![],
&signer,
)
.unwrap_err();

assert!(recover_error
.to_string()
.contains(ContractError::XRPLTokenNotInactive {}.to_string().as_str()));

// If we try to recover a token that is not registered, it should fail
let recover_error = wasm
.execute::<ExecuteMsg>(
&contract_addr,
&ExecuteMsg::RecoverXRPLTokenRegistration {
issuer: token.issuer.clone(),
currency: "NOT".to_string(),
},
&vec![],
&signer,
)
.unwrap_err();

assert!(recover_error
.to_string()
.contains(ContractError::TokenNotRegistered {}.to_string().as_str()));

// Let's fail the trust set operation to put the token to Inactive so that we can recover it

let query_pending_operations = wasm
.query::<QueryMsg, PendingOperationsResponse>(
&contract_addr,
&QueryMsg::PendingOperations {},
)
.unwrap();

assert_eq!(query_pending_operations.operations.len(), 1);

wasm.execute::<ExecuteMsg>(
&contract_addr,
&ExecuteMsg::SaveEvidence {
evidence: Evidence::XRPLTransactionResult {
tx_hash: Some(generate_hash()),
account_sequence: None,
ticket_sequence: Some(
query_pending_operations.operations[0]
.ticket_sequence
.unwrap(),
),
transaction_result: TransactionResult::Rejected,
operation_result: OperationResult::TrustSet {
issuer: token.issuer.clone(),
currency: token.currency.clone(),
},
},
},
&[],
&signer,
)
.unwrap();

let query_pending_operations = wasm
.query::<QueryMsg, PendingOperationsResponse>(
&contract_addr,
&QueryMsg::PendingOperations {},
)
.unwrap();

assert!(query_pending_operations.operations.is_empty());

// We should be able to recover the token now
wasm.execute::<ExecuteMsg>(
&contract_addr,
&ExecuteMsg::RecoverXRPLTokenRegistration {
issuer: token.issuer.clone(),
currency: token.currency.clone(),
},
&vec![],
&signer,
)
.unwrap();

let query_pending_operations = wasm
.query::<QueryMsg, PendingOperationsResponse>(
&contract_addr,
&QueryMsg::PendingOperations {},
)
.unwrap();

assert_eq!(query_pending_operations.operations.len(), 1);
assert_eq!(
query_pending_operations.operations[0],
Operation {
ticket_sequence: Some(
query_pending_operations.operations[0]
.ticket_sequence
.unwrap()
),
account_sequence: None,
signatures: vec![],
operation_type: OperationType::TrustSet {
issuer: token_issuer,
currency: token_currency,
trust_set_limit_amount: Uint128::new(TRUST_SET_LIMIT_AMOUNT),
},
}
);
}

#[test]
fn rejected_ticket_allocation_with_no_tickets_left() {
let app = CoreumTestApp::new();
Expand Down
2 changes: 1 addition & 1 deletion contract/src/tickets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub fn register_used_ticket(storage: &mut dyn Storage) -> Result<bool, ContractE
if used_tickets + 1 >= config.used_ticket_sequence_threshold
&& !PENDING_TICKET_UPDATE.load(storage)?
{
// If our creation of a ticket allocation operation failed because we have no tickets left, we need to propagate
// If our creation of a ticket allocation operation failed because we have no tickets left, we need to propagate
// this so that we are aware that we need to allocate new tickets because we've run out of them
match reserve_ticket(storage) {
Ok(ticket_to_update) => {
Expand Down

0 comments on commit 9ea11bc

Please sign in to comment.