-
Notifications
You must be signed in to change notification settings - Fork 3
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
Conversation
8855a0d
to
51e2160
Compare
There was a problem hiding this 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?
;; Check that fee is not higher than requesters max fee | ||
(asserts! (<= fee requested-max-fee) ERR_FEE_TOO_HIGH) |
There was a problem hiding this comment.
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:
- User makes a withdrawal request.
- The signers decide to act on the request.
- The signers submit a BTC transaction fulfilling the request.
- 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
;; Call into registry to confirm accepted withdrawal | ||
(try! (contract-call? .sbtc-registry complete-withdrawal request-id false none none none none)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
.
(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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- Return the result, so return
(list 100 (optional uint))
(or something like that) wherenone
means success and(some uint)
is the error number; or - 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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)}))) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
-
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. ↩
d68c040
to
8f983f6
Compare
There was a problem hiding this 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:
- The previous approach of basically returning the index that failed was relatively easy to follow in clarity.
- 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.
;; Call into registry to confirm accepted withdrawal | ||
(try! (contract-call? .sbtc-registry complete-withdrawal request-id false none none none none)) |
There was a problem hiding this comment.
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?
There was a problem hiding this 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); | ||
}); | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: new line here
;; Call into registry to confirm accepted withdrawal | ||
(try! (contract-call? .sbtc-registry complete-withdrawal request-id false none none none none)) |
There was a problem hiding this comment.
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)
.
;; Check that fee is not higher than requesters max fee | ||
(asserts! (<= fee requested-max-fee) ERR_FEE_TOO_HIGH) |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
1c4c8b0
to
cbf3ff2
Compare
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
pnpm test
passes