Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Refund failed FT deposits #253

Merged
merged 5 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions core-contracts/contracts/helper/simple-ft-l2-no-deposit.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
(define-constant ERR_NOT_AUTHORIZED (err u1001))

(impl-trait 'ST000000000000000000002AMW42H.subnet.ft-trait)

(define-fungible-token ft-token)

;; get the token balance of owner
(define-read-only (get-balance (owner principal))
(ok (ft-get-balance ft-token owner)))

;; returns the total number of tokens
(define-read-only (get-total-supply)
(ok (ft-get-supply ft-token)))

;; returns the token name
(define-read-only (get-name)
(ok "ft-token"))

;; the symbol or "ticker" for this token
(define-read-only (get-symbol)
(ok "EXFT"))

;; the number of decimals used
(define-read-only (get-decimals)
(ok u0))

;; Transfers tokens to a recipient
(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))
(begin
(asserts! (is-eq tx-sender sender) ERR_NOT_AUTHORIZED)
(try! (ft-transfer? ft-token amount sender recipient))
(print memo)
(ok true)
)
)

(define-read-only (get-token-uri)
(ok none)
)

(define-public (gift-tokens (amount uint) (recipient principal))
(begin
(asserts! (is-eq tx-sender recipient) ERR_NOT_AUTHORIZED)
(ft-mint? ft-token amount recipient)
)
)

(impl-trait 'ST000000000000000000002AMW42H.subnet.subnet-asset)

;; Called for deposit from the burnchain to the subnet
(define-public (deposit-from-burnchain (amount uint) (recipient principal))
ERR_NOT_AUTHORIZED
)

;; Called for withdrawal from the subnet to the burnchain
(define-public (burn-for-withdrawal (amount uint) (owner principal))
(begin
(asserts! (is-eq tx-sender owner) ERR_NOT_AUTHORIZED)
(ft-burn? ft-token amount owner)
)
)
138 changes: 86 additions & 52 deletions src/chainstate/stacks/db/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,49 @@ impl Streamer for MicroblockStreamData {
}
}

enum Token {
Nft { id: u128 },
Ft { amount: u128 },
}

fn make_withdrawal_event(
subnet_contract_id: QualifiedContractIdentifier,
sender: PrincipalData,
token: Token,
mainnet: bool,
) -> StacksTransactionEvent {
let (withdrawal_type, withdrawal_value) = match token {
Token::Nft { id } => ("nft", ("id".into(), Value::UInt(id))),
Token::Ft { amount } => ("ft", ("amount".into(), Value::UInt(amount))),
};

let values = vec![
("sender".into(), Value::Principal(sender)),
(
"event".into(),
Value::string_ascii_from_bytes("withdraw".into())
.expect("Supplied string was not ASCII"),
),
(
"type".into(),
Value::string_ascii_from_bytes(withdrawal_type.into())
.expect("Supplied string was not ASCII"),
),
(
"asset-contract".into(),
Value::Principal(PrincipalData::Contract(subnet_contract_id)),
),
withdrawal_value,
];

StacksTransactionEvent::SmartContractEvent(SmartContractEventData {
key: (boot_code_id("subnet", mainnet), "print".into()),
value: TupleData::from_data(values)
.expect("Failed to create tuple data.")
.into(),
})
}

impl StacksChainState {
fn get_index_block_pathbuf(blocks_dir: &str, index_block_hash: &StacksBlockId) -> PathBuf {
let block_hash_bytes = index_block_hash.as_bytes();
Expand Down Expand Up @@ -4612,7 +4655,7 @@ impl StacksChainState {
clarity_tx.with_temporary_cost_tracker(LimitedCostTracker::new_free(), |clarity_tx| {
operations
.into_iter()
.filter_map(|deposit_stx_op| {
.map(|deposit_stx_op| {
let DepositStxOp {
txid: _,
amount,
Expand All @@ -4632,7 +4675,7 @@ impl StacksChainState {
// deposits increment the STX liquidity in the layer 2
clarity_tx.increment_ustx_liquid_supply(amount);

Some(StacksTransactionReceipt {
StacksTransactionReceipt {
transaction: TransactionOrigin::Burn(deposit_stx_op.into()),
events: vec![result],
result: Value::okay_true(),
Expand All @@ -4642,7 +4685,7 @@ impl StacksChainState {
execution_cost: ExecutionCost::zero(),
microblock_header: None,
tx_index: 0,
})
}
})
.collect()
});
Expand Down Expand Up @@ -4677,7 +4720,7 @@ impl StacksChainState {
None,
&subnet_contract_id,
DEPOSIT_FUNCTION_NAME,
&[Value::UInt(amount), Value::Principal(sender)],
&[Value::UInt(amount), Value::Principal(sender.clone())],
|_, _| false,
)
});
Expand All @@ -4687,17 +4730,40 @@ impl StacksChainState {
.expect("BUG: cost declined between executions");

match result {
Ok((value, _, events)) => Some(StacksTransactionReceipt {
transaction: TransactionOrigin::Burn(deposit_ft_op.into()),
events,
result: value,
post_condition_aborted: false,
stx_burned: 0,
contract_analysis: None,
execution_cost,
microblock_header: None,
tx_index: 0,
}),
Ok((value, _, mut events)) => {
// Examine response to see if transaction failed
let deposit_op_failed = match &value {
Value::Response(r) => r.committed == false,
_ => {
// Public functions should always return type `Response`
error!("DepositFt op returned unexpected value"; "value" => %value);
false
}
};

// If deposit fails, create a withdrawal event to send NFT back to user
if deposit_op_failed {
info!("DepositFt op failed. Issue withdrawal tx");
events.push(make_withdrawal_event(
subnet_contract_id,
sender,
Token::Ft { amount },
mainnet,
));
};

Some(StacksTransactionReceipt {
transaction: TransactionOrigin::Burn(deposit_ft_op.into()),
events,
result: value,
post_condition_aborted: false,
stx_burned: 0,
contract_analysis: None,
execution_cost,
microblock_header: None,
tx_index: 0,
})
}
Err(e) => {
info!("DepositFt op processing error.";
"error" => ?e,
Expand All @@ -4710,45 +4776,13 @@ impl StacksChainState {
.collect()
}

fn make_nft_withdrawal_event(
subnet_contract_id: QualifiedContractIdentifier,
sender: PrincipalData,
id: u128,
mainnet: bool,
) -> StacksTransactionEvent {
StacksTransactionEvent::SmartContractEvent(SmartContractEventData {
key: (boot_code_id("subnet", mainnet), "print".into()),
value: TupleData::from_data(vec![
(
"event".into(),
Value::string_ascii_from_bytes("withdraw".into())
.expect("Supplied string was not ASCII"),
),
(
"type".into(),
Value::string_ascii_from_bytes("nft".into())
.expect("Supplied string was not ASCII"),
),
("sender".into(), Value::Principal(sender)),
("id".into(), Value::UInt(id)),
(
"asset-contract".into(),
Value::Principal(PrincipalData::Contract(subnet_contract_id)),
),
])
.expect("Failed to create tuple data.")
.into(),
})
}

/// Process any deposit NFT operations that haven't been processed in this
/// subnet fork yet.
pub fn process_deposit_nft_ops(
clarity_tx: &mut ClarityTx,
operations: Vec<DepositNftOp>,
) -> Vec<StacksTransactionReceipt> {
let mainnet = clarity_tx.config.mainnet;
let boot_addr = PrincipalData::from(boot_code_addr(mainnet));
let cost_so_far = clarity_tx.cost_so_far();
// return valid receipts
operations
Expand All @@ -4764,7 +4798,7 @@ impl StacksChainState {
} = deposit_nft_op.clone();
let result = clarity_tx.connection().as_transaction(|tx| {
tx.run_contract_call(
&boot_addr,
&boot_code_addr(mainnet).into(),
None,
&subnet_contract_id,
DEPOSIT_FUNCTION_NAME,
Expand All @@ -4783,19 +4817,19 @@ impl StacksChainState {
let deposit_op_failed = match &value {
Value::Response(r) => r.committed == false,
_ => {
// TODO: Do anything for the case where `value` is not of type `Value::Response()`?
warn!("DepositNft op returned unexpected value"; "value" => %value);
// Public functions should always return type `Response`
error!("DepositNft op returned unexpected value"; "value" => %value);
false
}
};

// If deposit fails, create a withdrawal event to send NFT back to user
if deposit_op_failed {
info!("DepositNft op failed. Issue withdrawal tx");
events.push(Self::make_nft_withdrawal_event(
events.push(make_withdrawal_event(
subnet_contract_id,
sender,
id,
Token::Nft { id },
mainnet,
));
};
Expand Down
Loading