API crash preventing correct processing of deposits
Description
Brief/Intro
An attacker can call Emily.create_deposit and pass in a Confiered deposit to overwrite the status of the deposit to Pending, thereby causing the Signer to process a deposit repeatedly.
Note that even if the integrity issue with Emily.create_deposit is fixed, this issue will still exist, because the issue is not related to integrity, it is about re-creating an already Confiermed deposit.
Vulnerability Details
The Emily.create_deposit code is as follows.
pub async fn create_deposit(
context: EmilyContext,
body: CreateDepositRequestBody,
) -> impl warp::reply::Reply {
debug!("In create deposit");
// Internal handler so `?` can be used correctly while still returning a reply.
async fn handler(
context: EmilyContext,
body: CreateDepositRequestBody,
) -> Result<impl warp::reply::Reply, Error> {
// Set variables.
let api_state = accessors::get_api_state(&context).await?;
api_state.error_if_reorganizing()?;
let chaintip = api_state.chaintip();
let stacks_block_hash: String = chaintip.key.hash;
let stacks_block_height: u64 = chaintip.key.height;
let status = Status::Pending;
// Get parameters from scripts.
let script_parameters =
scripts_to_resource_parameters(&body.deposit_script, &body.reclaim_script)?;
// Make table entry.
let deposit_entry: DepositEntry = DepositEntry {
key: DepositEntryKey {
bitcoin_txid: body.bitcoin_txid,
bitcoin_tx_output_index: body.bitcoin_tx_output_index,
},
recipient: script_parameters.recipient,
parameters: DepositParametersEntry {
max_fee: script_parameters.max_fee,
lock_time: script_parameters.lock_time,
},
history: vec![DepositEvent {
status: StatusEntry::Pending,
message: "Just received deposit".to_string(),
stacks_block_hash: stacks_block_hash.clone(),
stacks_block_height,
}],
status,
last_update_block_hash: stacks_block_hash,
last_update_height: stacks_block_height,
amount: script_parameters.amount,
reclaim_script: body.reclaim_script,
deposit_script: body.deposit_script,
..Default::default()
};
// Validate deposit entry.
deposit_entry.validate()?;
// Add entry to the table.
accessors::add_deposit_entry(&context, &deposit_entry).await?;
// Respond.
let response: Deposit = deposit_entry.try_into()?;
Ok(with_status(json(&response), StatusCode::CREATED))
}
// Handle and respond.
handler(context, body)
.await
.map_or_else(Reply::into_response, Reply::into_response)
}
It will insert the deposit into the table in the Pending state. It will not check whether the deposit already exists. Therefore, if the deposit has been Confiermed, it will be overwritten as Pending.
Impact Details
The signer will pull all pending deposits from Emily for processing each time. If an attacker overwrites the status of all historical deposits to pending, the signer will reprocess all deposits at once. This may make it difficult for the signer to handle so many deposits. In addition, these deposits will be submitted to the Stacks chain (which will fail to execute), and the signer may lose a lot of gas fees. The signer may also be unable to submit a new deposit due to insufficient gas fees.
References
None
Proof of Concept
Proof of Concept
Add this test case to emily/handler/tests/integration/deposit.rs