-
Notifications
You must be signed in to change notification settings - Fork 41
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
Expose revealDepositWithExtraData
in the Bridge
contract
#760
Changes from all commits
727e669
23aa5fc
182988d
481d020
dfe6d88
d12b0e1
4ad2b60
2a7d54b
78dc18c
103411a
a78cc16
165c474
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,25 @@ import "./Wallets.sol"; | |
/// Since each depositor has their own Ethereum address and their own | ||
/// blinding factor, each depositor’s script is unique, and the hash | ||
/// of each depositor’s script is unique. | ||
/// | ||
/// This library also supports another variant of the deposit script | ||
/// allowing to embed 32-byte extra data. The extra data allows to attach | ||
/// additional context to the deposit. The script with 32-byte extra data | ||
/// looks like this: | ||
/// | ||
/// ``` | ||
/// <depositorAddress> DROP | ||
/// <extraData> DROP | ||
/// <blindingFactor> DROP | ||
/// DUP HASH160 <walletPubKeyHash> EQUAL | ||
/// IF | ||
/// CHECKSIG | ||
/// ELSE | ||
/// DUP HASH160 <refundPubkeyHash> EQUALVERIFY | ||
/// <refundLocktime> CHECKLOCKTIMEVERIFY DROP | ||
/// CHECKSIG | ||
/// ENDIF | ||
/// ``` | ||
library Deposit { | ||
using BTCUtils for bytes; | ||
using BytesLib for bytes; | ||
|
@@ -96,6 +115,8 @@ library Deposit { | |
// the time when the sweep proof was delivered to the Ethereum chain. | ||
// XXX: Unsigned 32-bit int unix seconds, will break February 7th 2106. | ||
uint32 sweptAt; | ||
// The 32-byte deposit extra data. Optional, can be bytes32(0). | ||
bytes32 extraData; | ||
// This struct doesn't contain `__gap` property as the structure is stored | ||
// in a mapping, mappings store values in different slots and they are | ||
// not contiguous with other values. | ||
|
@@ -152,6 +173,28 @@ library Deposit { | |
BitcoinTx.Info calldata fundingTx, | ||
DepositRevealInfo calldata reveal | ||
) external { | ||
_revealDeposit(self, fundingTx, reveal, bytes32(0)); | ||
} | ||
|
||
/// @notice Internal function encapsulating the core logic of the deposit | ||
/// reveal process. Handles both regular deposits without extra data | ||
/// as well as deposits with 32-byte extra data embedded in the | ||
/// deposit script. The behavior is controlled by the `extraData` | ||
/// parameter. If `extraData` is bytes32(0), the function triggers | ||
/// the flow for regular deposits. If `extraData` is not bytes32(0), | ||
/// the function triggers the flow for deposits with 32-byte | ||
/// extra data. | ||
/// @param fundingTx Bitcoin funding transaction data, see `BitcoinTx.Info`. | ||
/// @param reveal Deposit reveal data, see `RevealInfo struct. | ||
/// @param extraData 32-byte deposit extra data. Can be bytes32(0). | ||
/// @dev Requirements are described in the docstrings of `revealDeposit` and | ||
/// `revealDepositWithExtraData` external functions. | ||
function _revealDeposit( | ||
BridgeState.Storage storage self, | ||
BitcoinTx.Info calldata fundingTx, | ||
DepositRevealInfo calldata reveal, | ||
bytes32 extraData | ||
) internal { | ||
require( | ||
self.registeredWallets[reveal.walletPubKeyHash].state == | ||
Wallets.WalletState.Live, | ||
|
@@ -167,33 +210,70 @@ library Deposit { | |
validateDepositRefundLocktime(self, reveal.refundLocktime); | ||
} | ||
|
||
bytes memory expectedScript = abi.encodePacked( | ||
hex"14", // Byte length of depositor Ethereum address. | ||
msg.sender, | ||
hex"75", // OP_DROP | ||
hex"08", // Byte length of blinding factor value. | ||
reveal.blindingFactor, | ||
hex"75", // OP_DROP | ||
hex"76", // OP_DUP | ||
hex"a9", // OP_HASH160 | ||
hex"14", // Byte length of a compressed Bitcoin public key hash. | ||
reveal.walletPubKeyHash, | ||
hex"87", // OP_EQUAL | ||
hex"63", // OP_IF | ||
hex"ac", // OP_CHECKSIG | ||
hex"67", // OP_ELSE | ||
hex"76", // OP_DUP | ||
hex"a9", // OP_HASH160 | ||
hex"14", // Byte length of a compressed Bitcoin public key hash. | ||
reveal.refundPubKeyHash, | ||
hex"88", // OP_EQUALVERIFY | ||
hex"04", // Byte length of refund locktime value. | ||
reveal.refundLocktime, | ||
hex"b1", // OP_CHECKLOCKTIMEVERIFY | ||
hex"75", // OP_DROP | ||
hex"ac", // OP_CHECKSIG | ||
hex"68" // OP_ENDIF | ||
); | ||
bytes memory expectedScript; | ||
|
||
if (extraData == bytes32(0)) { | ||
// Regular deposit without 32-byte extra data. | ||
expectedScript = abi.encodePacked( | ||
hex"14", // Byte length of depositor Ethereum address. | ||
msg.sender, | ||
hex"75", // OP_DROP | ||
hex"08", // Byte length of blinding factor value. | ||
reveal.blindingFactor, | ||
hex"75", // OP_DROP | ||
hex"76", // OP_DUP | ||
hex"a9", // OP_HASH160 | ||
hex"14", // Byte length of a compressed Bitcoin public key hash. | ||
reveal.walletPubKeyHash, | ||
hex"87", // OP_EQUAL | ||
hex"63", // OP_IF | ||
hex"ac", // OP_CHECKSIG | ||
hex"67", // OP_ELSE | ||
hex"76", // OP_DUP | ||
hex"a9", // OP_HASH160 | ||
hex"14", // Byte length of a compressed Bitcoin public key hash. | ||
reveal.refundPubKeyHash, | ||
hex"88", // OP_EQUALVERIFY | ||
hex"04", // Byte length of refund locktime value. | ||
reveal.refundLocktime, | ||
hex"b1", // OP_CHECKLOCKTIMEVERIFY | ||
hex"75", // OP_DROP | ||
hex"ac", // OP_CHECKSIG | ||
hex"68" // OP_ENDIF | ||
); | ||
} else { | ||
// Deposit with 32-byte extra data. | ||
expectedScript = abi.encodePacked( | ||
hex"14", // Byte length of depositor Ethereum address. | ||
msg.sender, | ||
hex"75", // OP_DROP | ||
hex"20", // Byte length of extra data. | ||
extraData, | ||
hex"75", // OP_DROP | ||
hex"08", // Byte length of blinding factor value. | ||
reveal.blindingFactor, | ||
hex"75", // OP_DROP | ||
hex"76", // OP_DUP | ||
hex"a9", // OP_HASH160 | ||
hex"14", // Byte length of a compressed Bitcoin public key hash. | ||
reveal.walletPubKeyHash, | ||
hex"87", // OP_EQUAL | ||
hex"63", // OP_IF | ||
hex"ac", // OP_CHECKSIG | ||
hex"67", // OP_ELSE | ||
hex"76", // OP_DUP | ||
hex"a9", // OP_HASH160 | ||
hex"14", // Byte length of a compressed Bitcoin public key hash. | ||
reveal.refundPubKeyHash, | ||
hex"88", // OP_EQUALVERIFY | ||
hex"04", // Byte length of refund locktime value. | ||
reveal.refundLocktime, | ||
hex"b1", // OP_CHECKLOCKTIMEVERIFY | ||
hex"75", // OP_DROP | ||
hex"ac", // OP_CHECKSIG | ||
hex"68" // OP_ENDIF | ||
); | ||
} | ||
|
||
bytes memory fundingOutput = fundingTx | ||
.outputVector | ||
|
@@ -258,6 +338,21 @@ library Deposit { | |
deposit.treasuryFee = self.depositTreasuryFeeDivisor > 0 | ||
? fundingOutputAmount / self.depositTreasuryFeeDivisor | ||
: 0; | ||
deposit.extraData = extraData; | ||
|
||
_emitDepositRevealedEvent(fundingTxHash, fundingOutputAmount, reveal); | ||
} | ||
|
||
/// @notice Emits the `DepositRevealed` event. | ||
/// @param fundingTxHash The funding transaction hash. | ||
/// @param fundingOutputAmount The funding output amount in satoshi. | ||
/// @param reveal Deposit reveal data, see `RevealInfo struct. | ||
/// @dev This function is extracted to overcome the stack too deep error. | ||
function _emitDepositRevealedEvent( | ||
bytes32 fundingTxHash, | ||
uint64 fundingOutputAmount, | ||
DepositRevealInfo calldata reveal | ||
) internal { | ||
// slither-disable-next-line reentrancy-events | ||
emit DepositRevealed( | ||
fundingTxHash, | ||
|
@@ -272,6 +367,40 @@ library Deposit { | |
); | ||
} | ||
|
||
/// @notice Sibling of the `revealDeposit` function. This function allows | ||
/// to reveal a P2(W)SH Bitcoin deposit with 32-byte extra data | ||
/// embedded in the deposit script. The extra data allows to | ||
/// attach additional context to the deposit. For example, | ||
/// it allows a third-party smart contract to reveal the | ||
/// deposit on behalf of the original depositor and provide | ||
/// additional services once the deposit is handled. In this | ||
/// case, the address of the original depositor can be encoded | ||
/// as extra data. | ||
/// @param fundingTx Bitcoin funding transaction data, see `BitcoinTx.Info`. | ||
/// @param reveal Deposit reveal data, see `RevealInfo struct. | ||
/// @param extraData 32-byte deposit extra data. | ||
/// @dev Requirements: | ||
/// - All requirements from `revealDeposit` function must be met, | ||
/// - `extraData` must not be bytes32(0), | ||
/// - `extraData` must be the actual extra data used in the P2(W)SH | ||
/// BTC deposit transaction. | ||
/// | ||
/// If any of these requirements is not met, the wallet _must_ refuse | ||
/// to sweep the deposit and the depositor has to wait until the | ||
/// deposit script unlocks to receive their BTC back. | ||
function revealDepositWithExtraData( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am wondering if this is not a good opportunity to allow anyone to reveal the deposit. In We could also lift the requirements of not having extra data empty (see my previous comment) and potentially allow to use I think we had this discussion before but I do not remember why we did not decide on this option. Having a separate depositor contract may not be needed in all cases and I am not even sure if it is needed for Solana/Base, cases. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not only about the vault. Most importantly, the Let's use Base as an example: While making a deposit, we are setting the Last but not least, I'd say that not having the vault in the script is more flexible and safe. What if the vault is set as untrusted due to a critical vulnerability? If the vault is embedded in the script, the deposit can be revealed there at any time by anyone, far before the refund. The vault cannot be changed in that case. If we don't embed, the deposit can always be revealed with vault That said, I see more problems than advantages with letting anyone reveal the deposit. The |
||
BridgeState.Storage storage self, | ||
BitcoinTx.Info calldata fundingTx, | ||
DepositRevealInfo calldata reveal, | ||
bytes32 extraData | ||
) external { | ||
// Strong requirement in order to differentiate from the regular | ||
// reveal flow and reduce potential attack surface. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any particular attack surface? If the vault doesn't use extra data as in the example of tBTC, this is just about mapping the revealed deposit to the Bitcoin script to unlock UTXO. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please see: #760 (comment) |
||
require(extraData != bytes32(0), "Extra data must not be empty"); | ||
|
||
_revealDeposit(self, fundingTx, reveal, extraData); | ||
} | ||
|
||
/// @notice Validates the deposit refund locktime. The validation passes | ||
/// successfully only if the deposit reveal is done respectively | ||
/// earlier than the moment when the deposit refund locktime is | ||
|
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? My first thought was that it is to protect users but at this point, the Bitcoin transaction with 0x0 extra data is probably already in the Bitcoin mempool. I am not saying this condition is wrong, just curious about the reasons.
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.
My reasoning was: I don't see a reason to use
revealDepositWithExtraData
with empty extra data. Allowing to do so is a corner case. Such corner cases are often vectors of attack. This brought me to the security vs flexibility dilemma. In this case, I choose security. If there is a strong need to allow this case, we can always lift it in the future. Moreover, this requirement nicely separatesrevealDepositWithExtraData
fromrevealDeposit
.Also, if someone does such a Bitcoin transaction with 0x0 extra data on purpose, there is always the refund option.