From 83f9757fc173b03eb4c9513ae2bd47a6db1b8a61 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 15 Dec 2023 16:18:39 +0100 Subject: [PATCH] [WIP] TBTC Depositor PoC --- core/contracts/Acre.sol | 6 +- core/contracts/tbtc/TbtcDepositor.sol | 241 ++++++++++++++++++++++ core/contracts/test/BridgeStub.sol | 93 +++++++++ core/contracts/test/TBTCVaultStub.sol | 18 ++ core/contracts/test/TbtcDepositorStub.sol | 71 +++++++ 5 files changed, 426 insertions(+), 3 deletions(-) create mode 100644 core/contracts/tbtc/TbtcDepositor.sol create mode 100644 core/contracts/test/BridgeStub.sol create mode 100644 core/contracts/test/TBTCVaultStub.sol create mode 100644 core/contracts/test/TbtcDepositorStub.sol diff --git a/core/contracts/Acre.sol b/core/contracts/Acre.sol index 0e1cf640f..967896c3e 100644 --- a/core/contracts/Acre.sol +++ b/core/contracts/Acre.sol @@ -15,7 +15,7 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; /// burning of shares (stBTC), which are represented as standard ERC20 /// tokens, providing a seamless exchange with tBTC tokens. contract Acre is ERC4626 { - event StakeReferral(bytes32 indexed referral, uint256 assets); + event StakeReferral(uint16 indexed referral, uint256 assets); constructor( IERC20 tbtc @@ -32,12 +32,12 @@ contract Acre is ERC4626 { function stake( uint256 assets, address receiver, - bytes32 referral + uint16 referral ) public returns (uint256) { // TODO: revisit the type of referral. uint256 shares = deposit(assets, receiver); - if (referral != bytes32(0)) { + if (referral > 0) { emit StakeReferral(referral, assets); } diff --git a/core/contracts/tbtc/TbtcDepositor.sol b/core/contracts/tbtc/TbtcDepositor.sol new file mode 100644 index 000000000..ef751cb4c --- /dev/null +++ b/core/contracts/tbtc/TbtcDepositor.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {Acre} from "../Acre.sol"; + +interface IBridge { + struct BitcoinTxInfo { + bytes4 version; + bytes inputVector; + bytes outputVector; + bytes4 locktime; + } + + struct DepositRevealInfo { + uint32 fundingOutputIndex; + bytes8 blindingFactor; + bytes20 walletPubKeyHash; + bytes20 refundPubKeyHash; + bytes4 refundLocktime; + address vault; + } + + struct DepositRequest { + address depositor; + uint64 amount; + uint32 revealedAt; + address vault; + uint64 treasuryFee; + uint32 sweptAt; + } + + function revealDepositWithExtraData( + BitcoinTxInfo calldata fundingTx, + DepositRevealInfo calldata reveal, + bytes32 extraData + ) external; + + function deposits( + uint256 depositKey + ) external view returns (DepositRequest memory); + + function depositParameters() + external + view + returns ( + uint64 depositDustThreshold, + uint64 depositTreasuryFeeDivisor, + uint64 depositTxMaxFee, + uint32 depositRevealAheadPeriod + ); +} + +interface ITBTCVault { + struct OptimisticMintingRequest { + // UNIX timestamp at which the optimistic minting was requested. + uint64 requestedAt; + // UNIX timestamp at which the optimistic minting was finalized. + // 0 if not yet finalized. + uint64 finalizedAt; + } + + function optimisticMintingRequests( + uint256 depositKey + ) external returns (OptimisticMintingRequest memory); + + function optimisticMintingFeeDivisor() external returns (uint32); +} + +contract TbtcDepositor { + using BTCUtils for bytes; + using SafeERC20 for IERC20; + + struct DepositRequest { + // Receiver of the stBTC token. + address receiver; + // Referral for the stake operation. + uint16 referral; + // UNIX timestamp at which the optimistic minting was requested. + uint64 requestedAt; + // UNIX timestamp at which the optimistic minting was finalized. + // 0 if not yet finalized. + uint64 finalizedAt; + // Maximum Deposit Transaction Fee snapshotted from the Bridge contract + // at the moment of deposit reveal. + uint256 depositTxMaxFee; + // Optimistic Minting Fee Divisor snapshotted from the TBTC Vault contract + // at the moment of deposit reveal. + uint32 optimisticMintingFeeDivisor; + } + + IBridge public bridge; + ITBTCVault public tbtcVault; + Acre public acre; + + mapping(uint256 => DepositRequest) public depositRequests; + + constructor(IBridge _bridge, ITBTCVault _tbtcVault, Acre _acre) { + bridge = _bridge; + tbtcVault = _tbtcVault; + acre = _acre; + } + + // Extra Data 32 byte + // receiver - address - 20 byte + // referral - uint16 - 2 byte + function initializeDeposit( + IBridge.BitcoinTxInfo calldata fundingTx, + IBridge.DepositRevealInfo calldata reveal, + bytes32 extraData + ) external { + bytes32 fundingTxHash = abi + .encodePacked( + fundingTx.version, + fundingTx.inputVector, + fundingTx.outputVector, + fundingTx.locktime + ) + .hash256View(); + + DepositRequest storage request = depositRequests[ + calculateDepositKey(fundingTxHash, reveal.fundingOutputIndex) + ]; + + // TODO: Replace with custom errors + require(request.requestedAt == 0, "deposit already initialized"); + + // solhint-disable-next-line not-rely-on-time + request.requestedAt = uint64(block.timestamp); + + // First 20 bytes of extra data is receiver address. + request.receiver = address(uint160(bytes20(extraData))); + // Next 2 bytes of extra data is referral info. + request.referral = uint16(bytes2(extraData << (8 * 20))); + + require(request.receiver != address(0), "receiver cannot be zero"); + + // Reveal the deposit to tBTC Bridge contract. + bridge.revealDepositWithExtraData(fundingTx, reveal, extraData); + + // Store Deposit Transaction Max Fee. + (, , uint64 depositTxMaxFee, ) = bridge.depositParameters(); + request.depositTxMaxFee = depositTxMaxFee; + + // Store Optimistic Minting Fee Divisor. + request.optimisticMintingFeeDivisor = tbtcVault + .optimisticMintingFeeDivisor(); + } + + function finalizeDeposit( + bytes32 fundingTxHash, + uint32 fundingOutputIndex + ) external { + uint256 depositKey = calculateDepositKey( + fundingTxHash, + fundingOutputIndex + ); + DepositRequest storage request = depositRequests[depositKey]; + + // TODO: Replace with custom errors + require(request.requestedAt > 0, "deposit not initialized"); + require(request.finalizedAt == 0, "deposit already finalized"); + + // Set finalization timestamp. + // solhint-disable-next-line not-rely-on-time + request.finalizedAt = uint64(block.timestamp); + + // Get deposit details from tBTC contracts. + IBridge.DepositRequest memory bridgeDepositRequest = bridge.deposits( + depositKey + ); + ITBTCVault.OptimisticMintingRequest + memory optimisticMintingRequest = tbtcVault + .optimisticMintingRequests(depositKey); + + // tBTC amount calculation. + // - for optimistically minted deposits: + // amount = depositAmount - depositTreasuryFee - depositTxMaxFee - optimisticMintingFee + // - for swept deposits. + // amount = depositAmount - depositTreasuryFee - depositTxMaxFee + // + // NOTE: These calculation are simplified and can leave some positive + // imbalance in this contract. + // - depositTxMaxFee - this is a maximum transaction fee that can be deducted + // on Bitcoin transaction sweeping, + // - optimisticMintingFee - this is a optimistic minting fee snapshotted + // at the moment of the reveal, minting finalization can be completed; + // the final value deducted on optimistic minting finalization can change + // in the meantime. + // The imbalance should be donated to the Acre staking contract. + + // Extract initial value sent by the user. + uint256 fundingTxAmount = bridgeDepositRequest.amount; + + uint256 amount = fundingTxAmount - + bridgeDepositRequest.treasuryFee - + request.depositTxMaxFee; + + // TODO: Revisit logic to find edge cases of mixed minting paths. + // Check if deposit was optimistically minted. + if (optimisticMintingRequest.finalizedAt > 0) { + // TODO: Consider checking optimisticMintingFee once again and take + // the max(optimisticMintingFeeOnReval, optimisticMintingFeeOnFinalize) + // Subtract optimistic minting fee. + uint256 optimisticMintingFeeDivisor = request + .optimisticMintingFeeDivisor; + + uint256 optimisticMintingFee = optimisticMintingFeeDivisor > 0 + ? (fundingTxAmount / optimisticMintingFeeDivisor) + : 0; + + amount -= optimisticMintingFee; + // If not optimistically minted check if deposit was swept. + } else { + require( + bridgeDepositRequest.sweptAt > 0, + "tbtc bridge deposit not swept" + ); + } + + // Stake tBTC in Acre. + IERC20(acre.asset()).safeIncreaseAllowance(address(acre), amount); + acre.stake(amount, request.receiver, request.referral); + } + + /// @notice Calculates deposit key the same way as the Bridge contract. + /// The deposit key is computed as + /// `keccak256(fundingTxHash | fundingOutputIndex)`. + function calculateDepositKey( + bytes32 fundingTxHash, + uint32 fundingOutputIndex + ) public pure returns (uint256) { + return + uint256( + keccak256(abi.encodePacked(fundingTxHash, fundingOutputIndex)) + ); + } +} diff --git a/core/contracts/test/BridgeStub.sol b/core/contracts/test/BridgeStub.sol new file mode 100644 index 000000000..0aa6c77d1 --- /dev/null +++ b/core/contracts/test/BridgeStub.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol"; +import {IBridge} from "../tbtc/TbtcDepositor.sol"; + +contract BridgeStub is IBridge { + using BTCUtils for bytes; + + mapping(uint256 => DepositRequest) dep; + + uint64 depositTreasuryFeeDivisor = 2000; // 0.05% + + function revealDepositWithExtraData( + BitcoinTxInfo calldata fundingTx, + DepositRevealInfo calldata reveal, + bytes32 extraData + ) external { + bytes32 fundingTxHash = abi + .encodePacked( + fundingTx.version, + fundingTx.inputVector, + fundingTx.outputVector, + fundingTx.locktime + ) + .hash256View(); + + DepositRequest storage deposit = dep[ + calculateDepositKey(fundingTxHash, reveal.fundingOutputIndex) + ]; + + require(deposit.revealedAt == 0, "Deposit already revealed"); + + bytes memory fundingOutput = fundingTx + .outputVector + .extractOutputAtIndex(reveal.fundingOutputIndex); + + uint64 fundingOutputAmount = fundingOutput.extractValue(); + + deposit.amount = fundingOutputAmount; + deposit.depositor = msg.sender; + /* solhint-disable-next-line not-rely-on-time */ + deposit.revealedAt = uint32(block.timestamp); + deposit.vault = reveal.vault; + deposit.treasuryFee = depositTreasuryFeeDivisor > 0 + ? fundingOutputAmount / depositTreasuryFeeDivisor + : 0; + } + + function deposits( + uint256 depositKey + ) external view returns (DepositRequest memory) { + return dep[depositKey]; + } + + function sweep(bytes32 fundingTxHash, uint32 fundingOutputIndex) public { + DepositRequest storage deposit = dep[ + calculateDepositKey(fundingTxHash, fundingOutputIndex) + ]; + + deposit.sweptAt = uint32(block.timestamp); + + // TODO: Mint TBTC + } + + function calculateDepositKey( + bytes32 fundingTxHash, + uint32 fundingOutputIndex + ) public pure returns (uint256) { + return + uint256( + keccak256(abi.encodePacked(fundingTxHash, fundingOutputIndex)) + ); + } + + function depositParameters() + external + view + returns ( + uint64 depositDustThreshold, + uint64 depositTreasuryFeeDivisor, + uint64 depositTxMaxFee, + uint32 depositRevealAheadPeriod + ) + { + return ( + 1000000, // 1000000 satoshi = 0.01 BTC + 2000, // 1/2000 == 5bps == 0.05% == 0.0005 + 100000, // 100000 satoshi = 0.001 BTC + 15 days + ); + } +} diff --git a/core/contracts/test/TBTCVaultStub.sol b/core/contracts/test/TBTCVaultStub.sol new file mode 100644 index 000000000..ce9245f72 --- /dev/null +++ b/core/contracts/test/TBTCVaultStub.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; +import {ITBTCVault} from "../tbtc/TbtcDepositor.sol"; + +contract TBTCVaultStub is ITBTCVault { + uint32 public optimisticMintingFeeDivisor = 500; // 1/500 = 0.002 = 0.2% + + // request.optimisticMintFee = optimisticMintingFeeDivisor > 0 + // ? (amountToMint / optimisticMintingFeeDivisor) + // : 0; + + function optimisticMintingRequests( + uint256 depositKey + ) external returns (OptimisticMintingRequest memory) { + OptimisticMintingRequest memory result; + return result; + } +} diff --git a/core/contracts/test/TbtcDepositorStub.sol b/core/contracts/test/TbtcDepositorStub.sol new file mode 100644 index 000000000..00d07e893 --- /dev/null +++ b/core/contracts/test/TbtcDepositorStub.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "../Acre.sol"; +import "./TestERC20.sol"; + +// interface IBridge { +// struct BitcoinTxInfo { +// bytes4 version; +// bytes inputVector; +// bytes outputVector; +// bytes4 locktime; +// } + +// struct DepositRevealInfo { +// uint32 fundingOutputIndex; +// bytes8 blindingFactor; +// bytes20 walletPubKeyHash; +// bytes20 refundPubKeyHash; +// bytes4 refundLocktime; +// address vault; +// bytes32 depositorExtraData; +// } + +// function revealDepositWithExtraData( +// BitcoinTxInfo calldata fundingTx, +// DepositRevealInfo calldata reveal +// ) external; +// } + +contract AcreDepositor { + // IBridge bridge; + // Acre acre; + // TestERC20 tbtc; + // struct DepositRequest { + // // UNIX timestamp at which the optimistic minting was requested. + // uint64 requestedAt; + // // UNIX timestamp at which the optimistic minting was finalized. + // // 0 if not yet finalized. + // uint64 finalizedAt; + // } + // mapping(uint256 => DepositRequest) public depositRequests; + // constructor(IBridge _bridge, Acre _acre, TestERC20 _tbtc) { + // bridge = _bridge; + // acre = _acre; + // } + // function revealDeposit( + // IBridge.BitcoinTxInfo calldata fundingTx, + // IBridge.DepositRevealInfo calldata reveal + // ) external {} + // function stake( + // bytes32 fundingTxHash, + // uint32 fundingOutputIndex, + // address receiver, + // bytes32 referral + // ) { + // // acre.stake(assets, receiver, referral); + // } + // /// @notice Calculates deposit key the same way as the Bridge contract. + // /// The deposit key is computed as + // /// `keccak256(fundingTxHash | fundingOutputIndex)`. + // function calculateDepositKey( + // bytes32 fundingTxHash, + // uint32 fundingOutputIndex + // ) public pure returns (uint256) { + // return + // uint256( + // keccak256(abi.encodePacked(fundingTxHash, fundingOutputIndex)) + // ); + // } +}