Skip to content

Commit

Permalink
Merge pull request #253 from jbencin/fix/handle-failed-ft-deposits
Browse files Browse the repository at this point in the history
Fix: Refund failed FT deposits
  • Loading branch information
jbencin authored Apr 13, 2023
2 parents 37652e7 + b32b7eb commit 84fb7e0
Show file tree
Hide file tree
Showing 3 changed files with 660 additions and 62 deletions.
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

0 comments on commit 84fb7e0

Please sign in to comment.