-
Notifications
You must be signed in to change notification settings - Fork 2
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
Implementation of BitcoinRedeemer contract #309
Changes from all commits
1680946
4c3025e
e028a3b
ede91a7
4b18df7
79a5305
96c6c0f
aca3bd9
d58c14a
29dd099
a53d360
d5090d8
5ed70fc
18a4ac2
9123555
e75bd73
0d03792
f719271
2a4dd70
e30fca2
1660f8d
cd5e6c3
e9b5092
8e779c2
3ea84f8
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 |
---|---|---|
@@ -0,0 +1,147 @@ | ||
// SPDX-License-Identifier: GPL-3.0-only | ||
pragma solidity ^0.8.21; | ||
|
||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
|
||
import "@thesis/solidity-contracts/contracts/token/IReceiveApproval.sol"; | ||
|
||
import "./stBTC.sol"; | ||
import "./bridge/ITBTCToken.sol"; | ||
|
||
/// @title tBTC Redemption Library | ||
/// @notice This library contains functions for handling tBTC redemption data. | ||
library TbtcRedemption { | ||
/// @notice Extracts the Bitcoin output script hash from the provided redemption | ||
/// data. | ||
/// @dev This function decodes redemption data and returns the keccak256 hash | ||
/// of the redeemer output script. | ||
/// @param redemptionData Redemption data. | ||
/// @return The keccak256 hash of the redeemer output script. | ||
function extractBitcoinOutputScriptHash( | ||
bytes calldata redemptionData | ||
) internal pure returns (bytes32) { | ||
(, , , , , bytes memory redeemerOutputScript) = abi.decode( | ||
redemptionData, | ||
(address, bytes20, bytes32, uint32, uint64, bytes) | ||
); | ||
|
||
return keccak256(redeemerOutputScript); | ||
} | ||
} | ||
|
||
/// @title Bitcoin Redeemer | ||
/// @notice This contract facilitates redemption of stBTC tokens to Bitcoin through | ||
/// tBTC redemption process. | ||
contract BitcoinRedeemer is Initializable, IReceiveApproval { | ||
/// Interface for tBTC token contract. | ||
ITBTCToken public tbtcToken; | ||
|
||
/// stBTC token contract. | ||
stBTC public stbtc; | ||
|
||
/// Emitted when redemption is requested. | ||
/// @param owner Owner of stBTC tokens. | ||
/// @param shares Number of stBTC tokens. | ||
/// @param tbtcAmount Number of tBTC tokens. | ||
event RedemptionRequested( | ||
address indexed owner, | ||
uint256 shares, | ||
uint256 tbtcAmount | ||
); | ||
|
||
/// Reverts if the tBTC Token address is zero. | ||
error TbtcTokenZeroAddress(); | ||
|
||
/// Reverts if the stBTC address is zero. | ||
error StbtcZeroAddress(); | ||
|
||
/// Attempted to call receiveApproval for not supported token. | ||
error UnsupportedToken(address token); | ||
|
||
/// Attempted to call receiveApproval by supported token. | ||
error CallerNotAllowed(address caller); | ||
|
||
/// Attempted to call receiveApproval with empty data. | ||
error EmptyExtraData(); | ||
|
||
/// Reverts when approveAndCall to tBTC contract fails. | ||
error ApproveAndCallFailed(); | ||
|
||
/// @custom:oz-upgrades-unsafe-allow constructor | ||
constructor() { | ||
_disableInitializers(); | ||
} | ||
|
||
/// @notice Initializes the contract with tBTC token and stBTC token addresses | ||
/// @param _tbtcToken The address of the tBTC token contract | ||
/// @param _stbtc The address of the stBTC token contract | ||
function initialize(address _tbtcToken, address _stbtc) public initializer { | ||
if (address(_tbtcToken) == address(0)) { | ||
revert TbtcTokenZeroAddress(); | ||
} | ||
if (address(_stbtc) == address(0)) { | ||
revert StbtcZeroAddress(); | ||
} | ||
|
||
tbtcToken = ITBTCToken(_tbtcToken); | ||
stbtc = stBTC(_stbtc); | ||
} | ||
|
||
/// @notice Redeems shares for tBTC and requests bridging to Bitcoin. | ||
/// @param from Shares token holder executing redemption. | ||
/// @param amount Amount of shares to redeem. | ||
/// @param token stBTC token address. | ||
/// @param extraData Redemption data in a format expected from | ||
/// `redemptionData` parameter of Bridge's `receiveBalanceApproval` | ||
/// function. | ||
function receiveApproval( | ||
address from, | ||
uint256 amount, | ||
address token, | ||
bytes calldata extraData | ||
) external { | ||
if (token != address(stbtc)) revert UnsupportedToken(token); | ||
if (msg.sender != token) revert CallerNotAllowed(msg.sender); | ||
if (extraData.length == 0) revert EmptyExtraData(); | ||
|
||
redeemSharesAndUnmint(from, amount, extraData); | ||
} | ||
|
||
/// @notice Initiates the redemption process by exchanging stBTC tokens for | ||
/// tBTC tokens and requesting bridging to Bitcoin. | ||
/// @dev Redeems stBTC shares to receive tBTC and requests redemption of tBTC | ||
/// to Bitcoin via tBTC Bridge. | ||
/// Redemption data in a format expected from `redemptionData` parameter | ||
/// of Bridge's `receiveBalanceApproval`. | ||
/// It uses tBTC token owner which is the TBTCVault contract as spender | ||
/// of tBTC requested for redemption. | ||
/// @dev tBTC Bridge redemption process has a path where request can timeout. | ||
/// It is a scenario that is unlikely to happen with the current Bridge | ||
/// setup. This contract remains upgradable to have flexibility to handle | ||
/// adjustments to tBTC Bridge changes. | ||
/// @param owner The owner of the stBTC tokens. | ||
/// @param shares The number of stBTC tokens to redeem. | ||
/// @param tbtcRedemptionData Additional data required for the tBTC redemption. | ||
/// See `redemptionData` parameter description of `Bridge.requestRedemption` | ||
/// function. | ||
function redeemSharesAndUnmint( | ||
address owner, | ||
uint256 shares, | ||
bytes calldata tbtcRedemptionData | ||
) internal { | ||
uint256 tbtcAmount = stbtc.redeem(shares, address(this), owner); | ||
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. Just to double check.. I take that this call would fail if the 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. Great question! I need to revisit the implementation, as I found out that there is a bug for the direct call path. |
||
|
||
// slither-disable-next-line reentrancy-events | ||
emit RedemptionRequested(owner, shares, tbtcAmount); | ||
|
||
if ( | ||
!tbtcToken.approveAndCall( | ||
tbtcToken.owner(), | ||
tbtcAmount, | ||
tbtcRedemptionData | ||
) | ||
) { | ||
revert ApproveAndCallFailed(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
// SPDX-License-Identifier: MIT | ||
// | ||
// This code is copied from OpenZeppelin Upgradable Contracts library: | ||
// https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/3cf491630086558f50504d88e76bb4e736c738ab/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol | ||
// With the following changes: | ||
// - replaced relative import paths with `@openzeppelin/contracts-upgradeable`, | ||
// | ||
// | ||
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC20Permit.sol) | ||
|
||
pragma solidity ^0.8.20; | ||
|
||
import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; | ||
import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; | ||
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | ||
import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; | ||
import {NoncesUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/NoncesUpgradeable.sol"; | ||
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
|
||
/** | ||
* @dev Implementation of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in | ||
* https://eips.ethereum.org/EIPS/eip-2612[ERC-2612]. | ||
* | ||
* Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by | ||
* presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't | ||
* need to send a transaction, and thus is not required to hold Ether at all. | ||
*/ | ||
abstract contract ERC20PermitUpgradeable is | ||
Initializable, | ||
ERC20Upgradeable, | ||
IERC20Permit, | ||
EIP712Upgradeable, | ||
NoncesUpgradeable | ||
{ | ||
bytes32 private constant PERMIT_TYPEHASH = | ||
keccak256( | ||
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" | ||
); | ||
|
||
/** | ||
* @dev Permit deadline has expired. | ||
*/ | ||
error ERC2612ExpiredSignature(uint256 deadline); | ||
|
||
/** | ||
* @dev Mismatched signature. | ||
*/ | ||
error ERC2612InvalidSigner(address signer, address owner); | ||
|
||
/** | ||
* @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. | ||
* | ||
* It's a good idea to use the same `name` that is defined as the ERC-20 token name. | ||
*/ | ||
function __ERC20Permit_init(string memory name) internal onlyInitializing { | ||
__EIP712_init_unchained(name, "1"); | ||
} | ||
|
||
function __ERC20Permit_init_unchained( | ||
string memory | ||
) internal onlyInitializing {} | ||
|
||
/** | ||
* @inheritdoc IERC20Permit | ||
*/ | ||
function permit( | ||
address owner, | ||
address spender, | ||
uint256 value, | ||
uint256 deadline, | ||
uint8 v, | ||
bytes32 r, | ||
bytes32 s | ||
) public virtual { | ||
/* solhint-disable-next-line not-rely-on-time */ | ||
if (block.timestamp > deadline) { | ||
revert ERC2612ExpiredSignature(deadline); | ||
} | ||
|
||
bytes32 structHash = keccak256( | ||
abi.encode( | ||
PERMIT_TYPEHASH, | ||
owner, | ||
spender, | ||
value, | ||
_useNonce(owner), | ||
deadline | ||
) | ||
); | ||
|
||
bytes32 hash = _hashTypedDataV4(structHash); | ||
|
||
address signer = ECDSA.recover(hash, v, r, s); | ||
if (signer != owner) { | ||
revert ERC2612InvalidSigner(signer, owner); | ||
} | ||
|
||
_approve(owner, spender, value); | ||
} | ||
|
||
/** | ||
* @inheritdoc IERC20Permit | ||
*/ | ||
function nonces( | ||
address owner | ||
) | ||
public | ||
view | ||
virtual | ||
override(IERC20Permit, NoncesUpgradeable) | ||
returns (uint256) | ||
{ | ||
return super.nonces(owner); | ||
} | ||
|
||
/** | ||
* @inheritdoc IERC20Permit | ||
*/ | ||
// solhint-disable-next-line func-name-mixedcase | ||
function DOMAIN_SEPARATOR() external view virtual returns (bytes32) { | ||
return _domainSeparatorV4(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// SPDX-License-Identifier: GPL-3.0-only | ||
pragma solidity ^0.8.21; | ||
|
||
/// @title Interface of TBTC token contract. | ||
/// @notice This interface defines functions of TBTC token contract used by Acre | ||
/// contracts. | ||
interface ITBTCToken { | ||
/// @notice Calls `receiveApproval` function on spender previously approving | ||
/// the spender to withdraw from the caller multiple times, up to | ||
/// the `amount` amount. If this function is called again, it | ||
/// overwrites the current allowance with `amount`. Reverts if the | ||
/// approval reverted or if `receiveApproval` call on the spender | ||
/// reverted. | ||
/// @return True if both approval and `receiveApproval` calls succeeded. | ||
/// @dev If the `amount` is set to `type(uint256).max` then | ||
/// `transferFrom` and `burnFrom` will not reduce an allowance. | ||
function approveAndCall( | ||
address spender, | ||
uint256 amount, | ||
bytes memory extraData | ||
) external returns (bool); | ||
|
||
/// @dev Returns the address of the contract owner. | ||
function owner() external view returns (address); | ||
} |
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.
Since we already import
"@keep-network/tbtc-v2"
should we maybe import TBTC type instead?