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

Implementation of BitcoinRedeemer contract #309

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1680946
Implement Bitcoin Redeemer contract
nkuba Mar 10, 2024
4c3025e
Inherit ERC20Permit in stBTC contract
nkuba Mar 10, 2024
e028a3b
Add reference to BitcoinRedeemer in stBTC
nkuba Mar 10, 2024
ede91a7
Implement stBTC redemption to Bitcoin with signature
nkuba Mar 10, 2024
4b18df7
Add test implementation of TBTC token
nkuba Mar 10, 2024
79a5305
Add deployment script for BitcoinRedeemer contract
nkuba Mar 10, 2024
96c6c0f
Add dependency to @openzeppelin/contracts-upgradeable
nkuba Mar 10, 2024
aca3bd9
Disable slither's reentrancy-event for requestRedemption
nkuba Mar 10, 2024
d58c14a
Upgrade @openzeppelin/hardhat-upgrades dependency
nkuba Mar 10, 2024
29dd099
Add core/cache/ dir to CI upload artifacts
nkuba Mar 10, 2024
a53d360
Add unit tests for redeemToBitcoin function
nkuba Mar 11, 2024
d5090d8
Add unit tests for updateBitcoinRedeemer
nkuba Mar 11, 2024
5ed70fc
Use beforeAfterSnapshotWrapper in updateDispatcher test
nkuba Mar 11, 2024
18a4ac2
Modify TreasuryUpdated event to include old address
nkuba Mar 11, 2024
9123555
Merge remote-tracking branch 'origin/main' into bitcoin-redeemer
nkuba Mar 11, 2024
e75bd73
Rename requestRedemption to redeemSharesAndUnmint
nkuba Mar 13, 2024
0d03792
Add WithdrawToBitcoin function
nkuba Mar 13, 2024
f719271
Add tests for withdrawToBitcoin function
nkuba Mar 13, 2024
2a4dd70
Add ERC20PermitUpgradeable contract
nkuba Mar 13, 2024
e30fca2
Merge remote-tracking branch 'origin/main' into bitcoin-redeemer
nkuba Mar 13, 2024
1660f8d
Revert "Add WithdrawToBitcoin function"
nkuba Mar 13, 2024
cd5e6c3
Revert "Add tests for withdrawToBitcoin function"
nkuba Mar 13, 2024
e9b5092
Implement approveAndCall patter in the vault
nkuba Mar 15, 2024
8e779c2
Merge remote-tracking branch 'origin/main' into bitcoin-redeemer
nkuba Apr 3, 2024
3ea84f8
Add unit tests for redeemer contract
nkuba Apr 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions core/contracts/BitcoinRedeemer.sol
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;
Copy link
Member

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?


/// 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);
Copy link
Member

Choose a reason for hiding this comment

The 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 requestRedemption is called directly skipping redeemToBitcoinWithPermit because of the approval?

Copy link
Member Author

Choose a reason for hiding this comment

The 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();
}
}
}
123 changes: 123 additions & 0 deletions core/contracts/ERC20PermitUpgradable.sol
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();
}
}
25 changes: 25 additions & 0 deletions core/contracts/bridge/ITBTCToken.sol
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);
}
Loading
Loading