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

[Clarity] Complete Accepted Withdrawal Request #243

Merged
merged 21 commits into from
Jul 3, 2024

Conversation

setzeus
Copy link
Collaborator

@setzeus setzeus commented Jun 19, 2024

Description

This PR deals with #95, #96, & #97 with the focus of initiating, accepting & rejecting withdrawal requests. As a signer, once completed, I want to mark a withdrawal as completed on-chain & therefore burning the sBTC.

Type of Change

New feature planned in #95 that comes from the sBTC Clarity documentation.

Does this introduce a breaking change?

No.

Are documentation updates required?

No.

Testing information

Six different unit tests included here that can be ran CDing into 'contracts' & running 'pnpm test.'

Checklist

  • Code is commented where needed
  • Unit test coverage for new or modified code paths
  • pnpm test passes
  • Changelog is updated
  • Tag 1 of @person1 or @Person2

@setzeus setzeus force-pushed the feat/accept-withdrawal-lock branch 2 times, most recently from 8855a0d to 51e2160 Compare June 23, 2024 18:56
@setzeus setzeus requested review from djordon and netrome June 24, 2024 14:50
Copy link
Contributor

@djordon djordon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This largely looks good, but I think we can simplify the 'complete-many' function by specializing it into two functions or just have it care about acceptances. Also, I don't think we should throw an error if the fee was wrong. Maybe we should print something instead?

contracts/contracts/sbtc-withdrawal.clar Outdated Show resolved Hide resolved
Comment on lines +67 to +68
;; Check that fee is not higher than requesters max fee
(asserts! (<= fee requested-max-fee) ERR_FEE_TOO_HIGH)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting check. It made me think of the withdrawal flow:

  1. User makes a withdrawal request.
  2. The signers decide to act on the request.
  3. The signers submit a BTC transaction fulfilling the request.
  4. The signers wait for the transaction to be confirmed and then make a contract call to this function.

If the signers somehow make a mistake and submit a transaction with a fee that was higher than the users' max fee, what then? With this check the contract call fails but it probably should not fail since the transaction has been confirmed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's an interesting scenario. I mean, the withdrawal hasn't been fulfilled if the signers exceed the users' max fee. I think the user should be able to reclaim their sBTC, or at least a portion of it, in that scenario.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, it's not obvious what the right course of action should be here. Giving them some back feels right but if we do that then the amount minted is no longer 1:1 with the amount under the signers' control. Well, I'm okay with letting this pass for now and we can revisit later. The sBTC is still locked, so I think it's fine.

contracts/contracts/sbtc-withdrawal.clar Outdated Show resolved Hide resolved
Comment on lines 103 to 104
;; Call into registry to confirm accepted withdrawal
(try! (contract-call? .sbtc-registry complete-withdrawal request-id false none none none none))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be the following?

(try! (contract-call? .sbtc-registry complete-withdrawal request-id false none (some signer-bitmap) none none))

Also, does it matter?

Copy link
Collaborator Author

@setzeus setzeus Jun 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I believe the signer-bitmap is needed just for the print event.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so is it actually not needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I was confused and unintentionally confusing before. I think the only thing that needs to change here is changing the none in the third argument in the complete-withdrawal function call to (some signer-bitmap).

Comment on lines 119 to 138
(fold complete-individual-withdrawal-helper withdrawals (ok u0))
)
)

(define-private (complete-individual-withdrawal-helper (withdrawal {request-id: uint, status: bool, signer-bitmap: uint, bitcoin-txid: (optional (buff 32)), output-index: (optional uint), fee: (optional uint)}) (helper-response (response uint uint)))
(match helper-response
index
Copy link
Contributor

@djordon djordon Jun 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this short-circuiting? If one of the withdrawals inputs was set erroneously, I wouldn't think that the error would have an effect on any of the other withdrawals in the input.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, confused here? If one withdrawal fails they'll all fail. When that happens we want to know where the error is thrown so if the error message is already thrown we don't want to update it but return it.

Copy link
Contributor

@djordon djordon Jun 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If one withdrawal fails they'll all fail. When that happens we want to know where the error is thrown so if the error message is already thrown we don't want to update it but return it.

Yup, that makes sense. If we were going to go this route, I think we should should just return a tuple with the error number and the index as distinct fields.

But, I was thinking that the complete-withdrawals function would always try each withdrawal input and either:

  1. Return the result, so return (list 100 (optional uint)) (or something like that) where none means success and (some uint) is the error number; or
  2. Return a tuple for the failures, so return (list 100 {error_number: uint, index: uint}).

The downside is that this would ideally lead to (list 100 none) (or whatever type you go with) being committed on chain, while the upside is that the signers wouldn't need to reprocess failures (since they would be making the same contract call with kinda the same inputs). If we short-circuit then there could be withdrawals after the failed entry that can succeed and the signers need to make another contract call to try those.

Thoughts?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: error messages
We can change the return of the (ok response) to include a tuple but not sure I follow how returning a tuple for the error case would work? All error messages across the protocol currently return a uint, including this a similar list mechanic found in sbtc-deposit. I don't think breaking this convention makes sense.

Re: processing w/ failures
This is a great point that I think merits more thought since it'd refactor this mechanic in other places. I'd like to leave this as-is & open a discussion for further consideration, does that work?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: error messages We can change the return of the (ok response) to include a tuple but not sure I follow how returning a tuple for the error case would work? All error messages across the protocol currently return a uint, including this a similar list mechanic found in sbtc-deposit. I don't think breaking this convention makes sense.

Eh, I'm more than okay with breaking this convention here (actually, is it only a convention or is it required?). But I think we can side-step this issue and just figure out if we should short-circuit.

Re: processing w/ failures This is a great point that I think merits more thought since it'd refactor this mechanic in other places. I'd like to leave this as-is & open a discussion for further consideration, does that work?

Great idea, let's leave this alone for now until we discuss it with everyone else.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uint as error messages are a universal convention (& requirement for a lot of trait contracts), not strictly a requirement.

Copy, opened here: #261

)
)
;; Reject multiple withdrawal requests
(define-public (complete-withdrawals (withdrawals (list 100 {request-id: uint, status: bool, signer-bitmap: uint, bitcoin-txid: (optional (buff 32)), output-index: (optional uint), fee: (optional uint)})))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify this functionality (or break it up into complete-accept-withdrawals and complete-reject-withdrawals or whatever you want to call it). The signers need to wait for a bitcoin confirmation to complete an accepted withdrawal, but they can complete a rejection right away (well, right after "voting"). So bundling acceptances makes sense, we find out about them in bundles when the transaction package gets confirmed on bitcoin. The flow for rejections should be much faster, since it would happen shortly after it gets confirmed on a stacks Nakamoto block.

Also, I think I prefer breaking them out since it'll be easier to reason about both here and in the signer codebase.

Copy link
Collaborator Author

@setzeus setzeus Jun 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. Hmmm, my thinking here is giving the signers flexibility to bundle both accepts & rejects together to submit a single list & therefore make fewer transaction calls. Which, while separating them might improve readability, the real pro-argument here that is signers have to make more transactions, which means they have to pay more fees.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh right, good point.

There is a decent chance that having it as one function will not lead to fewer transaction calls. We might have signers make a contract call for rejects every ~5-30 seconds1 and then make another for the accepts every ~10 minutes (for new bitcoin blocks) since this approach has the better user experience.


Anyway, I'm okay leaving this as is for now. We can always revisit this later when we get this part nailed down in the signer.

Footnotes

  1. The signers might reject shortly after they find out about it, which is when the request is included in a new Nakamoto block. I'm not sure how quickly Nakamoto blocks are supposed to be created.

contracts/tests/sbtc-withdrawal.test.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@djordon djordon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As much as I like the new functionality (which I advocated for) I feel like this approach adds more complexity than it is worth. The issues I have are:

  1. The previous approach of basically returning the index that failed was relatively easy to follow in clarity.
  2. The new approach has a limit of around ~20 errors before the string-to-uint? function fails. This is okay, but adds more complexity that the signers need to handle.

Sorry, but I think I prefer the previous approach. I suppose this also means that there is no need to update the other clarity functions.

Comment on lines 103 to 104
;; Call into registry to confirm accepted withdrawal
(try! (contract-call? .sbtc-registry complete-withdrawal request-id false none none none none))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so is it actually not needed?

contracts/contracts/sbtc-withdrawal.clar Outdated Show resolved Hide resolved
Copy link
Contributor

@djordon djordon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, basically some small nits.

);
expect(receipt.value).toEqual(2n);
});
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: new line here

contracts/contracts/sbtc-registry.clar Show resolved Hide resolved
Comment on lines 103 to 104
;; Call into registry to confirm accepted withdrawal
(try! (contract-call? .sbtc-registry complete-withdrawal request-id false none none none none))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I was confused and unintentionally confusing before. I think the only thing that needs to change here is changing the none in the third argument in the complete-withdrawal function call to (some signer-bitmap).

Comment on lines +67 to +68
;; Check that fee is not higher than requesters max fee
(asserts! (<= fee requested-max-fee) ERR_FEE_TOO_HIGH)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, it's not obvious what the right course of action should be here. Giving them some back feels right but if we do that then the amount minted is no longer 1:1 with the amount under the signers' control. Well, I'm okay with letting this pass for now and we can revisit later. The sBTC is still locked, so I think it's fine.

Copy link
Contributor

@djordon djordon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Feel free to merge once the tests pass.

@setzeus setzeus force-pushed the feat/accept-withdrawal-lock branch from 1c4c8b0 to cbf3ff2 Compare July 3, 2024 14:31
@setzeus setzeus merged commit 467f5a3 into main Jul 3, 2024
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants