Skip to content

Commit

Permalink
[WIP] TBTC Depositor PoC
Browse files Browse the repository at this point in the history
  • Loading branch information
nkuba committed Dec 19, 2023
1 parent d44e32f commit 83f9757
Show file tree
Hide file tree
Showing 5 changed files with 426 additions and 3 deletions.
6 changes: 3 additions & 3 deletions core/contracts/Acre.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}

Expand Down
241 changes: 241 additions & 0 deletions core/contracts/tbtc/TbtcDepositor.sol
Original file line number Diff line number Diff line change
@@ -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))
);
}
}
93 changes: 93 additions & 0 deletions core/contracts/test/BridgeStub.sol
Original file line number Diff line number Diff line change
@@ -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
);
}
}
18 changes: 18 additions & 0 deletions core/contracts/test/TBTCVaultStub.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 83f9757

Please sign in to comment.