diff --git a/.github/workflows/core.yaml b/.github/workflows/core.yaml index de42c2dc6..8dbe76ae6 100644 --- a/.github/workflows/core.yaml +++ b/.github/workflows/core.yaml @@ -7,6 +7,15 @@ on: paths: - "core/**" pull_request: + workflow_dispatch: + inputs: + environment: + description: "Testnet to deploy contracts to" + required: true + type: choice + options: + - "sepolia" + default: "sepolia" defaults: run: @@ -23,7 +32,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 - name: Set up Node uses: actions/setup-node@v4 @@ -35,7 +44,7 @@ jobs: run: pnpm install --prefer-offline --frozen-lockfile - name: Download Build Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: core-build path: core/ @@ -50,7 +59,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 - name: Set up Node uses: actions/setup-node@v4 @@ -71,7 +80,7 @@ jobs: run: pip3 install slither-analyzer==$SLITHER_VERSION - name: Download Build Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: core-build path: core/ @@ -86,7 +95,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 - name: Set up Node uses: actions/setup-node@v4 @@ -98,7 +107,7 @@ jobs: run: pnpm install --prefer-offline --frozen-lockfile - name: Download Build Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: core-build path: core/ @@ -113,7 +122,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 - name: Set up Node uses: actions/setup-node@v4 @@ -125,10 +134,57 @@ jobs: run: pnpm install --prefer-offline --frozen-lockfile - name: Download Build Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: core-build path: core/ - name: Deploy run: pnpm run deploy --no-compile + + core-deploy-testnet: + needs: [core-deploy-dry-run] + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PNPM + uses: pnpm/action-setup@v3 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: "core/.nvmrc" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --prefer-offline --frozen-lockfile + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: core-build + path: core/ + + - name: Remove existing deployment artifacts for the selected network + run: rm -rf deployments/${{ github.event.inputs.environment }} + + - name: Deploy contracts + env: + ACCOUNTS_PRIVATE_KEYS: ${{ secrets.TESTNET_ETH_CONTRACT_OWNER_PRIVATE_KEY }} + CHAIN_API_URL: ${{ secrets.SEPOLIA_CHAIN_API_URL }} + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} + run: | + pnpm run deploy --network ${{ github.event.inputs.environment }} + + - name: Upload deployed contracts as workflow artifact + # The step will be executed even if the previous step fails (can be + # useful for partially failed deployments). + if: always() + uses: actions/upload-artifact@v4 + with: + name: deployed-contracts-${{ github.event.inputs.environment }} + path: | + core/deployments/${{ github.event.inputs.environment }} + if-no-files-found: error diff --git a/.github/workflows/dapp.yaml b/.github/workflows/dapp.yaml index 4d845443c..c0afd2ac5 100644 --- a/.github/workflows/dapp.yaml +++ b/.github/workflows/dapp.yaml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 - name: Set up Node uses: actions/setup-node@v4 @@ -32,7 +32,7 @@ jobs: cache: "pnpm" - name: Download SDK Build Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: sdk-build path: sdk/dist @@ -50,7 +50,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 - name: Set up Node uses: actions/setup-node@v4 @@ -59,7 +59,7 @@ jobs: cache: "pnpm" - name: Download SDK Build Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: sdk-build path: sdk/dist diff --git a/.github/workflows/reusable-core-build.yaml b/.github/workflows/reusable-core-build.yaml index cb77ec17c..68269918b 100644 --- a/.github/workflows/reusable-core-build.yaml +++ b/.github/workflows/reusable-core-build.yaml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 - name: Set up Node uses: actions/setup-node@v4 @@ -27,7 +27,7 @@ jobs: run: pnpm run build - name: Upload Build Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: core-build path: | diff --git a/.github/workflows/reusable-sdk-build.yaml b/.github/workflows/reusable-sdk-build.yaml index 88f7bf702..fe0a3d5ab 100644 --- a/.github/workflows/reusable-sdk-build.yaml +++ b/.github/workflows/reusable-sdk-build.yaml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 - name: Set up Node uses: actions/setup-node@v4 @@ -25,7 +25,7 @@ jobs: cache: "pnpm" - name: Download Core Build Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: core-build path: core/ @@ -37,7 +37,7 @@ jobs: run: pnpm run build - name: Upload Build Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: sdk-build path: sdk/dist/ diff --git a/.github/workflows/sdk.yaml b/.github/workflows/sdk.yaml index fbfd8c817..5898da5ee 100644 --- a/.github/workflows/sdk.yaml +++ b/.github/workflows/sdk.yaml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 - name: Set up Node uses: actions/setup-node@v4 @@ -32,7 +32,7 @@ jobs: cache: "pnpm" - name: Download Core Build Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: core-build path: core/ @@ -50,7 +50,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 - name: Set up Node uses: actions/setup-node@v4 @@ -59,7 +59,7 @@ jobs: cache: "pnpm" - name: Download Core Build Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: core-build path: core/ diff --git a/.github/workflows/website.yaml b/.github/workflows/website.yaml index c9064b398..317e6bd41 100644 --- a/.github/workflows/website.yaml +++ b/.github/workflows/website.yaml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 - name: Set up Node uses: actions/setup-node@v4 @@ -36,7 +36,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 - name: Set up Node uses: actions/setup-node@v4 diff --git a/core/.eslintrc b/core/.eslintrc index 781127ef7..00fc354f3 100644 --- a/core/.eslintrc +++ b/core/.eslintrc @@ -12,11 +12,12 @@ "test/**" ] } - ] + ], + "@typescript-eslint/no-use-before-define": "off" }, "overrides": [ { - "files": ["deploy/*.ts"], + "files": ["deploy/**/*.ts", "test/**/*.ts"], "rules": { "@typescript-eslint/unbound-method": "off" } diff --git a/core/contracts/AcreBitcoinDepositor.sol b/core/contracts/AcreBitcoinDepositor.sol new file mode 100644 index 000000000..9366a5472 --- /dev/null +++ b/core/contracts/AcreBitcoinDepositor.sol @@ -0,0 +1,679 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol"; + +import {stBTC} from "./stBTC.sol"; + +// TODO: Make Upgradable +// TODO: Make Pausable + +/// @title Acre Bitcoin Depositor contract. +/// @notice The contract integrates Acre staking with tBTC minting. +/// User who wants to stake BTC in Acre should submit a Bitcoin transaction +/// to the most recently created off-chain ECDSA wallets of the tBTC Bridge +/// using pay-to-script-hash (P2SH) or pay-to-witness-script-hash (P2WSH) +/// containing hashed information about this Depositor contract address, +/// and staker's Ethereum address. +/// Then, the staker initiates tBTC minting by revealing their Ethereum +/// address along with their deposit blinding factor, refund public key +/// hash and refund locktime on the tBTC Bridge through this Depositor +/// contract. +/// The off-chain ECDSA wallet and Optimistic Minting bots listen for these +/// sorts of messages and when they get one, they check the Bitcoin network +/// to make sure the deposit lines up. Majority of tBTC minting is finalized +/// by the Optimistic Minting process, where Minter bot initializes +/// minting process and if there is no veto from the Guardians, the +/// process is finalized and tBTC minted to the Depositor address. If +/// the revealed deposit is not handled by the Optimistic Minting process +/// the off-chain ECDSA wallet may decide to pick the deposit transaction +/// for sweeping, and when the sweep operation is confirmed on the Bitcoin +/// network, the tBTC Bridge and tBTC vault mint the tBTC token to the +/// Depositor address. After tBTC is minted to the Depositor, on the stake +/// finalization tBTC is staked in Acre and stBTC shares are emitted +/// to the staker. +contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step { + using SafeERC20 for IERC20; + + /// @notice State of the stake request. + enum StakeRequestState { + Unknown, + Initialized, + Finalized, + Queued, + FinalizedFromQueue, + CancelledFromQueue + } + + struct StakeRequest { + // State of the stake request. + StakeRequestState state; + // The address to which the stBTC shares will be minted. Stored only when + // request is queued. + address staker; + // tBTC token amount to stake after deducting tBTC minting fees and the + // Depositor fee. Stored only when request is queued. + uint88 queuedAmount; + } + + /// @notice Mapping of stake requests. + /// @dev The key is a deposit key identifying the deposit. + mapping(uint256 => StakeRequest) public stakeRequests; + + /// @notice tBTC Token contract. + // TODO: Remove slither disable when introducing upgradeability. + // slither-disable-next-line immutable-states + IERC20 public tbtcToken; + + /// @notice stBTC contract. + // TODO: Remove slither disable when introducing upgradeability. + // slither-disable-next-line immutable-states + stBTC public stbtc; + + /// @notice Minimum amount of a single stake request (in tBTC token precision). + /// @dev This parameter should be set to a value exceeding the minimum deposit + /// amount supported by tBTC Bridge. + uint256 public minStakeAmount; + + /// @notice Maximum amount of a single stake request (in tBTC token precision). + /// @dev The staking flow in the dApp is asynchronous and there is a short period + /// of time between a deposit funding transaction is made on Bitcoin chain + /// and revealed to this contract. This limit is used to gain better control + /// on the stakes queue, and reduce a risk of concurrent stake requests + /// made in the dApp being blocked by another big deposit. + uint256 public maxSingleStakeAmount; + + /// @notice Maximum total assets soft limit (in tBTC token precision). + /// @dev stBTC contract defines a maximum total assets limit held by the protocol + /// that new deposits cannot exceed (hard cap). Due to the asynchronous + /// manner of Bitcoin deposits process we introduce a soft limit (soft cap) + /// set to a value lower than the hard cap to let the dApp initialize + /// Bitcoin deposits only up to the soft cap limit. + uint256 public maxTotalAssetsSoftLimit; + + /// @notice Total balance of pending stake requests (in tBTC token precision). + /// @dev stBTC contract introduces limits for total deposits amount. Due to + /// asynchronous manner of the staking flow, this contract needs to track + /// balance of pending stake requests to ensure new stake request are + /// not initialized if they won't be able to finalize. + uint256 public queuedStakesBalance; + + /// @notice Divisor used to compute the depositor fee taken from each deposit + /// and transferred to the treasury upon stake request finalization. + /// @dev That fee is computed as follows: + /// `depositorFee = depositedAmount / depositorFeeDivisor` + /// for example, if the depositor fee needs to be 2% of each deposit, + /// the `depositorFeeDivisor` should be set to `50` because + /// `1/50 = 0.02 = 2%`. + uint64 public depositorFeeDivisor; + + /// @notice Emitted when a stake request is initialized. + /// @dev Deposit details can be fetched from {{ Bridge.DepositRevealed }} + /// event emitted in the same transaction. + /// @param depositKey Deposit key identifying the deposit. + /// @param caller Address that initialized the stake request. + /// @param staker The address to which the stBTC shares will be minted. + event StakeRequestInitialized( + uint256 indexed depositKey, + address indexed caller, + address indexed staker + ); + + /// @notice Emitted when bridging completion has been notified. + /// @param depositKey Deposit key identifying the deposit. + /// @param caller Address that notified about bridging completion. + /// @param referral Identifier of a partner in the referral program. + /// @param bridgedAmount Amount of tBTC tokens that was bridged by the tBTC bridge. + /// @param depositorFee Depositor fee amount. + event BridgingCompleted( + uint256 indexed depositKey, + address indexed caller, + uint16 indexed referral, + uint256 bridgedAmount, + uint256 depositorFee + ); + + /// @notice Emitted when a stake request is finalized. + /// @dev Deposit details can be fetched from {{ ERC4626.Deposit }} + /// event emitted in the same transaction. + /// @param depositKey Deposit key identifying the deposit. + /// @param caller Address that finalized the stake request. + /// @param stakedAmount Amount of staked tBTC tokens. + event StakeRequestFinalized( + uint256 indexed depositKey, + address indexed caller, + uint256 stakedAmount + ); + + /// @notice Emitted when a stake request is queued. + /// @param depositKey Deposit key identifying the deposit. + /// @param caller Address that finalized the stake request. + /// @param queuedAmount Amount of queued tBTC tokens. + event StakeRequestQueued( + uint256 indexed depositKey, + address indexed caller, + uint256 queuedAmount + ); + + /// @notice Emitted when a stake request is finalized from the queue. + /// @dev Deposit details can be fetched from {{ ERC4626.Deposit }} + /// event emitted in the same transaction. + /// @param depositKey Deposit key identifying the deposit. + /// @param caller Address that finalized the stake request. + /// @param stakedAmount Amount of staked tBTC tokens. + event StakeRequestFinalizedFromQueue( + uint256 indexed depositKey, + address indexed caller, + uint256 stakedAmount + ); + + /// @notice Emitted when a queued stake request is cancelled. + /// @param depositKey Deposit key identifying the deposit. + /// @param staker Address of the staker. + /// @param amountCancelled Amount of queued tBTC tokens that got cancelled. + event StakeRequestCancelledFromQueue( + uint256 indexed depositKey, + address indexed staker, + uint256 amountCancelled + ); + + /// @notice Emitted when a minimum single stake amount is updated. + /// @param minStakeAmount New value of the minimum single stake + /// amount (in tBTC token precision). + event MinStakeAmountUpdated(uint256 minStakeAmount); + + /// @notice Emitted when a maximum single stake amount is updated. + /// @param maxSingleStakeAmount New value of the maximum single stake + /// amount (in tBTC token precision). + event MaxSingleStakeAmountUpdated(uint256 maxSingleStakeAmount); + + /// @notice Emitted when a maximum total assets soft limit is updated. + /// @param maxTotalAssetsSoftLimit New value of the maximum total assets + /// soft limit (in tBTC token precision). + event MaxTotalAssetsSoftLimitUpdated(uint256 maxTotalAssetsSoftLimit); + + /// @notice Emitted when a depositor fee divisor is updated. + /// @param depositorFeeDivisor New value of the depositor fee divisor. + event DepositorFeeDivisorUpdated(uint64 depositorFeeDivisor); + + /// Reverts if the tBTC Token address is zero. + error TbtcTokenZeroAddress(); + + /// Reverts if the stBTC address is zero. + error StbtcZeroAddress(); + + /// @dev Staker address is zero. + error StakerIsZeroAddress(); + + /// @dev Attempted to execute function for stake request in unexpected current + /// state. + error UnexpectedStakeRequestState( + StakeRequestState currentState, + StakeRequestState expectedState + ); + + /// @dev Attempted to initialize a stake request with a deposit amount + /// exceeding the maximum limit for a single stake amount. + error ExceededMaxSingleStake(uint256 amount, uint256 max); + + /// @dev Attempted to finalize bridging with depositor's contract tBTC balance + /// lower than the calculated bridged tBTC amount. This error means + /// that Governance should top-up the tBTC reserve for bridging fees + /// approximation. + error InsufficientTbtcBalance( + uint256 amountToStake, + uint256 currentBalance + ); + + /// @dev Attempted to notify a bridging completion, while it was already + /// notified. + error BridgingCompletionAlreadyNotified(); + + /// @dev Attempted to finalize a stake request, while bridging completion has + /// not been notified yet. + error BridgingNotCompleted(); + + /// @dev Calculated depositor fee exceeds the amount of minted tBTC tokens. + error DepositorFeeExceedsBridgedAmount( + uint256 depositorFee, + uint256 bridgedAmount + ); + + /// @dev Attempted to call bridging finalization for a stake request for + /// which the function was already called. + error BridgingFinalizationAlreadyCalled(); + + /// @dev Attempted to finalize or cancel a stake request that was not added + /// to the queue, or was already finalized or cancelled. + error StakeRequestNotQueued(); + + /// @dev Attempted to call function by an account that is not the staker. + error CallerNotStaker(); + + /// @dev Attempted to set minimum stake amount to a value lower than the + /// tBTC Bridge deposit dust threshold. + error MinStakeAmountLowerThanBridgeMinDeposit( + uint256 minStakeAmount, + uint256 bridgeMinDepositAmount + ); + + /// @notice Acre Bitcoin Depositor contract constructor. + /// @param bridge tBTC Bridge contract instance. + /// @param tbtcVault tBTC Vault contract instance. + /// @param _tbtcToken tBTC token contract instance. + /// @param _stbtc stBTC contract instance. + // TODO: Move to initializer when making the contract upgradeable. + constructor( + address bridge, + address tbtcVault, + address _tbtcToken, + address _stbtc + ) Ownable(msg.sender) { + __AbstractTBTCDepositor_initialize(bridge, tbtcVault); + + if (address(_tbtcToken) == address(0)) { + revert TbtcTokenZeroAddress(); + } + if (address(_stbtc) == address(0)) { + revert StbtcZeroAddress(); + } + + tbtcToken = IERC20(_tbtcToken); + stbtc = stBTC(_stbtc); + + // TODO: Revisit initial values before mainnet deployment. + minStakeAmount = 0.015 * 1e18; // 0.015 BTC + maxSingleStakeAmount = 0.5 * 1e18; // 0.5 BTC + maxTotalAssetsSoftLimit = 7 * 1e18; // 7 BTC + depositorFeeDivisor = 1000; // 1/1000 == 10bps == 0.1% == 0.001 + } + + /// @notice This function allows staking process initialization for a Bitcoin + /// deposit made by an user with a P2(W)SH transaction. It uses the + /// supplied information to reveal a deposit to the tBTC Bridge contract. + /// @dev Requirements: + /// - The revealed vault address must match the TBTCVault address, + /// - All requirements from {Bridge#revealDepositWithExtraData} + /// function must be met. + /// - `staker` must be the staker address used in the P2(W)SH BTC + /// deposit transaction as part of the extra data. + /// - `referral` must be the referral info used in the P2(W)SH BTC + /// deposit transaction as part of the extra data. + /// - BTC deposit for the given `fundingTxHash`, `fundingOutputIndex` + /// can be revealed only one time. + /// @param fundingTx Bitcoin funding transaction data, see `IBridgeTypes.BitcoinTxInfo`. + /// @param reveal Deposit reveal data, see `IBridgeTypes.DepositRevealInfo`. + /// @param staker The address to which the stBTC shares will be minted. + /// @param referral Data used for referral program. + function initializeStake( + IBridgeTypes.BitcoinTxInfo calldata fundingTx, + IBridgeTypes.DepositRevealInfo calldata reveal, + address staker, + uint16 referral + ) external { + if (staker == address(0)) revert StakerIsZeroAddress(); + + // We don't check if the request was already initialized, as this check + // is enforced in `_initializeDeposit` when calling the + // `Bridge.revealDepositWithExtraData` function. + (uint256 depositKey, uint256 initialDepositAmount) = _initializeDeposit( + fundingTx, + reveal, + encodeExtraData(staker, referral) + ); + + transitionStakeRequestState( + depositKey, + StakeRequestState.Unknown, + StakeRequestState.Initialized + ); + + if (initialDepositAmount > maxSingleStakeAmount) + revert ExceededMaxSingleStake( + initialDepositAmount, + maxSingleStakeAmount + ); + + emit StakeRequestInitialized(depositKey, msg.sender, staker); + } + + /// @notice This function should be called for previously initialized stake + /// request, after tBTC bridging process was finalized. + /// It stakes the tBTC from the given deposit into stBTC, emitting the + /// stBTC shares to the staker specified in the deposit extra data + /// and using the referral provided in the extra data. + /// @dev In case depositing in stBTC vault fails (e.g. because of the + /// maximum deposit limit being reached), the `queueStake` function + /// should be called to add the stake request to the staking queue. + /// @param depositKey Deposit key identifying the deposit. + function finalizeStake(uint256 depositKey) external { + transitionStakeRequestState( + depositKey, + StakeRequestState.Initialized, + StakeRequestState.Finalized + ); + + (uint256 amountToStake, address staker) = finalizeBridging(depositKey); + + emit StakeRequestFinalized(depositKey, msg.sender, amountToStake); + + // Deposit tBTC in stBTC. + tbtcToken.safeIncreaseAllowance(address(stbtc), amountToStake); + // slither-disable-next-line unused-return + stbtc.deposit(amountToStake, staker); + } + + /// @notice This function should be called for previously initialized stake + /// request, after tBTC bridging process was finalized, in case the + /// `finalizeStake` failed due to stBTC vault deposit limit + /// being reached. + /// @dev It queues the stake request, until the stBTC vault is ready to + /// accept the deposit. The request must be finalized with `finalizeQueuedStake` + /// after the limit is increased or other user withdraws their funds + /// from the stBTC contract to make place for another deposit. + /// The staker has a possibility to submit `cancelQueuedStake` that + /// will withdraw the minted tBTC token and abort staking process. + /// @param depositKey Deposit key identifying the deposit. + function queueStake(uint256 depositKey) external { + transitionStakeRequestState( + depositKey, + StakeRequestState.Initialized, + StakeRequestState.Queued + ); + + StakeRequest storage request = stakeRequests[depositKey]; + + uint256 amountToQueue; + (amountToQueue, request.staker) = finalizeBridging(depositKey); + + request.queuedAmount = SafeCast.toUint88(amountToQueue); + + // Increase pending stakes balance. + queuedStakesBalance += amountToQueue; + + emit StakeRequestQueued(depositKey, msg.sender, amountToQueue); + } + + /// @notice This function should be called for previously queued stake + /// request, when stBTC vault is able to accept a deposit. + /// @param depositKey Deposit key identifying the deposit. + function finalizeQueuedStake(uint256 depositKey) external { + transitionStakeRequestState( + depositKey, + StakeRequestState.Queued, + StakeRequestState.FinalizedFromQueue + ); + + StakeRequest storage request = stakeRequests[depositKey]; + + if (request.queuedAmount == 0) revert StakeRequestNotQueued(); + + uint256 amountToStake = request.queuedAmount; + delete (request.queuedAmount); + + // Decrease pending stakes balance. + queuedStakesBalance -= amountToStake; + + emit StakeRequestFinalizedFromQueue( + depositKey, + msg.sender, + amountToStake + ); + + // Deposit tBTC in stBTC. + tbtcToken.safeIncreaseAllowance(address(stbtc), amountToStake); + // slither-disable-next-line unused-return + stbtc.deposit(amountToStake, request.staker); + } + + /// @notice Cancel queued stake. + /// The function can be called by the staker to recover tBTC that cannot + /// be finalized to stake in stBTC contract due to a deposit limit being + /// reached. + /// @dev This function can be called only after the stake request was added + /// to queue. + /// @dev Only staker provided in the extra data of the stake request can + /// call this function. + /// @param depositKey Deposit key identifying the deposit. + function cancelQueuedStake(uint256 depositKey) external { + transitionStakeRequestState( + depositKey, + StakeRequestState.Queued, + StakeRequestState.CancelledFromQueue + ); + + StakeRequest storage request = stakeRequests[depositKey]; + + uint256 amount = request.queuedAmount; + if (amount == 0) revert StakeRequestNotQueued(); + + address staker = request.staker; + // Check if caller is the staker. + if (msg.sender != staker) revert CallerNotStaker(); + + delete (request.queuedAmount); + + emit StakeRequestCancelledFromQueue(depositKey, staker, amount); + + // Decrease pending stakes balance. + queuedStakesBalance -= amount; + + tbtcToken.safeTransfer(staker, amount); + } + + /// @notice Updates the minimum stake amount. + /// @dev It requires that the new value is greater or equal to the tBTC Bridge + /// deposit dust threshold, to ensure deposit will be able to be bridged. + /// @param newMinStakeAmount New minimum stake amount (in tBTC precision). + function updateMinStakeAmount( + uint256 newMinStakeAmount + ) external onlyOwner { + uint256 minBridgeDepositAmount = _minDepositAmount(); + + // Check if new value is at least equal the tBTC Bridge Deposit Dust Threshold. + if (newMinStakeAmount < minBridgeDepositAmount) + revert MinStakeAmountLowerThanBridgeMinDeposit( + newMinStakeAmount, + minBridgeDepositAmount + ); + + minStakeAmount = newMinStakeAmount; + + emit MinStakeAmountUpdated(newMinStakeAmount); + } + + /// @notice Updates the maximum single stake amount. + /// @param newMaxSingleStakeAmount New maximum single stake amount (in tBTC + /// precision). + function updateMaxSingleStakeAmount( + uint256 newMaxSingleStakeAmount + ) external onlyOwner { + maxSingleStakeAmount = newMaxSingleStakeAmount; + + emit MaxSingleStakeAmountUpdated(newMaxSingleStakeAmount); + } + + /// @notice Updates the maximum total assets soft limit. + /// @param newMaxTotalAssetsSoftLimit New maximum total assets soft limit + /// (in tBTC precision). + function updateMaxTotalAssetsSoftLimit( + uint256 newMaxTotalAssetsSoftLimit + ) external onlyOwner { + maxTotalAssetsSoftLimit = newMaxTotalAssetsSoftLimit; + + emit MaxTotalAssetsSoftLimitUpdated(newMaxTotalAssetsSoftLimit); + } + + /// @notice Updates the depositor fee divisor. + /// @param newDepositorFeeDivisor New depositor fee divisor value. + function updateDepositorFeeDivisor( + uint64 newDepositorFeeDivisor + ) external onlyOwner { + // TODO: Introduce a parameters update process. + depositorFeeDivisor = newDepositorFeeDivisor; + + emit DepositorFeeDivisorUpdated(newDepositorFeeDivisor); + } + + /// @notice Minimum stake amount (in tBTC token precision). + /// @dev This function should be used by dApp to check the minimum amount + /// for the stake request. + /// @dev It is not enforced in the `initializeStakeRequest` function, as + /// it is intended to be used in the dApp staking form. + function minStake() external view returns (uint256) { + return minStakeAmount; + } + + /// @notice Maximum stake amount (in tBTC token precision). + /// @dev It takes into consideration the maximum total assets soft limit (soft + /// cap), that is expected to be set below the stBTC maximum total assets + /// limit (hard cap). + /// @dev This function should be called before Bitcoin transaction funding + /// is made. The `initializeStakeRequest` function is not enforcing this + /// limit, not to block the reveal deposit operation of the concurrent + /// deposits made in the dApp in the short window between limit check, + /// submission of Bitcoin funding transaction and stake request + /// initialization. + /// @return Maximum allowed stake amount. + function maxStake() external view returns (uint256) { + uint256 currentTotalAssets = stbtc.totalAssets(); + + if (currentTotalAssets >= maxTotalAssetsSoftLimit) { + return 0; + } + + uint256 availableLimit = maxTotalAssetsSoftLimit - currentTotalAssets; + + if (queuedStakesBalance >= availableLimit) { + return 0; + } + availableLimit -= queuedStakesBalance; + + return Math.min(availableLimit, maxSingleStakeAmount); + } + + // TODO: Handle minimum deposit amount in tBTC Bridge vs stBTC. + + /// @notice Encodes staker address and referral as extra data. + /// @dev Packs the data to bytes32: 20 bytes of staker address and + /// 2 bytes of referral, 10 bytes of trailing zeros. + /// @param staker The address to which the stBTC shares will be minted. + /// @param referral Data used for referral program. + /// @return Encoded extra data. + function encodeExtraData( + address staker, + uint16 referral + ) public pure returns (bytes32) { + return bytes32(abi.encodePacked(staker, referral)); + } + + /// @notice Decodes staker address and referral from extra data. + /// @dev Unpacks the data from bytes32: 20 bytes of staker address and + /// 2 bytes of referral, 10 bytes of trailing zeros. + /// @param extraData Encoded extra data. + /// @return staker The address to which the stBTC shares will be minted. + /// @return referral Data used for referral program. + function decodeExtraData( + bytes32 extraData + ) public pure returns (address staker, uint16 referral) { + // First 20 bytes of extra data is staker address. + staker = address(uint160(bytes20(extraData))); + // Next 2 bytes of extra data is referral info. + referral = uint16(bytes2(extraData << (8 * 20))); + } + + /// @notice This function is used for state transitions. It ensures the current + /// state matches expected, and updates the stake request to a new + /// state. + /// @param depositKey Deposit key identifying the deposit. + /// @param expectedState Expected current stake request state. + /// @param newState New stake request state. + function transitionStakeRequestState( + uint256 depositKey, + StakeRequestState expectedState, + StakeRequestState newState + ) internal { + // Validate current stake request state. + if (stakeRequests[depositKey].state != expectedState) + revert UnexpectedStakeRequestState( + stakeRequests[depositKey].state, + expectedState + ); + + // Transition to a new state. + stakeRequests[depositKey].state = newState; + } + + /// @notice This function should be called for previously initialized stake + /// request, after tBTC minting process completed, meaning tBTC was + /// minted to this contract. + /// @dev It calculates the amount to stake based on the approximate minted + /// tBTC amount reduced by the depositor fee. + /// @dev IMPORTANT NOTE: The minted tBTC amount used by this function is an + /// approximation. See documentation of the + /// {{AbstractTBTCDepositor#_calculateTbtcAmount}} responsible for calculating + /// this value for more details. + /// @dev In case balance of tBTC tokens in this contract doesn't meet the + /// calculated tBTC amount, the function reverts with `InsufficientTbtcBalance` + /// error. This case requires Governance's validation, as tBTC Bridge minting + /// fees might changed in the way that reserve mentioned in + /// {{AbstractTBTCDepositor#_calculateTbtcAmount}} needs a top-up. + /// @param depositKey Deposit key identifying the deposit. + /// @return amountToStake tBTC token amount to stake after deducting tBTC bridging + /// fees and the depositor fee. + /// @return staker The address to which the stBTC shares will be minted. + function finalizeBridging( + uint256 depositKey + ) internal returns (uint256, address) { + ( + uint256 initialDepositAmount, + uint256 tbtcAmount, + bytes32 extraData + ) = _finalizeDeposit(depositKey); + + // Check if current balance is sufficient to finalize bridging of `tbtcAmount`. + uint256 currentBalance = tbtcToken.balanceOf(address(this)); + if (tbtcAmount > tbtcToken.balanceOf(address(this))) { + revert InsufficientTbtcBalance(tbtcAmount, currentBalance); + } + + // Compute depositor fee. The fee is calculated based on the initial funding + // transaction amount, before the tBTC protocol network fees were taken. + uint256 depositorFee = depositorFeeDivisor > 0 + ? (initialDepositAmount / depositorFeeDivisor) + : 0; + + // Ensure the depositor fee does not exceed the approximate minted tBTC + // amount. + if (depositorFee >= tbtcAmount) { + revert DepositorFeeExceedsBridgedAmount(depositorFee, tbtcAmount); + } + + uint256 amountToStake = tbtcAmount - depositorFee; + + (address staker, uint16 referral) = decodeExtraData(extraData); + + // Emit event for accounting purposes to track partner's referral ID and + // depositor fee taken. + emit BridgingCompleted( + depositKey, + msg.sender, + referral, + tbtcAmount, + depositorFee + ); + + // Transfer depositor fee to the treasury wallet. + if (depositorFee > 0) { + tbtcToken.safeTransfer(stbtc.treasury(), depositorFee); + } + + return (amountToStake, staker); + } +} diff --git a/core/contracts/Dispatcher.sol b/core/contracts/Dispatcher.sol index cac8fa317..ec1696239 100644 --- a/core/contracts/Dispatcher.sol +++ b/core/contracts/Dispatcher.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/access/Ownable2Step.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/interfaces/IERC4626.sol"; import "./Router.sol"; @@ -11,7 +11,7 @@ import "./stBTC.sol"; /// @notice Dispatcher is a contract that routes tBTC from stBTC to /// yield vaults and back. Vaults supply yield strategies with tBTC that /// generate yield for Bitcoin holders. -contract Dispatcher is Router, Ownable { +contract Dispatcher is Router, Ownable2Step { using SafeERC20 for IERC20; /// Struct holds information about a vault. diff --git a/core/contracts/TbtcDepositor.sol b/core/contracts/TbtcDepositor.sol deleted file mode 100644 index 732d289c0..000000000 --- a/core/contracts/TbtcDepositor.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol"; - -contract TbtcDepositor is AbstractTBTCDepositor { - function initializeStakeRequest( - IBridgeTypes.BitcoinTxInfo calldata fundingTx, - IBridgeTypes.DepositRevealInfo calldata reveal, - address receiver, - uint16 referral - ) external {} -} diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 9b5c186a4..b3c112869 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/access/Ownable2Step.sol"; import "./Dispatcher.sol"; /// @title stBTC @@ -17,7 +17,7 @@ import "./Dispatcher.sol"; /// of yield-bearing vaults. This contract facilitates the minting and /// burning of shares (stBTC), which are represented as standard ERC20 /// tokens, providing a seamless exchange with tBTC tokens. -contract stBTC is ERC4626, Ownable { +contract stBTC is ERC4626, Ownable2Step { using SafeERC20 for IERC20; /// Dispatcher contract that routes tBTC from stBTC to a given vault and back. @@ -26,8 +26,13 @@ contract stBTC is ERC4626, Ownable { /// Address of the treasury wallet, where fees should be transferred to. address public treasury; - /// Minimum amount for a single deposit operation. + /// Minimum amount for a single deposit operation. The value should be set + /// low enough so the deposits routed through Bitcoin Depositor contract won't + /// be rejected. It means that minimumDepositAmount should be lower than + /// tBTC protocol's depositDustThreshold reduced by all the minting fees taken + /// before depositing in the Acre contract. uint256 public minimumDepositAmount; + /// Maximum total amount of tBTC token held by Acre protocol. uint256 public maximumTotalAssets; diff --git a/core/contracts/test/AcreBitcoinDepositorHarness.sol b/core/contracts/test/AcreBitcoinDepositorHarness.sol new file mode 100644 index 000000000..75531dd3b --- /dev/null +++ b/core/contracts/test/AcreBitcoinDepositorHarness.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* solhint-disable func-name-mixedcase */ +pragma solidity ^0.8.21; + +import {AcreBitcoinDepositor} from "../AcreBitcoinDepositor.sol"; +import {MockBridge, MockTBTCVault} from "@keep-network/tbtc-v2/contracts/test/TestTBTCDepositor.sol"; +import {IBridge} from "@keep-network/tbtc-v2/contracts/integrator/IBridge.sol"; +import {IBridgeTypes} from "@keep-network/tbtc-v2/contracts/integrator/IBridge.sol"; + +import {TestERC20} from "./TestERC20.sol"; + +/// @dev A test contract to expose internal function from AcreBitcoinDepositor contract. +/// This solution follows Foundry recommendation: +/// https://book.getfoundry.sh/tutorials/best-practices#internal-functions +contract AcreBitcoinDepositorHarness is AcreBitcoinDepositor { + constructor( + address bridge, + address tbtcVault, + address tbtcToken, + address stbtc + ) AcreBitcoinDepositor(bridge, tbtcVault, tbtcToken, stbtc) {} + + function exposed_finalizeBridging( + uint256 depositKey + ) external returns (uint256 amountToStake, address staker) { + return finalizeBridging(depositKey); + } + + function exposed_setQueuedStakesBalance(uint256 amount) external { + queuedStakesBalance = amount; + } +} + +/// @dev A test contract to stub tBTC Bridge contract. +contract BridgeStub is MockBridge {} + +/// @dev A test contract to stub tBTC Vault contract. +contract TBTCVaultStub is MockTBTCVault { + TestERC20 public immutable tbtc; + IBridge public immutable bridge; + + /// @notice Multiplier to convert satoshi to TBTC token units. + uint256 public constant SATOSHI_MULTIPLIER = 10 ** 10; + + constructor(TestERC20 _tbtc, IBridge _bridge) { + tbtc = _tbtc; + bridge = _bridge; + } + + function finalizeOptimisticMintingRequest( + uint256 depositKey + ) public override { + IBridgeTypes.DepositRequest memory deposit = bridge.deposits( + depositKey + ); + + uint256 amountSubTreasury = (deposit.amount - deposit.treasuryFee) * + SATOSHI_MULTIPLIER; + + uint256 omFee = optimisticMintingFeeDivisor > 0 + ? (amountSubTreasury / optimisticMintingFeeDivisor) + : 0; + + // The deposit transaction max fee is in the 1e8 satoshi precision. + // We need to convert them to the 1e18 TBTC precision. + // slither-disable-next-line unused-return + (, , uint64 depositTxMaxFee, ) = bridge.depositParameters(); + uint256 txMaxFee = depositTxMaxFee * SATOSHI_MULTIPLIER; + + uint256 amountToMint = amountSubTreasury - omFee - txMaxFee; + + finalizeOptimisticMintingRequestWithAmount(depositKey, amountToMint); + } + + function finalizeOptimisticMintingRequestWithAmount( + uint256 depositKey, + uint256 amountToMint + ) public { + MockTBTCVault.finalizeOptimisticMintingRequest(depositKey); + + tbtc.mint(bridge.deposits(depositKey).depositor, amountToMint); + } +} diff --git a/core/contracts/test/TestERC20.sol b/core/contracts/test/TestERC20.sol index 44e5e14dc..c6b25896d 100644 --- a/core/contracts/test/TestERC20.sol +++ b/core/contracts/test/TestERC20.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract TestERC20 is ERC20 { - constructor() ERC20("Test Token", "TEST") {} + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} function mint(address account, uint256 value) external { _mint(account, value); diff --git a/core/deploy/00_resolve_tbtc_bridge.ts b/core/deploy/00_resolve_tbtc_bridge.ts new file mode 100644 index 000000000..4150b696a --- /dev/null +++ b/core/deploy/00_resolve_tbtc_bridge.ts @@ -0,0 +1,35 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { + HardhatNetworkConfig, + HardhatRuntimeEnvironment, +} from "hardhat/types" +import { isNonZeroAddress } from "../helpers/address" +import { waitConfirmationsNumber } from "../helpers/deployment" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { log } = deployments + const { deployer } = await getNamedAccounts() + + const bridge = await deployments.getOrNull("Bridge") + + if (bridge && isNonZeroAddress(bridge.address)) { + log(`using Bridge contract at ${bridge.address}`) + } else if ((hre.network.config as HardhatNetworkConfig)?.forking?.enabled) { + throw new Error("deployed Bridge contract not found") + } else { + log("deploying Bridge contract stub") + + await deployments.deploy("Bridge", { + contract: "BridgeStub", + args: [], + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }) + } +} + +export default func + +func.tags = ["TBTC", "Bridge"] diff --git a/core/deploy/00_resolve_tbtc.ts b/core/deploy/00_resolve_tbtc_token.ts similarity index 63% rename from core/deploy/00_resolve_tbtc.ts rename to core/deploy/00_resolve_tbtc_token.ts index dc1bfeff5..ef00bcdef 100644 --- a/core/deploy/00_resolve_tbtc.ts +++ b/core/deploy/00_resolve_tbtc_token.ts @@ -1,6 +1,10 @@ -import type { HardhatRuntimeEnvironment } from "hardhat/types" import type { DeployFunction } from "hardhat-deploy/types" +import type { + HardhatNetworkConfig, + HardhatRuntimeEnvironment, +} from "hardhat/types" import { isNonZeroAddress } from "../helpers/address" +import { waitConfirmationsNumber } from "../helpers/deployment" const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, deployments } = hre @@ -11,20 +15,24 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { if (tbtc && isNonZeroAddress(tbtc.address)) { log(`using TBTC contract at ${tbtc.address}`) - } else if (!hre.network.tags.allowStubs) { + } else if ( + !hre.network.tags.allowStubs || + (hre.network.config as HardhatNetworkConfig)?.forking?.enabled + ) { throw new Error("deployed TBTC contract not found") } else { log("deploying TBTC contract stub") await deployments.deploy("TBTC", { contract: "TestERC20", + args: ["Test tBTC", "TestTBTC"], from: deployer, log: true, - waitConfirmations: 1, + waitConfirmations: waitConfirmationsNumber(hre), }) } } export default func -func.tags = ["TBTC"] +func.tags = ["TBTC", "TBTCToken"] diff --git a/core/deploy/00_resolve_tbtc_vault.ts b/core/deploy/00_resolve_tbtc_vault.ts new file mode 100644 index 000000000..46edbd919 --- /dev/null +++ b/core/deploy/00_resolve_tbtc_vault.ts @@ -0,0 +1,42 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { + HardhatNetworkConfig, + HardhatRuntimeEnvironment, +} from "hardhat/types" +import { isNonZeroAddress } from "../helpers/address" +import { waitConfirmationsNumber } from "../helpers/deployment" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { log } = deployments + const { deployer } = await getNamedAccounts() + + const tbtcVault = await deployments.getOrNull("TBTCVault") + + if (tbtcVault && isNonZeroAddress(tbtcVault.address)) { + log(`using TBTCVault contract at ${tbtcVault.address}`) + } else if ( + !hre.network.tags.allowStubs || + (hre.network.config as HardhatNetworkConfig)?.forking?.enabled + ) { + throw new Error("deployed TBTCVault contract not found") + } else { + log("deploying TBTCVault contract stub") + + const tbtc = await deployments.get("TBTC") + const bridge = await deployments.get("Bridge") + + await deployments.deploy("TBTCVault", { + contract: "TBTCVaultStub", + args: [tbtc.address, bridge.address], + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }) + } +} + +export default func + +func.tags = ["TBTC", "TBTCVault"] +func.dependencies = ["TBTCToken", "Bridge"] diff --git a/core/deploy/02_deploy_dispatcher.ts b/core/deploy/02_deploy_dispatcher.ts index 8104d906f..4473531e9 100644 --- a/core/deploy/02_deploy_dispatcher.ts +++ b/core/deploy/02_deploy_dispatcher.ts @@ -9,7 +9,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const tbtc = await deployments.get("TBTC") const stbtc = await deployments.get("stBTC") - await deployments.deploy("Dispatcher", { + const dispatcher = await deployments.deploy("Dispatcher", { from: deployer, args: [stbtc.address, tbtc.address], log: true, @@ -17,7 +17,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { }) if (hre.network.tags.etherscan) { - await helpers.etherscan.verify(stbtc) + await helpers.etherscan.verify(dispatcher) } // TODO: Add Tenderly verification diff --git a/core/deploy/03_deploy_acre_bitcoin_depositor.ts b/core/deploy/03_deploy_acre_bitcoin_depositor.ts new file mode 100644 index 000000000..4863dd77b --- /dev/null +++ b/core/deploy/03_deploy_acre_bitcoin_depositor.ts @@ -0,0 +1,35 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import { waitConfirmationsNumber } from "../helpers/deployment" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments, helpers } = hre + const { deployer } = await getNamedAccounts() + + const bridge = await deployments.get("Bridge") + const tbtcVault = await deployments.get("TBTCVault") + const tbtc = await deployments.get("TBTC") + const stbtc = await deployments.get("stBTC") + + const depositor = await deployments.deploy("AcreBitcoinDepositor", { + contract: + process.env.HARDHAT_TEST === "true" + ? "AcreBitcoinDepositorHarness" + : "AcreBitcoinDepositor", + from: deployer, + args: [bridge.address, tbtcVault.address, tbtc.address, stbtc.address], + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }) + + if (hre.network.tags.etherscan) { + await helpers.etherscan.verify(depositor) + } + + // TODO: Add Tenderly verification +} + +export default func + +func.tags = ["AcreBitcoinDepositor"] +func.dependencies = ["TBTC", "stBTC"] diff --git a/core/deploy/21_transfer_ownership_stbtc.ts b/core/deploy/21_transfer_ownership_stbtc.ts index d3554032e..fa2ad6c47 100644 --- a/core/deploy/21_transfer_ownership_stbtc.ts +++ b/core/deploy/21_transfer_ownership_stbtc.ts @@ -14,8 +14,17 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { "transferOwnership", governance, ) + + if (hre.network.name !== "mainnet") { + await deployments.execute( + "stBTC", + { from: governance, log: true, waitConfirmations: 1 }, + "acceptOwnership", + ) + } } export default func func.tags = ["TransferOwnershipStBTC"] +func.dependencies = ["stBTC"] diff --git a/core/deploy/22_transfer_ownership_dispatcher.ts b/core/deploy/22_transfer_ownership_dispatcher.ts index 5e85a15d0..5b8e930dc 100644 --- a/core/deploy/22_transfer_ownership_dispatcher.ts +++ b/core/deploy/22_transfer_ownership_dispatcher.ts @@ -14,6 +14,14 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { "transferOwnership", governance, ) + + if (hre.network.name !== "mainnet") { + await deployments.execute( + "Dispatcher", + { from: governance, log: true, waitConfirmations: 1 }, + "acceptOwnership", + ) + } } export default func diff --git a/core/deploy/23_transfer_ownership_acre_bitcoin_depositor.ts b/core/deploy/23_transfer_ownership_acre_bitcoin_depositor.ts new file mode 100644 index 000000000..c04f6f679 --- /dev/null +++ b/core/deploy/23_transfer_ownership_acre_bitcoin_depositor.ts @@ -0,0 +1,32 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { deployer, governance } = await getNamedAccounts() + const { log } = deployments + + log( + `transferring ownership of AcreBitcoinDepositor contract to ${governance}`, + ) + + await deployments.execute( + "AcreBitcoinDepositor", + { from: deployer, log: true, waitConfirmations: 1 }, + "transferOwnership", + governance, + ) + + if (hre.network.name !== "mainnet") { + await deployments.execute( + "AcreBitcoinDepositor", + { from: governance, log: true, waitConfirmations: 1 }, + "acceptOwnership", + ) + } +} + +export default func + +func.tags = ["TransferOwnershipAcreBitcoinDepositor"] +func.dependencies = ["AcreBitcoinDepositor"] diff --git a/core/package.json b/core/package.json index 46baa37f3..58568dc37 100644 --- a/core/package.json +++ b/core/package.json @@ -26,7 +26,7 @@ "lint:sol:fix": "solhint 'contracts/**/*.sol' --fix && prettier --write 'contracts/**/*.sol'", "lint:config": "prettier --check '**/*.@(json)'", "lint:config:fix": "prettier --write '**/*.@(json)'", - "test": "hardhat test" + "test": "HARDHAT_TEST=true hardhat test" }, "devDependencies": { "@keep-network/hardhat-helpers": "^0.7.1", diff --git a/core/slither.config.json b/core/slither.config.json index fed211ee1..46c3cb424 100644 --- a/core/slither.config.json +++ b/core/slither.config.json @@ -1,5 +1,5 @@ { "detectors_to_exclude": "assembly,naming-convention,timestamp,pragma,solc-version", "hardhat_artifacts_directory": "build", - "filter_paths": "node_modules/.*" + "filter_paths": "contracts/test|node_modules" } diff --git a/core/test/AcreBitcoinDepositor.test.ts b/core/test/AcreBitcoinDepositor.test.ts new file mode 100644 index 000000000..313ba35a3 --- /dev/null +++ b/core/test/AcreBitcoinDepositor.test.ts @@ -0,0 +1,1924 @@ +/* eslint-disable func-names */ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" + +import { ethers, helpers } from "hardhat" +import { expect } from "chai" +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" +import { ContractTransactionResponse, MaxUint256, ZeroAddress } from "ethers" + +import { StakeRequestState } from "../types" + +import type { + StBTC, + BridgeStub, + TBTCVaultStub, + AcreBitcoinDepositorHarness, + TestERC20, +} from "../typechain" +import { deployment } from "./helpers" +import { beforeAfterSnapshotWrapper } from "./helpers/snapshot" +import { tbtcDepositData } from "./data/tbtc" +import { to1ePrecision } from "./utils" + +async function fixture() { + const { bitcoinDepositor, tbtcBridge, tbtcVault, stbtc, tbtc } = + await deployment() + + return { bitcoinDepositor, tbtcBridge, tbtcVault, stbtc, tbtc } +} + +const { lastBlockTime } = helpers.time +const { getNamedSigners, getUnnamedSigners } = helpers.signers + +describe("AcreBitcoinDepositor", () => { + const defaultDepositDustThreshold = 1000000 // 1000000 satoshi = 0.01 BTC + const defaultDepositTreasuryFeeDivisor = 2000 // 1/2000 = 0.05% = 0.0005 + const defaultDepositTxMaxFee = 1000 // 1000 satoshi = 0.00001 BTC + const defaultOptimisticFeeDivisor = 500 // 1/500 = 0.002 = 0.2% + const defaultDepositorFeeDivisor = 1000 // 1/1000 = 0.001 = 0.1% + + const initialQueuedStakesBalance = to1ePrecision(30000, 10) // 30000 satoshi + + // Funding transaction amount: 10000 satoshi + // tBTC Deposit Treasury Fee: 0.05% = 10000 * 0.05% = 5 satoshi + // tBTC Optimistic Minting Fee: 0.2% = (10000 - 5) * 0.2% = 19,99 satoshi + // tBTC Deposit Transaction Max Fee: 1000 satoshi + // Depositor Fee: 1.25% = 10000 satoshi * 0.01% = 10 satoshi + // Amounts below are calculated in 1e18 precision: + const initialDepositAmount = to1ePrecision(10000, 10) // 10000 satoshi + const bridgedTbtcAmount = to1ePrecision(897501, 8) // 8975,01 satoshi + const depositorFee = to1ePrecision(10, 10) // 10 satoshi + const amountToStake = to1ePrecision(896501, 8) // 8965,01 satoshi + + let bitcoinDepositor: AcreBitcoinDepositorHarness + let tbtcBridge: BridgeStub + let tbtcVault: TBTCVaultStub + let stbtc: StBTC + let tbtc: TestERC20 + + let governance: HardhatEthersSigner + let treasury: HardhatEthersSigner + let thirdParty: HardhatEthersSigner + let staker: HardhatEthersSigner + + before(async () => { + ;({ bitcoinDepositor, tbtcBridge, tbtcVault, stbtc, tbtc } = + await loadFixture(fixture)) + ;({ governance, treasury } = await getNamedSigners()) + ;[thirdParty] = await getUnnamedSigners() + + staker = await helpers.account.impersonateAccount(tbtcDepositData.staker, { + from: thirdParty, + }) + + await stbtc.connect(governance).updateDepositParameters( + 10000000000000, // 0.00001 + await stbtc.maximumTotalAssets(), + ) + + tbtcDepositData.reveal.vault = await tbtcVault.getAddress() + + await tbtcBridge + .connect(governance) + .setDepositDustThreshold(defaultDepositDustThreshold) + await tbtcBridge + .connect(governance) + .setDepositTreasuryFeeDivisor(defaultDepositTreasuryFeeDivisor) + await tbtcBridge + .connect(governance) + .setDepositTxMaxFee(defaultDepositTxMaxFee) + await tbtcVault + .connect(governance) + .setOptimisticMintingFeeDivisor(defaultOptimisticFeeDivisor) + await bitcoinDepositor + .connect(governance) + .updateDepositorFeeDivisor(defaultDepositorFeeDivisor) + }) + + describe("initializeStake", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor.exposed_setQueuedStakesBalance( + initialQueuedStakesBalance, + ) + }) + + describe("when staker is zero address", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor.initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + ZeroAddress, + 0, + ), + ).to.be.revertedWithCustomError(bitcoinDepositor, "StakerIsZeroAddress") + }) + }) + + describe("when staker is non zero address", () => { + describe("when stake is not in progress", () => { + describe("when tbtc vault address is incorrect", () => { + beforeAfterSnapshotWrapper() + + it("should revert", async () => { + const invalidTbtcVault = + await ethers.Wallet.createRandom().getAddress() + + await expect( + bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + { ...tbtcDepositData.reveal, vault: invalidTbtcVault }, + tbtcDepositData.staker, + tbtcDepositData.referral, + ), + ).to.be.revertedWith("Vault address mismatch") + }) + }) + + describe("when tbtc vault address is correct", () => { + describe("when referral is non-zero", () => { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + tbtcDepositData.referral, + ) + }) + + it("should emit StakeRequestInitialized event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "StakeRequestInitialized") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + tbtcDepositData.staker, + ) + }) + + it("should update stake state", async () => { + const stakeRequest = await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect(stakeRequest.state).to.be.equal( + StakeRequestState.Initialized, + ) + }) + + it("should not store stake data in queue", async () => { + const stakeRequest = await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect(stakeRequest.staker, "invalid staker").to.be.equal( + ZeroAddress, + ) + expect( + stakeRequest.queuedAmount, + "invalid queuedAmount", + ).to.be.equal(0) + }) + + it("should not update queuedStakesBalance", async () => { + expect(await bitcoinDepositor.queuedStakesBalance()).to.be.equal( + initialQueuedStakesBalance, + ) + }) + + it("should reveal the deposit to the bridge contract with extra data", async () => { + const storedRevealedDeposit = await tbtcBridge.deposits( + tbtcDepositData.depositKey, + ) + + expect( + storedRevealedDeposit.revealedAt, + "invalid revealedAt", + ).to.be.equal(await lastBlockTime()) + + expect( + storedRevealedDeposit.extraData, + "invalid extraData", + ).to.be.equal(tbtcDepositData.extraData) + }) + }) + + describe("when referral is zero", () => { + beforeAfterSnapshotWrapper() + + it("should succeed", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + 0, + ), + ).to.be.not.reverted + }) + }) + }) + }) + + describe("when stake is already in progress", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + tbtcDepositData.referral, + ), + ).to.be.revertedWith("Deposit already revealed") + }) + }) + + describe("when stake is already finalized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey) + + await bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + tbtcDepositData.referral, + ), + ).to.be.revertedWith("Deposit already revealed") + }) + }) + + describe("when stake is already queued", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey) + + await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + tbtcDepositData.referral, + ), + ).to.be.revertedWith("Deposit already revealed") + }) + }) + + describe("when stake is already cancelled", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey) + + await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + + await bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + tbtcDepositData.referral, + ), + ).to.be.revertedWith("Deposit already revealed") + }) + }) + }) + }) + + describe("finalizeBridging", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor.exposed_setQueuedStakesBalance( + initialQueuedStakesBalance, + ) + }) + + describe("when stake has not been initialized", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging(tbtcDepositData.depositKey), + ).to.be.revertedWith("Deposit not initialized") + }) + }) + + describe("when stake has been initialized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + describe("when deposit was not bridged", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging(tbtcDepositData.depositKey), + ).to.be.revertedWith("Deposit not finalized by the bridge") + }) + }) + + describe("when deposit was bridged", () => { + beforeAfterSnapshotWrapper() + + describe("when depositor contract balance is lower than bridged amount", () => { + beforeAfterSnapshotWrapper() + + // The minted value should be less than calculated `bridgedTbtcAmount`. + const mintedAmount = to1ePrecision(7455, 10) // 7455 satoshi + + before(async () => { + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey, mintedAmount) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "InsufficientTbtcBalance", + ) + .withArgs(bridgedTbtcAmount, mintedAmount) + }) + }) + + describe("when depositor contract balance is higher than bridged amount", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey) + }) + + describe("when bridging finalization has not been called", () => { + describe("when depositor fee divisor is not zero", () => { + beforeAfterSnapshotWrapper() + + let returnedValue: bigint + let tx: ContractTransactionResponse + + before(async () => { + returnedValue = await bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging.staticCall( + tbtcDepositData.depositKey, + ) + + tx = await bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging(tbtcDepositData.depositKey) + }) + + it("should emit BridgingCompleted event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "BridgingCompleted") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + tbtcDepositData.referral, + bridgedTbtcAmount, + depositorFee, + ) + }) + + it("should return amount to stake", () => { + expect(returnedValue[0]).to.be.equal(amountToStake) + }) + + it("should return staker", () => { + expect(returnedValue[1]).to.be.equal(tbtcDepositData.staker) + }) + + it("should transfer depositor fee", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [treasury], + [depositorFee], + ) + }) + }) + + describe("when depositor fee divisor is zero", () => { + beforeAfterSnapshotWrapper() + + let returnedValue: bigint + let tx: ContractTransactionResponse + + before(async () => { + await bitcoinDepositor + .connect(governance) + .updateDepositorFeeDivisor(0) + + returnedValue = await bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging.staticCall( + tbtcDepositData.depositKey, + ) + + tx = await bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging(tbtcDepositData.depositKey) + }) + + it("should emit BridgingCompleted event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "BridgingCompleted") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + tbtcDepositData.referral, + bridgedTbtcAmount, + 0, + ) + }) + + it("should return amount to stake", () => { + expect(returnedValue[0]).to.be.equal(bridgedTbtcAmount) + }) + + it("should return staker", () => { + expect(returnedValue[1]).to.be.equal(tbtcDepositData.staker) + }) + + it("should not transfer depositor fee", async () => { + await expect(tx).to.changeTokenBalances(tbtc, [treasury], [0]) + }) + }) + + describe("when depositor fee exceeds bridged amount", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(governance) + .updateDepositorFeeDivisor(1) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "DepositorFeeExceedsBridgedAmount", + ) + .withArgs(initialDepositAmount, bridgedTbtcAmount) + }) + }) + }) + }) + }) + }) + }) + + describe("finalizeStake", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor.exposed_setQueuedStakesBalance( + initialQueuedStakesBalance, + ) + }) + + describe("when stake has not been initialized", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs(StakeRequestState.Unknown, StakeRequestState.Initialized) + }) + }) + + describe("when stake has been initialized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + describe("when deposit was not bridged", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ).to.be.revertedWith("Deposit not finalized by the bridge") + }) + }) + + describe("when deposit was bridged", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey) + }) + + describe("when stake has not been finalized", () => { + beforeAfterSnapshotWrapper() + + const expectedAssetsAmount = amountToStake + const expectedReceivedSharesAmount = amountToStake + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey) + }) + + it("should emit BridgingCompleted event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "BridgingCompleted") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + tbtcDepositData.referral, + bridgedTbtcAmount, + depositorFee, + ) + }) + + it("should transfer depositor fee", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [treasury], + [depositorFee], + ) + }) + + it("should update stake state", async () => { + const stakeRequest = await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect(stakeRequest.state).to.be.equal(StakeRequestState.Finalized) + }) + + it("should not update queuedStakesBalance", async () => { + expect(await bitcoinDepositor.queuedStakesBalance()).to.be.equal( + initialQueuedStakesBalance, + ) + }) + + it("should emit StakeRequestFinalized event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "StakeRequestFinalized") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + expectedAssetsAmount, + ) + }) + + it("should emit Deposit event", async () => { + await expect(tx) + .to.emit(stbtc, "Deposit") + .withArgs( + await bitcoinDepositor.getAddress(), + tbtcDepositData.staker, + expectedAssetsAmount, + expectedReceivedSharesAmount, + ) + }) + + it("should stake in Acre contract", async () => { + await expect( + tx, + "invalid minted stBTC amount", + ).to.changeTokenBalances( + stbtc, + [tbtcDepositData.staker], + [expectedReceivedSharesAmount], + ) + + await expect( + tx, + "invalid staked tBTC amount", + ).to.changeTokenBalances(tbtc, [stbtc], [expectedAssetsAmount]) + }) + }) + + describe("when stake has been queued", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + }) + + describe("when stake is still in the queue", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.Queued, + StakeRequestState.Initialized, + ) + }) + }) + + describe("when stake is finalized from the queue", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.FinalizedFromQueue, + StakeRequestState.Initialized, + ) + }) + }) + + describe("when stake has been cancelled", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.CancelledFromQueue, + StakeRequestState.Initialized, + ) + }) + }) + }) + + describe("when stake has been finalized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + // Finalize stake. + await bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.Finalized, + StakeRequestState.Initialized, + ) + }) + }) + }) + }) + }) + + describe("queueStake", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor.exposed_setQueuedStakesBalance( + initialQueuedStakesBalance, + ) + }) + + describe("when stake has not been initialized", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs(StakeRequestState.Unknown, StakeRequestState.Initialized) + }) + }) + + describe("when stake has been initialized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + describe("when deposit was not bridged", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey), + ).to.be.revertedWith("Deposit not finalized by the bridge") + }) + }) + + describe("when deposit was bridged", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey) + }) + + describe("when stake has not been finalized", () => { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + }) + + it("should emit BridgingCompleted event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "BridgingCompleted") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + tbtcDepositData.referral, + bridgedTbtcAmount, + depositorFee, + ) + }) + + it("should transfer depositor fee", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [treasury], + [depositorFee], + ) + }) + + it("should update stake state", async () => { + const stakeRequest = await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect(stakeRequest.state).to.be.equal(StakeRequestState.Queued) + }) + + it("should set staker", async () => { + expect( + (await bitcoinDepositor.stakeRequests(tbtcDepositData.depositKey)) + .staker, + ).to.be.equal(tbtcDepositData.staker) + }) + + it("should set queuedAmount", async () => { + expect( + (await bitcoinDepositor.stakeRequests(tbtcDepositData.depositKey)) + .queuedAmount, + ).to.be.equal(amountToStake) + }) + + it("should update queuedStakesBalance", async () => { + expect(await bitcoinDepositor.queuedStakesBalance()).to.be.equal( + initialQueuedStakesBalance + amountToStake, + ) + }) + + it("should not emit StakeRequestQueued event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "StakeRequestQueued") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + amountToStake, + ) + }) + + it("should not emit StakeRequestFinalized event", async () => { + await expect(tx).to.not.emit( + bitcoinDepositor, + "StakeRequestFinalized", + ) + }) + + it("should not emit Deposit event", async () => { + await expect(tx).to.not.emit(stbtc, "Deposit") + }) + + it("should not stake in Acre contract", async () => { + await expect( + tx, + "invalid minted stBTC amount", + ).to.changeTokenBalances(stbtc, [tbtcDepositData.staker], [0]) + + await expect( + tx, + "invalid staked tBTC amount", + ).to.changeTokenBalances(tbtc, [stbtc], [0]) + }) + }) + + describe("when stake has been queued", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + }) + + describe("when stake is still in the queue", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.Queued, + StakeRequestState.Initialized, + ) + }) + }) + + describe("when stake is finalized from the queue", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.FinalizedFromQueue, + StakeRequestState.Initialized, + ) + }) + }) + + describe("when stake has been cancelled", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.CancelledFromQueue, + StakeRequestState.Initialized, + ) + }) + }) + }) + + describe("when stake has been finalized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + // Finalize stake. + await bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.Finalized, + StakeRequestState.Initialized, + ) + }) + }) + }) + }) + }) + + describe("finalizeQueuedStake", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor.exposed_setQueuedStakesBalance( + initialQueuedStakesBalance, + ) + }) + + describe("when stake has not been initialized", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(staker) + .finalizeQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs(StakeRequestState.Unknown, StakeRequestState.Queued) + }) + }) + + describe("when stake has been initialized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + describe("when stake has not been queued", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs(StakeRequestState.Initialized, StakeRequestState.Queued) + }) + }) + + describe("when stake has been queued", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await finalizeMinting(tbtcDepositData.depositKey) + + await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + }) + + describe("when stake has not been finalized", () => { + beforeAfterSnapshotWrapper() + + const expectedAssetsAmount = amountToStake + const expectedReceivedSharesAmount = amountToStake + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey) + }) + + it("should update stake state", async () => { + const stakeRequest = await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect(stakeRequest.state).to.be.equal( + StakeRequestState.FinalizedFromQueue, + ) + }) + + it("should set queuedAmount to zero", async () => { + expect( + (await bitcoinDepositor.stakeRequests(tbtcDepositData.depositKey)) + .queuedAmount, + ).to.be.equal(0) + }) + + it("should update queuedStakesBalance", async () => { + expect(await bitcoinDepositor.queuedStakesBalance()).to.be.equal( + initialQueuedStakesBalance, + ) + }) + + it("should emit StakeRequestFinalizedFromQueue event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "StakeRequestFinalizedFromQueue") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + expectedAssetsAmount, + ) + }) + + it("should emit Deposit event", async () => { + await expect(tx) + .to.emit(stbtc, "Deposit") + .withArgs( + await bitcoinDepositor.getAddress(), + tbtcDepositData.staker, + expectedAssetsAmount, + expectedReceivedSharesAmount, + ) + }) + + it("should stake in Acre contract", async () => { + await expect( + tx, + "invalid minted stBTC amount", + ).to.changeTokenBalances( + stbtc, + [tbtcDepositData.staker], + [expectedReceivedSharesAmount], + ) + + await expect( + tx, + "invalid staked tBTC amount", + ).to.changeTokenBalances(tbtc, [stbtc], [expectedAssetsAmount]) + }) + }) + + describe("when stake has been finalized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.FinalizedFromQueue, + StakeRequestState.Queued, + ) + }) + }) + + describe("when stake has been cancelled", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.CancelledFromQueue, + StakeRequestState.Queued, + ) + }) + }) + }) + }) + }) + + describe("cancelQueuedStake", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor.exposed_setQueuedStakesBalance( + initialQueuedStakesBalance, + ) + }) + + describe("when stake has not been initialized", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs(StakeRequestState.Unknown, StakeRequestState.Queued) + }) + }) + + describe("when stake has been initialized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + describe("when stake has not been queued", () => { + beforeAfterSnapshotWrapper() + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs(StakeRequestState.Initialized, StakeRequestState.Queued) + }) + }) + + describe("when stake has been queued", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await finalizeMinting(tbtcDepositData.depositKey) + + await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + }) + + describe("when stake has not been cancelled", () => { + describe("when caller is non-staker", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .cancelQueuedStake(tbtcDepositData.depositKey), + ).to.be.revertedWithCustomError( + bitcoinDepositor, + "CallerNotStaker", + ) + }) + }) + + describe("when caller is staker", () => { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey) + }) + + it("should update stake state", async () => { + const stakeRequest = await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect(stakeRequest.state).to.be.equal( + StakeRequestState.CancelledFromQueue, + ) + }) + + it("should set queuedAmount to zero", async () => { + expect( + ( + await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + ).queuedAmount, + ).to.be.equal(0) + }) + + it("should update queuedStakesBalance", async () => { + expect(await bitcoinDepositor.queuedStakesBalance()).to.be.equal( + initialQueuedStakesBalance, + ) + }) + + it("should emit StakeRequestCancelledFromQueue event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "StakeRequestCancelledFromQueue") + .withArgs( + tbtcDepositData.depositKey, + staker.address, + amountToStake, + ) + }) + + it("should transfer tbtc to staker", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [bitcoinDepositor, staker], + [-amountToStake, amountToStake], + ) + }) + }) + }) + + describe("when stake has been finalized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.FinalizedFromQueue, + StakeRequestState.Queued, + ) + }) + }) + + describe("when stake has been cancelled", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.CancelledFromQueue, + StakeRequestState.Queued, + ) + }) + }) + }) + }) + }) + + describe("maxStake", () => { + beforeAfterSnapshotWrapper() + + const maxTotalAssetsSoftLimit = to1ePrecision(100000000, 10) // 100000000 satoshi = 1 BTC + + before(async () => { + await bitcoinDepositor + .connect(governance) + .updateMaxTotalAssetsSoftLimit(maxTotalAssetsSoftLimit) + }) + + describe("total assets of stBTC are greater than the soft limit", () => { + beforeAfterSnapshotWrapper() + + const currentTotalAssets = maxTotalAssetsSoftLimit + + before(async () => { + // Simulate tBTC already deposited in stBTC. + await tbtc.mint(await stbtc.getAddress(), currentTotalAssets) + }) + + it("should return 0", async () => { + expect(await bitcoinDepositor.maxStake()).to.be.equal(0) + }) + }) + + describe("total assets of stBTC are less than the soft limit", () => { + beforeAfterSnapshotWrapper() + + const currentTotalAssets = to1ePrecision(60000000, 10) // 60000000 satoshi = 0.6 BTC + const availableSoftLimit = to1ePrecision(40000000, 10) // 40000000 satoshi = 0.4 BTC + + before(async () => { + // Simulate tBTC already deposited in stBTC. + await tbtc.mint(await stbtc.getAddress(), currentTotalAssets) + + expect(await stbtc.totalAssets()).to.be.equal(currentTotalAssets) + }) + + describe("stakes queue balance exceeds the available limit", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor.exposed_setQueuedStakesBalance( + availableSoftLimit, + ) + }) + + it("should return 0", async () => { + expect(await bitcoinDepositor.maxStake()).to.be.equal(0) + }) + }) + + describe("stakes queue balance is less than the available limit", () => { + const stakesQueueBalance = to1ePrecision(10000000, 10) // 10000000 satoshi = 0.1 BTC + const availableSoftLimitMinusQueue = to1ePrecision(30000000, 10) // 30000000 satoshi = 0.3 BTC + + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor.exposed_setQueuedStakesBalance( + stakesQueueBalance, + ) + }) + + describe("max single stake amount is zero", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(governance) + .updateMaxSingleStakeAmount(0) + }) + + it("should return zero", async () => { + expect(await bitcoinDepositor.maxStake()).to.be.equal(0) + }) + }) + + describe("max single stake amount is less than the available limit", () => { + beforeAfterSnapshotWrapper() + + const maxSingleStakeAmount = to1ePrecision(20000000, 10) // 20000000 satoshi = 0.2 BTC + const expectedResult = maxSingleStakeAmount + + before(async () => { + await bitcoinDepositor + .connect(governance) + .updateMaxSingleStakeAmount(maxSingleStakeAmount) + }) + + it("should return max single stake amount", async () => { + expect(await bitcoinDepositor.maxStake()).to.be.equal( + expectedResult, + ) + }) + }) + + describe("max single stake amount is greater than the available limit", () => { + beforeAfterSnapshotWrapper() + + const maxSingleStakeAmount = to1ePrecision(35000000, 10) // 35000000 satoshi = 0.35 BTC + const expectedResult = availableSoftLimitMinusQueue + + before(async () => { + await bitcoinDepositor + .connect(governance) + .updateMaxSingleStakeAmount(maxSingleStakeAmount) + }) + + it("should return available limit", async () => { + expect(await bitcoinDepositor.maxStake()).to.be.equal( + expectedResult, + ) + }) + }) + }) + }) + }) + + describe("updateMinStakeAmount", () => { + beforeAfterSnapshotWrapper() + + describe("when caller is not governance", () => { + beforeAfterSnapshotWrapper() + + it("should revert", async () => { + await expect( + bitcoinDepositor.connect(thirdParty).updateMinStakeAmount(1234), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "OwnableUnauthorizedAccount", + ) + .withArgs(thirdParty.address) + }) + }) + + describe("when caller is governance", () => { + const testUpdateMinStakeAmount = (newValue: bigint) => + function () { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(governance) + .updateMinStakeAmount(newValue) + }) + + it("should emit MaxSingleStakeAmountUpdated event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "MinStakeAmountUpdated") + .withArgs(newValue) + }) + + it("should update value correctly", async () => { + expect(await bitcoinDepositor.minStakeAmount()).to.be.eq(newValue) + }) + } + + beforeAfterSnapshotWrapper() + + // Deposit dust threshold: 1000000 satoshi = 0.01 BTC + // tBTC Bridge stores the dust threshold in satoshi precision, + // we need to convert it to the tBTC token precision as `updateMinStakeAmount` + // function expects this precision. + const bridgeDepositDustThreshold = to1ePrecision( + defaultDepositDustThreshold, + 10, + ) + + describe("when new stake amount is less than bridge deposit dust threshold", () => { + beforeAfterSnapshotWrapper() + const newMinStakeAmount = bridgeDepositDustThreshold - 1n + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(governance) + .updateMinStakeAmount(newMinStakeAmount), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "MinStakeAmountLowerThanBridgeMinDeposit", + ) + .withArgs(newMinStakeAmount, bridgeDepositDustThreshold) + }) + }) + + describe( + "when new stake amount is equal to bridge deposit dust threshold", + testUpdateMinStakeAmount(bridgeDepositDustThreshold), + ) + + describe( + "when new stake amount is greater than bridge deposit dust threshold", + testUpdateMinStakeAmount(bridgeDepositDustThreshold + 1n), + ) + + describe( + "when new stake amount is equal max uint256", + testUpdateMinStakeAmount(MaxUint256), + ) + }) + }) + + describe("updateMaxSingleStakeAmount", () => { + beforeAfterSnapshotWrapper() + + describe("when caller is not governance", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor.connect(thirdParty).updateMaxSingleStakeAmount(1234), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "OwnableUnauthorizedAccount", + ) + .withArgs(thirdParty.address) + }) + }) + + describe("when caller is governance", () => { + const testUpdateMaxSingleStakeAmount = (newValue: bigint) => + function () { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(governance) + .updateMaxSingleStakeAmount(newValue) + }) + + it("should emit MaxSingleStakeAmountUpdated event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "MaxSingleStakeAmountUpdated") + .withArgs(newValue) + }) + + it("should update value correctly", async () => { + expect(await bitcoinDepositor.maxSingleStakeAmount()).to.be.eq( + newValue, + ) + }) + } + + describe( + "when new value is non-zero", + testUpdateMaxSingleStakeAmount(47281n), + ) + + describe("when new value is zero", testUpdateMaxSingleStakeAmount(0n)) + + describe( + "when new value is max uint256", + testUpdateMaxSingleStakeAmount(MaxUint256), + ) + }) + }) + + describe("updateMaxTotalAssetsSoftLimit", () => { + beforeAfterSnapshotWrapper() + + describe("when caller is not governance", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .updateMaxTotalAssetsSoftLimit(1234), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "OwnableUnauthorizedAccount", + ) + .withArgs(thirdParty.address) + }) + }) + + describe("when caller is governance", () => { + const testUpdateMaxTotalAssetsSoftLimit = (newValue: bigint) => + function () { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(governance) + .updateMaxTotalAssetsSoftLimit(newValue) + }) + + it("should emit MaxTotalAssetsSoftLimitUpdated event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "MaxTotalAssetsSoftLimitUpdated") + .withArgs(newValue) + }) + + it("should update value correctly", async () => { + expect(await bitcoinDepositor.maxTotalAssetsSoftLimit()).to.be.eq( + newValue, + ) + }) + } + + describe( + "when new value is non-zero", + testUpdateMaxTotalAssetsSoftLimit(47281n), + ) + + describe("when new value is zero", testUpdateMaxTotalAssetsSoftLimit(0n)) + + describe( + "when new value is max uint256", + testUpdateMaxTotalAssetsSoftLimit(MaxUint256), + ) + }) + }) + + describe("updateDepositorFeeDivisor", () => { + beforeAfterSnapshotWrapper() + + describe("when caller is not governance", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor.connect(thirdParty).updateDepositorFeeDivisor(1234), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "OwnableUnauthorizedAccount", + ) + .withArgs(thirdParty.address) + }) + }) + + describe("when caller is governance", () => { + const testUpdateDepositorFeeDivisor = (newValue: bigint) => + function () { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(governance) + .updateDepositorFeeDivisor(newValue) + }) + + it("should emit DepositorFeeDivisorUpdated event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "DepositorFeeDivisorUpdated") + .withArgs(newValue) + }) + + it("should update value correctly", async () => { + expect(await bitcoinDepositor.depositorFeeDivisor()).to.be.eq( + newValue, + ) + }) + } + + describe( + "when new value is non-zero", + testUpdateDepositorFeeDivisor(47281n), + ) + + describe("when new value is zero", testUpdateDepositorFeeDivisor(0n)) + + describe( + "when new value is max uint64", + testUpdateDepositorFeeDivisor(18446744073709551615n), + ) + }) + }) + + const extraDataValidTestData = new Map< + string, + { + staker: string + referral: number + extraData: string + } + >([ + [ + "staker has leading zeros", + { + staker: "0x000055d85E80A49B5930C4a77975d44f012D86C1", + referral: 6851, // hex: 0x1ac3 + extraData: + "0x000055d85e80a49b5930c4a77975d44f012d86c11ac300000000000000000000", + }, + ], + [ + "staker has trailing zeros", + { + staker: "0x2d2F8BC7923F7F806Dc9bb2e17F950b42CfE0000", + referral: 6851, // hex: 0x1ac3 + extraData: + "0x2d2f8bc7923f7f806dc9bb2e17f950b42cfe00001ac300000000000000000000", + }, + ], + [ + "referral is zero", + { + staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + referral: 0, + extraData: + "0xeb098d6cde6a202981316b24b19e64d82721e89e000000000000000000000000", + }, + ], + [ + "referral has leading zeros", + { + staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + referral: 31, // hex: 0x001f + extraData: + "0xeb098d6cde6a202981316b24b19e64d82721e89e001f00000000000000000000", + }, + ], + [ + "referral has trailing zeros", + { + staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + referral: 19712, // hex: 0x4d00 + extraData: + "0xeb098d6cde6a202981316b24b19e64d82721e89e4d0000000000000000000000", + }, + ], + [ + "referral is maximum value", + { + staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + referral: 65535, // max uint16 + extraData: + "0xeb098d6cde6a202981316b24b19e64d82721e89effff00000000000000000000", + }, + ], + ]) + + describe("encodeExtraData", () => { + extraDataValidTestData.forEach( + // eslint-disable-next-line @typescript-eslint/no-shadow + ({ staker, referral, extraData: expectedExtraData }, testName) => { + it(testName, async () => { + expect( + await bitcoinDepositor.encodeExtraData(staker, referral), + ).to.be.equal(expectedExtraData) + }) + }, + ) + }) + + describe("decodeExtraData", () => { + extraDataValidTestData.forEach( + ( + { staker: expectedStaker, referral: expectedReferral, extraData }, + testName, + ) => { + it(testName, async () => { + const [actualStaker, actualReferral] = + await bitcoinDepositor.decodeExtraData(extraData) + + expect(actualStaker, "invalid staker").to.be.equal(expectedStaker) + expect(actualReferral, "invalid referral").to.be.equal( + expectedReferral, + ) + }) + }, + ) + + it("with unused bytes filled with data", async () => { + // Extra data uses address (20 bytes) and referral (2 bytes), leaving the + // remaining 10 bytes unused. This test fills the unused bytes with a random + // value. + const extraData = + "0xeb098d6cde6a202981316b24b19e64d82721e89e1ac3105f9919321ea7d75f58" + const expectedStaker = "0xeb098d6cDE6A202981316b24B19e64D82721e89E" + const expectedReferral = 6851 // hex: 0x1ac3 + + const [actualStaker, actualReferral] = + await bitcoinDepositor.decodeExtraData(extraData) + + expect(actualStaker, "invalid staker").to.be.equal(expectedStaker) + expect(actualReferral, "invalid referral").to.be.equal(expectedReferral) + }) + }) + + async function initializeStake() { + await bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + tbtcDepositData.referral, + ) + } + + async function finalizeMinting(depositKey: bigint, amountToMint?: bigint) { + await tbtcVault.createOptimisticMintingRequest(depositKey) + + // Simulate deposit request finalization via optimistic minting. + if (amountToMint) { + await tbtcVault.finalizeOptimisticMintingRequestWithAmount( + depositKey, + amountToMint, + ) + } else { + await tbtcVault.finalizeOptimisticMintingRequest(depositKey) + } + } +}) diff --git a/core/test/Deployment.test.ts b/core/test/Deployment.test.ts index 4c0d9233b..5914e98bb 100644 --- a/core/test/Deployment.test.ts +++ b/core/test/Deployment.test.ts @@ -1,16 +1,18 @@ import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" import { expect } from "chai" import { MaxUint256 } from "ethers" +import { helpers } from "hardhat" import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { deployment } from "./helpers/context" -import { getNamedSigner } from "./helpers/signer" import type { StBTC as stBTC, Dispatcher, TestERC20 } from "../typechain" +const { getNamedSigners } = helpers.signers + async function fixture() { const { tbtc, stbtc, dispatcher } = await deployment() - const { governance, maintainer, treasury } = await getNamedSigner() + const { governance, maintainer, treasury } = await getNamedSigners() return { stbtc, dispatcher, tbtc, governance, maintainer, treasury } } diff --git a/core/test/Dispatcher.test.ts b/core/test/Dispatcher.test.ts index 8328ffbef..4e1e8a987 100644 --- a/core/test/Dispatcher.test.ts +++ b/core/test/Dispatcher.test.ts @@ -1,4 +1,4 @@ -import { ethers } from "hardhat" +import { ethers, helpers } from "hardhat" import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { expect } from "chai" import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" @@ -8,8 +8,6 @@ import { beforeAfterEachSnapshotWrapper, beforeAfterSnapshotWrapper, deployment, - getNamedSigner, - getUnnamedSigner, } from "./helpers" import { @@ -21,10 +19,12 @@ import { import { to1e18 } from "./utils" +const { getNamedSigners, getUnnamedSigners } = helpers.signers + async function fixture() { const { tbtc, stbtc, dispatcher, vault } = await deployment() - const { governance, maintainer } = await getNamedSigner() - const [thirdParty] = await getUnnamedSigner() + const { governance, maintainer } = await getNamedSigners() + const [thirdParty] = await getUnnamedSigners() return { dispatcher, governance, thirdParty, maintainer, vault, tbtc, stbtc } } diff --git a/core/test/data/tbtc.ts b/core/test/data/tbtc.ts new file mode 100644 index 000000000..fd491081f --- /dev/null +++ b/core/test/data/tbtc.ts @@ -0,0 +1,49 @@ +/* eslint-disable import/prefer-default-export */ + +import { ethers } from "hardhat" + +// TODO: Revisit the data once full integration is tested on testnet with valid +// contracts integration. +// Fixture used for revealDepositWithExtraData test scenario. +// source: https://github.com/keep-network/tbtc-v2/blob/103411a595c33895ff6bff8457383a69eca4963c/solidity/test/bridge/Bridge.Deposit.test.ts#L132 +export const tbtcDepositData = { + // Data of a proper P2SH deposit funding transaction embedding some + // extra data. Little-endian hash is: + // 0x6383cd1829260b6034cd12bad36171748e8c3c6a8d57fcb6463c62f96116dfbc. + fundingTxInfo: { + version: "0x01000000", + inputVector: + "0x018348cdeb551134fe1f19d378a8adec9b146671cb67b945b71bf56b20d" + + "c2b952f0100000000ffffffff", + outputVector: + "0x02102700000000000017a9149fe6615a307aa1d7eee668c1227802b2fbc" + + "aa919877ed73b00000000001600147ac2d9378a1c47e589dfb8095ca95ed2" + + "140d2726", + locktime: "0x00000000", + }, + fundingTxHash: + "0x6383cd1829260b6034cd12bad36171748e8c3c6a8d57fcb6463c62f96116dfbc", + // Data matching the redeem script locking the funding output of + // P2SHFundingTx and P2WSHFundingTx. + depositorAddress: "0x934B98637cA318a4D6E7CA6ffd1690b8e77df637", + reveal: { + fundingOutputIndex: 0, + blindingFactor: "0xf9f0c90d00039523", + // HASH160 of 03989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d9. + walletPubKeyHash: "0x8db50eb52063ea9d98b3eac91489a90f738986f6", + // HASH160 of 0300d6f28a2f6bf9836f57fcda5d284c9a8f849316119779f0d6090830d97763a9. + refundPubKeyHash: "0x28e081f285138ccbe389c1eb8985716230129f89", + refundLocktime: "0x60bcea61", + vault: "0x594cfd89700040163727828AE20B52099C58F02C", + }, + // 20-bytes of extraData + staker: "0xa9B38eA6435c8941d6eDa6a46b68E3e211719699", + // 2-bytes of extraData + referral: "0x5bd1", + extraData: + "0xa9b38ea6435c8941d6eda6a46b68e3e2117196995bd100000000000000000000", + // Deposit key is keccak256(fundingTxHash | fundingOutputIndex). + depositKey: ethers.getBigInt( + "0x8dde6118338ae2a046eb77a4acceb0521699275f9cc8e9b50057b29d9de1e844", + ), +} diff --git a/core/test/helpers/context.ts b/core/test/helpers/context.ts index 81cece769..239721235 100644 --- a/core/test/helpers/context.ts +++ b/core/test/helpers/context.ts @@ -5,17 +5,35 @@ import type { StBTC as stBTC, Dispatcher, TestERC20, + BridgeStub, TestERC4626, + TBTCVaultStub, + AcreBitcoinDepositorHarness, } from "../../typechain" // eslint-disable-next-line import/prefer-default-export export async function deployment() { await deployments.fixture() - const tbtc: TestERC20 = await getDeployedContract("TBTC") const stbtc: stBTC = await getDeployedContract("stBTC") + const bitcoinDepositor: AcreBitcoinDepositorHarness = + await getDeployedContract("AcreBitcoinDepositor") + + const tbtc: TestERC20 = await getDeployedContract("TBTC") + const tbtcBridge: BridgeStub = await getDeployedContract("Bridge") + const tbtcVault: TBTCVaultStub = await getDeployedContract("TBTCVault") + const dispatcher: Dispatcher = await getDeployedContract("Dispatcher") + const vault: TestERC4626 = await getDeployedContract("Vault") - return { tbtc, stbtc, dispatcher, vault } + return { + tbtc, + stbtc, + bitcoinDepositor, + tbtcBridge, + tbtcVault, + dispatcher, + vault, + } } diff --git a/core/test/helpers/contract.ts b/core/test/helpers/contract.ts index 6ba7b36ae..9233529d4 100644 --- a/core/test/helpers/contract.ts +++ b/core/test/helpers/contract.ts @@ -1,7 +1,8 @@ -import { deployments, ethers } from "hardhat" +import { deployments, ethers, helpers } from "hardhat" import type { BaseContract } from "ethers" -import { getUnnamedSigner } from "./signer" + +const { getUnnamedSigners } = helpers.signers /** * Get instance of a contract from Hardhat Deployments. @@ -15,7 +16,7 @@ export async function getDeployedContract( const { address, abi } = await deployments.get(deploymentName) // Use default unnamed signer from index 0 to initialize the contract runner. - const [defaultSigner] = await getUnnamedSigner() + const [defaultSigner] = await getUnnamedSigners() return new ethers.BaseContract(address, abi, defaultSigner) as T } diff --git a/core/test/helpers/index.ts b/core/test/helpers/index.ts index e4df2196a..40da2ef89 100644 --- a/core/test/helpers/index.ts +++ b/core/test/helpers/index.ts @@ -1,4 +1,3 @@ export * from "./context" export * from "./contract" -export * from "./signer" export * from "./snapshot" diff --git a/core/test/helpers/signer.ts b/core/test/helpers/signer.ts deleted file mode 100644 index 0ae57f35e..000000000 --- a/core/test/helpers/signer.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ethers, getNamedAccounts, getUnnamedAccounts } from "hardhat" - -import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" - -/** - * Get named Hardhat Ethers Signers. - * @returns Map of named Hardhat Ethers Signers. - */ -export async function getNamedSigner(): Promise<{ - [name: string]: HardhatEthersSigner -}> { - const namedSigners: { [name: string]: HardhatEthersSigner } = {} - - await Promise.all( - Object.entries(await getNamedAccounts()).map(async ([name, address]) => { - namedSigners[name] = await ethers.getSigner(address) - }), - ) - - return namedSigners -} - -/** - * Get unnamed Hardhat Ethers Signers. - * @returns Array of unnamed Hardhat Ethers Signers. - */ -export async function getUnnamedSigner(): Promise { - const accounts = await getUnnamedAccounts() - - return Promise.all(accounts.map(ethers.getSigner)) -} diff --git a/core/test/stBTC.test.ts b/core/test/stBTC.test.ts index 3bde615b6..6b6d33261 100644 --- a/core/test/stBTC.test.ts +++ b/core/test/stBTC.test.ts @@ -4,26 +4,23 @@ import { } from "@nomicfoundation/hardhat-toolbox/network-helpers" import { expect } from "chai" import { ContractTransactionResponse, MaxUint256, ZeroAddress } from "ethers" -import { ethers } from "hardhat" +import { ethers, helpers } from "hardhat" import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import type { SnapshotRestorer } from "@nomicfoundation/hardhat-toolbox/network-helpers" -import { - beforeAfterSnapshotWrapper, - deployment, - getNamedSigner, - getUnnamedSigner, -} from "./helpers" +import { beforeAfterSnapshotWrapper, deployment } from "./helpers" import { to1e18 } from "./utils" import type { StBTC as stBTC, TestERC20, Dispatcher } from "../typechain" +const { getNamedSigners, getUnnamedSigners } = helpers.signers + async function fixture() { const { tbtc, stbtc, dispatcher } = await deployment() - const { governance, treasury } = await getNamedSigner() + const { governance, treasury } = await getNamedSigners() - const [depositor1, depositor2, thirdParty] = await getUnnamedSigner() + const [depositor1, depositor2, thirdParty] = await getUnnamedSigners() const amountToMint = to1e18(100000) await tbtc.mint(depositor1, amountToMint) diff --git a/core/tsconfig.json b/core/tsconfig.json index f9167f325..0350f4062 100644 --- a/core/tsconfig.json +++ b/core/tsconfig.json @@ -9,5 +9,5 @@ "resolveJsonModule": true }, "files": ["./hardhat.config.ts"], - "include": ["./deploy", "./test", "./typechain", "./helpers"] + "include": ["./deploy", "./test", "./typechain", "./helpers", "./types"] } diff --git a/core/types/index.ts b/core/types/index.ts new file mode 100644 index 000000000..8365a2fc1 --- /dev/null +++ b/core/types/index.ts @@ -0,0 +1,9 @@ +/* eslint-disable import/prefer-default-export */ +export enum StakeRequestState { + Unknown, + Initialized, + Finalized, + Queued, + FinalizedFromQueue, + CancelledFromQueue, +} diff --git a/dapp/.eslintrc b/dapp/.eslintrc index 950c59770..3fd1b6e42 100644 --- a/dapp/.eslintrc +++ b/dapp/.eslintrc @@ -18,6 +18,16 @@ "react/require-default-props": [0], "no-console": ["error", { "allow": ["error"] }], }, + "overrides": [ + { + "files": [ + "src/store/**/*Slice.ts" + ], + "rules": { + "no-param-reassign": ["error", {"ignorePropertyModificationsFor": ["state"] }] + } + } + ], "settings": { "import/resolver": { "alias": { diff --git a/dapp/package.json b/dapp/package.json index 429603c06..c8cdf2427 100644 --- a/dapp/package.json +++ b/dapp/package.json @@ -21,6 +21,7 @@ "@emotion/styled": "^11.11.0", "@ledgerhq/wallet-api-client": "^1.5.0", "@ledgerhq/wallet-api-client-react": "^1.3.0", + "@reduxjs/toolkit": "^2.2.0", "@sentry/react": "^7.98.0", "@sentry/types": "^7.102.0", "@tanstack/react-table": "^8.11.3", @@ -32,6 +33,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-number-format": "^5.3.1", + "react-redux": "^9.1.0", "react-router-dom": "^6.22.0", "react-slick": "^0.30.2", "recharts": "^2.12.0" diff --git a/dapp/public/_redirects b/dapp/public/_redirects new file mode 100644 index 000000000..7797f7c6a --- /dev/null +++ b/dapp/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 diff --git a/dapp/src/DApp.tsx b/dapp/src/DApp.tsx index bece62291..71db1bc16 100644 --- a/dapp/src/DApp.tsx +++ b/dapp/src/DApp.tsx @@ -1,7 +1,9 @@ import React from "react" import { Box, ChakraProvider } from "@chakra-ui/react" +import { Provider as ReduxProvider } from "react-redux" import { RouterProvider } from "react-router-dom" -import { useInitializeAcreSdk } from "./hooks" +import { useInitApp } from "./hooks" +import { store } from "./store" import theme from "./theme" import { DocsDrawerContextProvider, @@ -15,13 +17,9 @@ import Sidebar from "./components/Sidebar" import DocsDrawer from "./components/DocsDrawer" import GlobalStyles from "./components/GlobalStyles" import { router } from "./router" -import { useSentry } from "./hooks/sentry" function DApp() { - // TODO: Let's uncomment when dark mode is ready - // useDetectThemeMode() - useSentry() - useInitializeAcreSdk() + useInitApp() return ( <> @@ -42,10 +40,12 @@ function DAppProviders() { - - - - + + + + + + diff --git a/dapp/src/components/Header/ConnectWallet.tsx b/dapp/src/components/Header/ConnectWallet.tsx index 038a069ff..7879363bf 100644 --- a/dapp/src/components/Header/ConnectWallet.tsx +++ b/dapp/src/components/Header/ConnectWallet.tsx @@ -9,7 +9,7 @@ import { import { CurrencyBalance } from "#/components/shared/CurrencyBalance" import { TextMd } from "#/components/shared/Typography" import { Bitcoin, EthereumIcon } from "#/assets/icons" -import { truncateAddress, asyncWrapper } from "#/utils" +import { truncateAddress, logPromiseFailure } from "#/utils" export type ConnectButtonsProps = { leftIcon: typeof Icon @@ -25,7 +25,7 @@ function ConnectButton({ const colorScheme = !account ? "error" : undefined const handleClick = () => { - asyncWrapper(requestAccount()) + logPromiseFailure(requestAccount()) } return ( diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx index 935bc3ed5..1701afbe5 100644 --- a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx @@ -10,7 +10,7 @@ import { } from "#/hooks" import Alert from "#/components/shared/Alert" import { TextMd } from "#/components/shared/Typography" -import { asyncWrapper } from "#/utils" +import { logPromiseFailure } from "#/utils" import { PROCESS_STATUSES } from "#/types" import StakingStepsModalContent from "./StakingStepsModalContent" @@ -42,7 +42,7 @@ export default function DepositBTCModal() { // to make sure for the moment that it doesn't return an error about funds not found // TODO: Remove the delay when SDK is updated setTimeout(() => { - asyncWrapper(handleStake()) + logPromiseFailure(handleStake()) }, 10000) }, [setStatus, handleStake]) @@ -62,7 +62,7 @@ export default function DepositBTCModal() { // TODO: Display the correct message for the user if (response.verificationStatus !== "valid") return - asyncWrapper(sendBitcoinTransaction(tokenAmount?.amount, btcAddress)) + logPromiseFailure(sendBitcoinTransaction(tokenAmount?.amount, btcAddress)) }, [ btcAddress, depositReceipt, @@ -73,7 +73,7 @@ export default function DepositBTCModal() { ]) const handledDepositBTCWrapper = useCallback(() => { - asyncWrapper(handledDepositBTC()) + logPromiseFailure(handledDepositBTC()) }, [handledDepositBTC]) return ( diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/SignMessageModal.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/SignMessageModal.tsx index ac6fb44a9..84803da08 100644 --- a/dapp/src/components/TransactionModal/ActiveStakingStep/SignMessageModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/SignMessageModal.tsx @@ -4,7 +4,7 @@ import { useModalFlowContext, useStakeFlowContext, } from "#/hooks" -import { asyncWrapper } from "#/utils" +import { logPromiseFailure } from "#/utils" import AlertReceiveSTBTC from "#/components/shared/AlertReceiveSTBTC" import { PROCESS_STATUSES } from "#/types" import StakingStepsModalContent from "./StakingStepsModalContent" @@ -15,7 +15,7 @@ export default function SignMessageModal() { const handleSignMessage = useExecuteFunction(signMessage, goNext) const handleSignMessageWrapper = useCallback(() => { - asyncWrapper(handleSignMessage()) + logPromiseFailure(handleSignMessage()) }, [handleSignMessage]) useEffect(() => { diff --git a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx index dcf681c60..0f9bd1003 100644 --- a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx @@ -4,7 +4,7 @@ import { PROCESS_STATUSES } from "#/types" import { Button, ModalBody, ModalFooter, ModalHeader } from "@chakra-ui/react" import { TextMd } from "#/components/shared/Typography" import AlertReceiveSTBTC from "#/components/shared/AlertReceiveSTBTC" -import { asyncWrapper } from "#/utils" +import { logPromiseFailure } from "#/utils" export default function SignMessageModal() { const { setStatus } = useModalFlowContext() @@ -34,7 +34,7 @@ export default function SignMessageModal() { // TODO: Remove when SDK is ready setTimeout(() => { - asyncWrapper(handleSignMessage()) + logPromiseFailure(handleSignMessage()) }, 5000) }, [setStatus, handleSignMessage]) diff --git a/dapp/src/components/TransactionModal/ModalContentWrapper/ActionFormModal.tsx b/dapp/src/components/TransactionModal/ModalContentWrapper/ActionFormModal.tsx index 8a727d892..f900f85e2 100644 --- a/dapp/src/components/TransactionModal/ModalContentWrapper/ActionFormModal.tsx +++ b/dapp/src/components/TransactionModal/ModalContentWrapper/ActionFormModal.tsx @@ -16,7 +16,7 @@ import { } from "#/hooks" import { ACTION_FLOW_TYPES, ActionFlowType } from "#/types" import { TokenAmountFormValues } from "#/components/shared/TokenAmountForm/TokenAmountFormBase" -import { asyncWrapper } from "#/utils" +import { logPromiseFailure } from "#/utils" import StakeFormModal from "../ActiveStakingStep/StakeFormModal" import UnstakeFormModal from "../ActiveUnstakingStep/UnstakeFormModal" @@ -59,7 +59,8 @@ function ActionFormModal({ defaultType }: { defaultType: ActionFlowType }) { ) const handleSubmitFormWrapper = useCallback( - (values: TokenAmountFormValues) => asyncWrapper(handleSubmitForm(values)), + (values: TokenAmountFormValues) => + logPromiseFailure(handleSubmitForm(values)), [handleSubmitForm], ) diff --git a/dapp/src/components/TransactionModal/ModalContentWrapper/MissingAccountModal.tsx b/dapp/src/components/TransactionModal/ModalContentWrapper/MissingAccountModal.tsx index 21391e063..826dc325f 100644 --- a/dapp/src/components/TransactionModal/ModalContentWrapper/MissingAccountModal.tsx +++ b/dapp/src/components/TransactionModal/ModalContentWrapper/MissingAccountModal.tsx @@ -10,7 +10,7 @@ import { } from "@chakra-ui/react" import { TextMd } from "#/components/shared/Typography" import Alert from "#/components/shared/Alert" -import { asyncWrapper, getCurrencyByType } from "#/utils" +import { logPromiseFailure, getCurrencyByType } from "#/utils" import { CurrencyType, RequestAccountParams } from "#/types" type MissingAccountModalProps = { @@ -27,7 +27,7 @@ export default function MissingAccountModal({ const { name, symbol } = getCurrencyByType(currency) const handleClick = () => { - asyncWrapper(requestAccount()) + logPromiseFailure(requestAccount()) } return ( diff --git a/dapp/src/components/shared/CurrencyBalanceWithConversion/index.tsx b/dapp/src/components/shared/CurrencyBalanceWithConversion/index.tsx index b54cff5cb..5d19dd126 100644 --- a/dapp/src/components/shared/CurrencyBalanceWithConversion/index.tsx +++ b/dapp/src/components/shared/CurrencyBalanceWithConversion/index.tsx @@ -1,8 +1,7 @@ -import React, { useMemo } from "react" +import React from "react" +import { useCurrencyConversion } from "#/hooks" import { CurrencyBalance, CurrencyBalanceProps } from "../CurrencyBalance" -const MOCK_CONVERSION_AMOUNT = 100 - export function CurrencyBalanceWithConversion({ from, to, @@ -10,12 +9,7 @@ export function CurrencyBalanceWithConversion({ from: CurrencyBalanceProps to: CurrencyBalanceProps }) { - // TODO: Make the correct conversion - const conversionAmount = useMemo(() => { - if (!from.amount || BigInt(from.amount) < 0n) return undefined - - return MOCK_CONVERSION_AMOUNT - }, [from.amount]) + const conversionAmount = useCurrencyConversion({ from, to }) return ( <> diff --git a/dapp/src/components/shared/Form/FormTokenBalanceInput.tsx b/dapp/src/components/shared/Form/FormTokenBalanceInput.tsx index 5eefb5933..00fe36074 100644 --- a/dapp/src/components/shared/Form/FormTokenBalanceInput.tsx +++ b/dapp/src/components/shared/Form/FormTokenBalanceInput.tsx @@ -1,6 +1,6 @@ import React from "react" import { useField } from "formik" -import { asyncWrapper } from "#/utils" +import { logPromiseFailure } from "#/utils" import TokenBalanceInput, { TokenBalanceInputProps } from "../TokenBalanceInput" export type FormTokenBalanceInputProps = { @@ -13,7 +13,7 @@ export function FormTokenBalanceInput({ const [field, meta, helpers] = useField(name) const setAmount = (value?: bigint) => { - asyncWrapper(helpers.setValue(value)) + logPromiseFailure(helpers.setValue(value)) } return ( diff --git a/dapp/src/contexts/StakeFlowContext.tsx b/dapp/src/contexts/StakeFlowContext.tsx index 5fd80cbe5..8afe6870b 100644 --- a/dapp/src/contexts/StakeFlowContext.tsx +++ b/dapp/src/contexts/StakeFlowContext.tsx @@ -27,6 +27,7 @@ export function StakeFlowProvider({ children }: { children: React.ReactNode }) { initStake: acreInitStake, signMessage, btcAddress, + depositReceipt, stake, } = useStakeFlow() @@ -51,9 +52,10 @@ export function StakeFlowProvider({ children }: { children: React.ReactNode }) { initStake, signMessage, btcAddress, + depositReceipt, stake, }), - [initStake, signMessage, btcAddress, stake], + [initStake, signMessage, btcAddress, depositReceipt, stake], ) return ( diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index 56014a221..292568f86 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -1,3 +1,4 @@ +export * from "./store" export * from "./useDetectThemeMode" export * from "./useRequestBitcoinAccount" export * from "./useRequestEthereumAccount" @@ -12,4 +13,7 @@ export * from "./useInitializeAcreSdk" export * from "./useTransactionHistoryTable" export * from "./useExecuteFunction" export * from "./useStakeFlowContext" +export * from "./useInitApp" +export * from "./useCurrencyConversion" export * from "./useDepositTelemetry" +export * from "./useFetchBTCPriceUSD" diff --git a/dapp/src/hooks/store/index.ts b/dapp/src/hooks/store/index.ts new file mode 100644 index 000000000..20f33eb3c --- /dev/null +++ b/dapp/src/hooks/store/index.ts @@ -0,0 +1,2 @@ +export * from "./useAppDispatch" +export * from "./useAppSelector" diff --git a/dapp/src/hooks/store/useAppDispatch.ts b/dapp/src/hooks/store/useAppDispatch.ts new file mode 100644 index 000000000..ef697bd15 --- /dev/null +++ b/dapp/src/hooks/store/useAppDispatch.ts @@ -0,0 +1,4 @@ +import { useDispatch } from "react-redux" +import { AppDispatch } from "#/store" + +export const useAppDispatch = useDispatch.withTypes() diff --git a/dapp/src/hooks/store/useAppSelector.ts b/dapp/src/hooks/store/useAppSelector.ts new file mode 100644 index 000000000..9cbdbbe9f --- /dev/null +++ b/dapp/src/hooks/store/useAppSelector.ts @@ -0,0 +1,4 @@ +import { useSelector } from "react-redux" +import { RootState } from "#/store" + +export const useAppSelector = useSelector.withTypes() diff --git a/dapp/src/hooks/useCurrencyConversion.ts b/dapp/src/hooks/useCurrencyConversion.ts new file mode 100644 index 000000000..f3279c86e --- /dev/null +++ b/dapp/src/hooks/useCurrencyConversion.ts @@ -0,0 +1,39 @@ +import { CURRENCIES_BY_TYPE } from "#/constants" +import { selectBtcUsdPrice } from "#/store/btc" +import { bigIntToUserAmount } from "#/utils" +import { CurrencyType } from "#/types" +import { useMemo } from "react" +import { useAppSelector } from "./store" + +type CurrencyConversionType = { + currency: CurrencyType + amount?: number | string +} + +// TODO: should be updated to handle another currencies +const isBtcUsdConversion = ( + from: CurrencyConversionType, + to: CurrencyConversionType, +): boolean => from.currency === "bitcoin" && to.currency === "usd" + +export function useCurrencyConversion({ + from, + to, +}: { + from: CurrencyConversionType + to: CurrencyConversionType +}) { + const price = useAppSelector( + isBtcUsdConversion(from, to) ? selectBtcUsdPrice : () => undefined, + ) + const conversionAmount = useMemo(() => { + if (!from.amount || !price) return undefined + + const { amount, currency } = from + const { decimals, desiredDecimals } = CURRENCIES_BY_TYPE[currency] + + return bigIntToUserAmount(BigInt(amount), decimals, desiredDecimals) * price + }, [from, price]) + + return conversionAmount +} diff --git a/dapp/src/hooks/useFetchBTCPriceUSD.ts b/dapp/src/hooks/useFetchBTCPriceUSD.ts new file mode 100644 index 000000000..0bce617ca --- /dev/null +++ b/dapp/src/hooks/useFetchBTCPriceUSD.ts @@ -0,0 +1,12 @@ +import { useEffect } from "react" +import { fetchBTCPriceUSD } from "#/store/btc" +import { logPromiseFailure } from "#/utils" +import { useAppDispatch } from "./store" + +export function useFetchBTCPriceUSD() { + const dispatch = useAppDispatch() + + useEffect(() => { + logPromiseFailure(dispatch(fetchBTCPriceUSD())) + }, [dispatch]) +} diff --git a/dapp/src/hooks/useInitApp.ts b/dapp/src/hooks/useInitApp.ts new file mode 100644 index 000000000..bfa07283e --- /dev/null +++ b/dapp/src/hooks/useInitApp.ts @@ -0,0 +1,11 @@ +import { useSentry } from "./sentry" +import { useInitializeAcreSdk } from "./useInitializeAcreSdk" +import { useFetchBTCPriceUSD } from "./useFetchBTCPriceUSD" + +export function useInitApp() { + // TODO: Let's uncomment when dark mode is ready + // useDetectThemeMode() + useSentry() + useInitializeAcreSdk() + useFetchBTCPriceUSD() +} diff --git a/dapp/src/hooks/useInitializeAcreSdk.ts b/dapp/src/hooks/useInitializeAcreSdk.ts index eb1afce46..ab05a9aac 100644 --- a/dapp/src/hooks/useInitializeAcreSdk.ts +++ b/dapp/src/hooks/useInitializeAcreSdk.ts @@ -1,6 +1,6 @@ import { useEffect } from "react" import { ETHEREUM_NETWORK } from "#/constants" -import { asyncWrapper } from "#/utils" +import { logPromiseFailure } from "#/utils" import { useAcreContext } from "#/acre-react/hooks" import { useWalletContext } from "./useWalletContext" @@ -14,6 +14,6 @@ export function useInitializeAcreSdk() { const initSDK = async (ethAddress: string) => { await init(ethAddress, ETHEREUM_NETWORK) } - asyncWrapper(initSDK(ethAccount.address)) + logPromiseFailure(initSDK(ethAccount.address)) }, [ethAccount?.address, init]) } diff --git a/dapp/src/store/btc/btcSelector.ts b/dapp/src/store/btc/btcSelector.ts new file mode 100644 index 000000000..c8ae200ab --- /dev/null +++ b/dapp/src/store/btc/btcSelector.ts @@ -0,0 +1,3 @@ +import { RootState } from ".." + +export const selectBtcUsdPrice = (state: RootState) => state.btc.usdPrice diff --git a/dapp/src/store/btc/btcSlice.ts b/dapp/src/store/btc/btcSlice.ts new file mode 100644 index 000000000..84468710d --- /dev/null +++ b/dapp/src/store/btc/btcSlice.ts @@ -0,0 +1,34 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit" +import { fetchBTCPriceUSD } from "./btcThunk" + +type BtcState = { + isLoadingPriceUSD: boolean + usdPrice: number +} + +const initialState: BtcState = { + isLoadingPriceUSD: false, + usdPrice: 0, +} + +// Store Bitcoin data such as balance, balance in usd and other related data to Bitcoin chain. +export const btcSlice = createSlice({ + name: "btc", + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(fetchBTCPriceUSD.pending, (state) => { + state.isLoadingPriceUSD = true + }) + builder.addCase(fetchBTCPriceUSD.rejected, (state) => { + state.isLoadingPriceUSD = false + }) + builder.addCase( + fetchBTCPriceUSD.fulfilled, + (state, action: PayloadAction) => { + state.isLoadingPriceUSD = false + state.usdPrice = action.payload + }, + ) + }, +}) diff --git a/dapp/src/store/btc/btcThunk.ts b/dapp/src/store/btc/btcThunk.ts new file mode 100644 index 000000000..749c21fb8 --- /dev/null +++ b/dapp/src/store/btc/btcThunk.ts @@ -0,0 +1,15 @@ +import { createAsyncThunk } from "@reduxjs/toolkit" +import { fetchCryptoCurrencyPriceUSD } from "#/utils/exchangeApi" + +export const fetchBTCPriceUSD = createAsyncThunk( + "btc/fetchBTCPriceUSD", + async () => { + try { + const priceUSD = await fetchCryptoCurrencyPriceUSD("bitcoin") + return priceUSD + } catch (error) { + console.error(error) + return 0 + } + }, +) diff --git a/dapp/src/store/btc/index.ts b/dapp/src/store/btc/index.ts new file mode 100644 index 000000000..d0853dfb6 --- /dev/null +++ b/dapp/src/store/btc/index.ts @@ -0,0 +1,3 @@ +export * from "./btcSelector" +export * from "./btcSlice" +export * from "./btcThunk" diff --git a/dapp/src/store/index.ts b/dapp/src/store/index.ts new file mode 100644 index 000000000..fc68092ae --- /dev/null +++ b/dapp/src/store/index.ts @@ -0,0 +1,12 @@ +import { configureStore } from "@reduxjs/toolkit" +import { middleware } from "./middleware" +import { reducer } from "./reducer" + +export const store = configureStore({ + reducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware(middleware), + devTools: !import.meta.env.PROD, +}) + +export type RootState = ReturnType +export type AppDispatch = typeof store.dispatch diff --git a/dapp/src/store/middleware.ts b/dapp/src/store/middleware.ts new file mode 100644 index 000000000..451fd90f1 --- /dev/null +++ b/dapp/src/store/middleware.ts @@ -0,0 +1 @@ +export const middleware = {} diff --git a/dapp/src/store/reducer.ts b/dapp/src/store/reducer.ts new file mode 100644 index 000000000..a49f551e8 --- /dev/null +++ b/dapp/src/store/reducer.ts @@ -0,0 +1,6 @@ +import { combineReducers } from "@reduxjs/toolkit" +import { btcSlice } from "./btc/btcSlice" + +export const reducer = combineReducers({ + btc: btcSlice.reducer, +}) diff --git a/dapp/src/types/coingecko.ts b/dapp/src/types/coingecko.ts new file mode 100644 index 000000000..5d43c6fdc --- /dev/null +++ b/dapp/src/types/coingecko.ts @@ -0,0 +1,2 @@ +export type CoingeckoIdType = "bitcoin" +export type CoingeckoCurrencyType = "usd" diff --git a/dapp/src/types/index.ts b/dapp/src/types/index.ts index 6167106d2..12a5dc7b3 100644 --- a/dapp/src/types/index.ts +++ b/dapp/src/types/index.ts @@ -9,3 +9,4 @@ export * from "./action-flow" export * from "./location" export * from "./charts" export * from "./activity" +export * from "./coingecko" diff --git a/dapp/src/utils/async.ts b/dapp/src/utils/async.ts deleted file mode 100644 index df3657065..000000000 --- a/dapp/src/utils/async.ts +++ /dev/null @@ -1,8 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function asyncWrapper(func: Promise) { - return () => { - func.catch((error) => { - throw error - }) - } -} diff --git a/dapp/src/utils/exchangeApi.ts b/dapp/src/utils/exchangeApi.ts new file mode 100644 index 000000000..2f83c4ebf --- /dev/null +++ b/dapp/src/utils/exchangeApi.ts @@ -0,0 +1,20 @@ +import axios from "axios" +import { CoingeckoIdType, CoingeckoCurrencyType } from "#/types" + +const coingeckoApiURL = "https://api.coingecko.com/api/v3" + +type CoingeckoSimplePriceResponse = { + data: { + [id in CoingeckoIdType]: Record + } +} + +export const fetchCryptoCurrencyPriceUSD = async ( + coingeckoId: CoingeckoIdType, +): Promise => { + const response: CoingeckoSimplePriceResponse = await axios.get( + `${coingeckoApiURL}/simple/price?ids=${coingeckoId}&vs_currencies=usd`, + ) + + return response.data[coingeckoId].usd +} diff --git a/dapp/src/utils/index.ts b/dapp/src/utils/index.ts index 865b6211e..cb7034c47 100644 --- a/dapp/src/utils/index.ts +++ b/dapp/src/utils/index.ts @@ -5,5 +5,6 @@ export * from "./currency" export * from "./chain" export * from "./text" export * from "./time" -export * from "./async" +export * from "./promise" +export * from "./exchangeApi" export * from "./verifyDepositAddress" diff --git a/dapp/src/utils/promise.ts b/dapp/src/utils/promise.ts new file mode 100644 index 000000000..22f13c622 --- /dev/null +++ b/dapp/src/utils/promise.ts @@ -0,0 +1,12 @@ +/** + * If the promise fails, log the underlying error but maintain the failed + * promise. + * + * Does nothing to successful promises. + */ +export function logPromiseFailure(promise: Promise) { + promise.catch((error) => { + console.error(error) + throw error + }) +} diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 000000000..59e82dbf2 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,7 @@ +[build] + # Don't run builds after the changes touching only the listed paths. + ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF ':(exclude).github/' ':(exclude).vscode/' ':(exclude)core/' ':(exclude).git-blame-ignore-revs' ':(exclude).gitignore' ':(exclude).npmrc' ':(exclude).nvmrc' ':(exclude).pre-commit-config.yaml' ':(exclude).prettierignore' ':(exclude).prettierrc.js' ':(exclude).syncpackrc' ':(exclude)LICENSE' ':(exclude)README.md'" + +[context.production] + # Do not run builds for the production context. + ignore = "exit 0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df065c1f5..bd775673b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: version: 3.4.0-solc-0.8 '@keep-network/tbtc-v2': specifier: development - version: 1.6.0-dev.18(@keep-network/keep-core@1.8.1-dev.0) + version: 1.6.0-dev.24(@keep-network/keep-core@1.8.1-dev.0) '@openzeppelin/contracts': specifier: ^5.0.0 version: 5.0.0 @@ -141,6 +141,9 @@ importers: '@ledgerhq/wallet-api-client-react': specifier: ^1.3.0 version: 1.3.0(react@18.2.0) + '@reduxjs/toolkit': + specifier: ^2.2.0 + version: 2.2.1(react-redux@9.1.0)(react@18.2.0) '@sentry/react': specifier: ^7.98.0 version: 7.98.0(react@18.2.0) @@ -155,7 +158,7 @@ importers: version: 0.23.13 axios: specifier: ^1.6.7 - version: 1.6.7 + version: 1.6.7(debug@4.3.4) ethers: specifier: ^6.10.0 version: 6.10.0 @@ -174,15 +177,18 @@ importers: react-number-format: specifier: ^5.3.1 version: 5.3.1(react-dom@18.2.0)(react@18.2.0) + react-redux: + specifier: ^9.1.0 + version: 9.1.0(@types/react@18.2.38)(react@18.2.0)(redux@5.0.1) react-router-dom: specifier: ^6.22.0 - version: 6.22.0(react-dom@18.2.0)(react@18.2.0) + version: 6.22.1(react-dom@18.2.0)(react@18.2.0) react-slick: specifier: ^0.30.2 version: 0.30.2(react-dom@18.2.0)(react@18.2.0) recharts: specifier: ^2.12.0 - version: 2.12.0(react-dom@18.2.0)(react@18.2.0) + version: 2.12.1(react-dom@18.2.0)(react@18.2.0) devDependencies: '@thesis-co/eslint-config': specifier: github:thesis/eslint-config#7b9bc8c @@ -431,7 +437,7 @@ packages: '@babel/traverse': 7.23.4 '@babel/types': 7.23.4 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1730,7 +1736,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.4 '@babel/types': 7.23.4 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3455,7 +3461,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) espree: 9.6.1 globals: 13.23.0 ignore: 5.3.0 @@ -4044,7 +4050,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4354,15 +4360,15 @@ packages: - '@keep-network/keep-core' dev: false - /@keep-network/ecdsa@2.1.0-dev.18(@keep-network/keep-core@1.8.1-dev.0): - resolution: {integrity: sha512-VjgQL5wROhUHrVnu2glkLi0x6wj3Q0AW4f843cr/PgMhQoJ6LG6WQoE6OANbg4WbNIx5Tcf9/9FZ2m1k+IYQXQ==} + /@keep-network/ecdsa@2.1.0-dev.19(@keep-network/keep-core@1.8.1-dev.0): + resolution: {integrity: sha512-cyqRqK/sOqyaXZWY/O9ij6EINQuJ+bHLMiuufOFyP5YCj4GCuNqOcCytGAZPT+mED/0J/xn0vm+fgiCBq/uJkQ==} engines: {node: '>= 14.0.0'} dependencies: - '@keep-network/random-beacon': 2.1.0-dev.17(@keep-network/keep-core@1.8.1-dev.0) + '@keep-network/random-beacon': 2.1.0-dev.18(@keep-network/keep-core@1.8.1-dev.0) '@keep-network/sortition-pools': 2.0.0 '@openzeppelin/contracts': 4.9.5 '@openzeppelin/contracts-upgradeable': 4.9.5 - '@threshold-network/solidity-contracts': 1.3.0-dev.11(@keep-network/keep-core@1.8.1-dev.0) + '@threshold-network/solidity-contracts': 1.3.0-dev.12(@keep-network/keep-core@1.8.1-dev.0) transitivePeerDependencies: - '@keep-network/keep-core' dev: false @@ -4398,6 +4404,7 @@ packages: /@keep-network/keep-core@1.8.1-goerli.0: resolution: {integrity: sha512-h3La/RqbyEZjBBPg8V+pcRFo3UpWZUF4CxWfXHZnUR4PnkZKnIDrTNFQPhpV2uYFZwrbJxTR9mzOq/DOAiXPwA==} + deprecated: Package deprecated due to deprecation of the Goerli testnet. dependencies: '@openzeppelin/upgrades': 2.8.0 openzeppelin-solidity: 2.4.0 @@ -4409,6 +4416,7 @@ packages: /@keep-network/keep-ecdsa@1.9.0-goerli.0: resolution: {integrity: sha512-EA/oTcxmia5nznQ35ub9/5xBqBK4T+78oWYxASCc+THdPLalzriSAtQ517R4QnvkHi82NFhJjZH8WBoRXniddA==} + deprecated: Package deprecated due to deprecation of the Goerli testnet. dependencies: '@keep-network/keep-core': 1.8.1-goerli.0 '@keep-network/sortition-pools': 1.2.0-dev.1 @@ -4500,12 +4508,12 @@ packages: - utf-8-validate dev: false - /@keep-network/tbtc-v2@1.6.0-dev.18(@keep-network/keep-core@1.8.1-dev.0): - resolution: {integrity: sha512-62QBEPAsE3dju5Hk+yqeLhE5ohmVg8ZdFE+x+lmkDPbVz6DrHx0NqF6a1TLC/yH826zhnVCTNpXoPuOv3QUnvA==} + /@keep-network/tbtc-v2@1.6.0-dev.24(@keep-network/keep-core@1.8.1-dev.0): + resolution: {integrity: sha512-Nj/TYHlVg5J1ubVlLEbO7IQiqecDmVf2DPOqkICpveFxOiIbqzVtop4CsgEPvyRBomzHDUvZ6QFxYxJj/wbJbA==} engines: {node: '>= 14.0.0'} dependencies: '@keep-network/bitcoin-spv-sol': 3.4.0-solc-0.8 - '@keep-network/ecdsa': 2.1.0-dev.18(@keep-network/keep-core@1.8.1-dev.0) + '@keep-network/ecdsa': 2.1.0-dev.19(@keep-network/keep-core@1.8.1-dev.0) '@keep-network/random-beacon': 2.1.0-dev.18(@keep-network/keep-core@1.8.1-dev.0) '@keep-network/tbtc': 1.1.2-dev.1 '@openzeppelin/contracts': 4.9.5 @@ -5862,8 +5870,27 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false - /@remix-run/router@1.15.0: - resolution: {integrity: sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==} + /@reduxjs/toolkit@2.2.1(react-redux@9.1.0)(react@18.2.0): + resolution: {integrity: sha512-8CREoqJovQW/5I4yvvijm/emUiCCmcs4Ev4XPWd4mizSO+dD3g5G6w34QK5AGeNrSH7qM8Fl66j4vuV7dpOdkw==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + dependencies: + immer: 10.0.3 + react: 18.2.0 + react-redux: 9.1.0(@types/react@18.2.38)(react@18.2.0)(redux@5.0.1) + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.0 + dev: false + + /@remix-run/router@1.15.1: + resolution: {integrity: sha512-zcU0gM3z+3iqj8UX45AmWY810l3oUmXM7uH4dt5xtzvMhRtYVhKGOmgOd1877dOPPepfCjUv57w+syamWIYe7w==} engines: {node: '>=14.0.0'} dev: false @@ -6407,6 +6434,17 @@ packages: '@thesis/solidity-contracts': github.com/thesis/solidity-contracts/4985bcf dev: false + /@threshold-network/solidity-contracts@1.3.0-dev.12(@keep-network/keep-core@1.8.1-dev.0): + resolution: {integrity: sha512-06EF583uEwko3ik7qjnMOg+sJ+Vb7YWkqag4a9xZq8Mmy8rifpmLjnfDKCVGeKbUis3uI++pTGC9U/EfvVOrlQ==} + peerDependencies: + '@keep-network/keep-core': '>1.8.1-dev <1.8.1-goerli' + dependencies: + '@keep-network/keep-core': 1.8.1-dev.0 + '@openzeppelin/contracts': 4.5.0 + '@openzeppelin/contracts-upgradeable': 4.5.2 + '@thesis/solidity-contracts': github.com/thesis/solidity-contracts/4985bcf + dev: false + /@threshold-network/solidity-contracts@1.3.0-dev.8(@keep-network/keep-core@1.8.1-dev.0): resolution: {integrity: sha512-s6SFZyf1xXgOdMK1zYnjsURnVz7Xxzf0z/34vH+hDg8n/G8L0jPR6Iz4laWSSL2y1P3ffFAFTUMvwfJMJitfVw==} peerDependencies: @@ -6892,6 +6930,10 @@ packages: /@types/tmp@0.0.33: resolution: {integrity: sha512-gVC1InwyVrO326wbBZw+AO3u2vRXz/iRWq9jYhpG4W8LXyIgDv3ZmcLQ5Q4Gs+gFMyqx+viFoFT+l3p61QFCmQ==} + /@types/use-sync-external-store@0.0.3: + resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} + dev: false + /@types/utf8@2.1.6: resolution: {integrity: sha512-pRs2gYF5yoKYrgSaira0DJqVg2tFuF+Qjp838xS7K+mJyY2jJzjsrl6y17GbIa4uMRogMbxs+ghNCvKg6XyNrA==} dev: false @@ -6953,7 +6995,7 @@ packages: '@typescript-eslint/type-utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) '@typescript-eslint/utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.54.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -6998,7 +7040,7 @@ packages: '@typescript-eslint/types': 6.12.0 '@typescript-eslint/typescript-estree': 6.12.0(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.54.0 typescript: 5.3.2 transitivePeerDependencies: @@ -7049,7 +7091,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.12.0(typescript@5.3.2) '@typescript-eslint/utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.54.0 ts-api-utils: 1.0.3(typescript@5.3.2) typescript: 5.3.2 @@ -7096,7 +7138,7 @@ packages: dependencies: '@typescript-eslint/types': 6.12.0 '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -7816,16 +7858,6 @@ packages: transitivePeerDependencies: - debug - /axios@1.6.7: - resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} - dependencies: - follow-redirects: 1.15.5 - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - dev: false - /axios@1.6.7(debug@4.3.4): resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} dependencies: @@ -7834,7 +7866,6 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: true /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} @@ -8414,8 +8445,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001574 - electron-to-chromium: 1.4.622 + caniuse-lite: 1.0.30001585 + electron-to-chromium: 1.4.659 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.2) @@ -8642,8 +8673,8 @@ packages: /caniuse-lite@1.0.30001564: resolution: {integrity: sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==} - /caniuse-lite@1.0.30001574: - resolution: {integrity: sha512-BtYEK4r/iHt/txm81KBudCUcTy7t+s9emrIaHqjYurQ10x71zJ5VQ9x1dYPcz/b+pKSp4y/v1xSI67A+LzpNyg==} + /caniuse-lite@1.0.30001585: + resolution: {integrity: sha512-yr2BWR1yLXQ8fMpdS/4ZZXpseBgE7o4g41x3a6AJOqZuOi+iE/WdJYAuZ6Y95i4Ohd2Y+9MzIWRR+uGABH4s3Q==} /capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -9689,17 +9720,6 @@ packages: dependencies: ms: 2.1.3 - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -10126,8 +10146,8 @@ packages: /electron-to-chromium@1.4.592: resolution: {integrity: sha512-D3NOkROIlF+d5ixnz7pAf3Lu/AuWpd6AYgI9O67GQXMXTcCP1gJQRotOq35eQy5Sb4hez33XH1YdTtILA7Udww==} - /electron-to-chromium@1.4.622: - resolution: {integrity: sha512-GZ47DEy0Gm2Z8RVG092CkFvX7SdotG57c4YZOe8W8qD4rOmk3plgeNmiLVRHP/Liqj1wRiY3uUUod9vb9hnxZA==} + /electron-to-chromium@1.4.659: + resolution: {integrity: sha512-sRJ3nV3HowrYpBtPF9bASQV7OW49IgZC01Xiq43WfSE3RTCkK0/JidoCmR73Hyc1mN+l/H4Yqx0eNiomvExFZg==} /elliptic@6.3.3: resolution: {integrity: sha512-cIky9SO2H8W2eU1NOLySnhOYJnuEWCq9ZJeHvHd/lXzEL9vyraIMfilZSn57X3aVX+wkfYmqkch2LvmTzkjFpA==} @@ -10861,7 +10881,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -11634,16 +11654,6 @@ packages: dependencies: debug: 4.3.4(supports-color@8.1.1) - /follow-redirects@1.15.5: - resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dev: false - /follow-redirects@1.15.5(debug@4.3.4): resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} engines: {node: '>=4.0'} @@ -11654,7 +11664,6 @@ packages: optional: true dependencies: debug: 4.3.4(supports-color@8.1.1) - dev: true /follow-redirects@1.5.10: resolution: {integrity: sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==} @@ -13184,6 +13193,10 @@ packages: resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} engines: {node: '>= 4'} + /immer@10.0.3: + resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==} + dev: false + /immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} @@ -16715,6 +16728,28 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-redux@9.1.0(@types/react@18.2.38)(react@18.2.0)(redux@5.0.1): + resolution: {integrity: sha512-6qoDzIO+gbrza8h3hjMA9aq4nwVFCKFtY2iLxCtVT38Swyy2C/dJCGBXHeHLtx6qlg/8qzc2MrhOeduf5K32wQ==} + peerDependencies: + '@types/react': ^18.2.25 + react: ^18.0 + react-native: '>=0.69' + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-native: + optional: true + redux: + optional: true + dependencies: + '@types/react': 18.2.38 + '@types/use-sync-external-store': 0.0.3 + react: 18.2.0 + redux: 5.0.1 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + /react-refresh@0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} engines: {node: '>=0.10.0'} @@ -16754,26 +16789,26 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.38)(react@18.2.0) dev: false - /react-router-dom@6.22.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==} + /react-router-dom@6.22.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-iwMyyyrbL7zkKY7MRjOVRy+TMnS/OPusaFVxM2P11x9dzSzGmLsebkCvYirGq0DWB9K9hOspHYYtDz33gE5Duw==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@remix-run/router': 1.15.0 + '@remix-run/router': 1.15.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-router: 6.22.0(react@18.2.0) + react-router: 6.22.1(react@18.2.0) dev: false - /react-router@6.22.0(react@18.2.0): - resolution: {integrity: sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==} + /react-router@6.22.1(react@18.2.0): + resolution: {integrity: sha512-0pdoRGwLtemnJqn1K0XHUbnKiX0S4X8CgvVVmHGOWmofESj31msHo/1YiqcJWK7Wxfq2a4uvvtS01KAQyWK/CQ==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' dependencies: - '@remix-run/router': 1.15.0 + '@remix-run/router': 1.15.1 react: 18.2.0 dev: false @@ -16914,8 +16949,8 @@ packages: decimal.js-light: 2.5.1 dev: false - /recharts@2.12.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-rVNcdNQ5b7+40Ue7mcEKZJyEv+3SUk2bDEVvOyXPDXXVE7TU3lrvnJUgAvO36hSzhRP2DnAamKXvHLFIFOU0Ww==} + /recharts@2.12.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-35vUCEBPf+pM+iVgSgVTn86faKya5pc4JO6cYJL63qOK2zDEyzDn20Tdj+CDI/3z+VcpKyQ8ZBQ9OiQ+vuAbjg==} engines: {node: '>=14'} peerDependencies: react: ^16.0.0 || ^17.0.0 || ^18.0.0 @@ -16958,11 +16993,23 @@ packages: dependencies: redux: 4.2.1 + /redux-thunk@3.1.0(redux@5.0.1): + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + dependencies: + redux: 5.0.1 + dev: false + /redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} dependencies: '@babel/runtime': 7.23.4 + /redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + dev: false + /reflect.getprototypeof@1.0.4: resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} engines: {node: '>= 0.4'} @@ -17111,6 +17158,10 @@ packages: /require-package-name@2.0.1: resolution: {integrity: sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==} + /reselect@5.1.0: + resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==} + dev: false + /resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} dev: false @@ -19083,6 +19134,14 @@ packages: tslib: 2.6.2 dev: false + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /utf-8-validate@5.0.10: resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} engines: {node: '>=6.14.2'} diff --git a/sdk/src/lib/contracts/index.ts b/sdk/src/lib/contracts/index.ts index fc0664ae1..b64a27f1b 100644 --- a/sdk/src/lib/contracts/index.ts +++ b/sdk/src/lib/contracts/index.ts @@ -1,7 +1,9 @@ import { TBTCDepositor } from "./tbtc-depositor" +import { StBTC } from "./stbtc" export * from "./tbtc-depositor" export * from "./chain-identifier" +export * from "./stbtc" export * from "./depositor-proxy" /** @@ -9,4 +11,5 @@ export * from "./depositor-proxy" */ export type AcreContracts = { tbtcDepositor: TBTCDepositor + stBTC: StBTC } diff --git a/sdk/src/lib/contracts/stbtc.ts b/sdk/src/lib/contracts/stbtc.ts new file mode 100644 index 000000000..5eac12c4f --- /dev/null +++ b/sdk/src/lib/contracts/stbtc.ts @@ -0,0 +1,15 @@ +import { ChainIdentifier } from "./chain-identifier" + +export interface StBTC { + /** + * @param identifier The generic chain identifier. + * @returns Value of the basis for calculating final BTC balance. + */ + balanceOf(identifier: ChainIdentifier): Promise + + /** + * @param identifier The generic chain identifier. + * @returns Maximum withdraw value. + */ + assetsBalanceOf(identifier: ChainIdentifier): Promise +} diff --git a/sdk/src/lib/ethereum/artifacts/sepolia/stBTC.json b/sdk/src/lib/ethereum/artifacts/sepolia/stBTC.json new file mode 100644 index 000000000..ea1bf83ad --- /dev/null +++ b/sdk/src/lib/ethereum/artifacts/sepolia/stBTC.json @@ -0,0 +1,1642 @@ +{ + "address": "0x1265bDDDd268daf903551cAf52800D758F8930c4", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "_tbtc", + "type": "address" + }, + { + "internalType": "address", + "name": "_treasury", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "DisallowedAddress", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "max", + "type": "uint256" + } + ], + "name": "ERC4626ExceededMaxDeposit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "max", + "type": "uint256" + } + ], + "name": "ERC4626ExceededMaxMint", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "max", + "type": "uint256" + } + ], + "name": "ERC4626ExceededMaxRedeem", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "max", + "type": "uint256" + } + ], + "name": "ERC4626ExceededMaxWithdraw", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "min", + "type": "uint256" + } + ], + "name": "LessThanMinDeposit", + "type": "error" + }, + { + "inputs": [], + "name": "MathOverflowedMulDiv", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "minimumDepositAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "maximumTotalAssets", + "type": "uint256" + } + ], + "name": "DepositParametersUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldDispatcher", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newDispatcher", + "type": "address" + } + ], + "name": "DispatcherUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "treasury", + "type": "address" + } + ], + "name": "TreasuryUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "asset", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "assetsBalanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "convertToAssets", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "name": "convertToShares", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "deposit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "depositParameters", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "dispatcher", + "outputs": [ + { + "internalType": "contract Dispatcher", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "maxDeposit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "maxMint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "maxRedeem", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "maxWithdraw", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maximumTotalAssets", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minimumDepositAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "name": "previewDeposit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "previewMint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "previewRedeem", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "name": "previewWithdraw", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "redeem", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalAssets", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "treasury", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_minimumDepositAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_maximumTotalAssets", + "type": "uint256" + } + ], + "name": "updateDepositParameters", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract Dispatcher", + "name": "newDispatcher", + "type": "address" + } + ], + "name": "updateDispatcher", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newTreasury", + "type": "address" + } + ], + "name": "updateTreasury", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "withdraw", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "transactionHash": "0x15d8cea2a1d7bc42ef13ac211cf8cc7c438c110432d8695f77b303b2eab1a1a1", + "receipt": { + "to": null, + "from": "0x2d154A5c7cE9939274b89bbCe9f5B069E57b09A8", + "contractAddress": "0x1265bDDDd268daf903551cAf52800D758F8930c4", + "transactionIndex": 101, + "gasUsed": "1808006", + "logsBloom": "0x00000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000001000000000000000000000000000000400000020000000000000000000800000000000400000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000420000012000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0x77d0307f9267df860ef36c26a33209c98922ef1699e8b6f3ceaa76627632b61a", + "transactionHash": "0x15d8cea2a1d7bc42ef13ac211cf8cc7c438c110432d8695f77b303b2eab1a1a1", + "logs": [ + { + "transactionIndex": 101, + "blockNumber": 5415166, + "transactionHash": "0x15d8cea2a1d7bc42ef13ac211cf8cc7c438c110432d8695f77b303b2eab1a1a1", + "address": "0x1265bDDDd268daf903551cAf52800D758F8930c4", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000002d154a5c7ce9939274b89bbce9f5b069e57b09a8" + ], + "data": "0x", + "logIndex": 343, + "blockHash": "0x77d0307f9267df860ef36c26a33209c98922ef1699e8b6f3ceaa76627632b61a" + } + ], + "blockNumber": 5415166, + "cumulativeGasUsed": "21174168", + "status": 1, + "byzantium": true + }, + "args": [ + "0x517f2982701695D4E52f1ECFBEf3ba31Df470161", + "0x2d154A5c7cE9939274b89bbCe9f5B069E57b09A8" + ], + "numDeployments": 1, + "solcInputHash": "ca2ee42c4a116704dc3eb0bec6d11bc6", + "metadata": "{\"compiler\":{\"version\":\"0.8.21+commit.d9974bed\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"_tbtc\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_treasury\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"}],\"name\":\"AddressEmptyCode\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"AddressInsufficientBalance\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DisallowedAddress\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"allowance\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"needed\",\"type\":\"uint256\"}],\"name\":\"ERC20InsufficientAllowance\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"needed\",\"type\":\"uint256\"}],\"name\":\"ERC20InsufficientBalance\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"approver\",\"type\":\"address\"}],\"name\":\"ERC20InvalidApprover\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"}],\"name\":\"ERC20InvalidReceiver\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"}],\"name\":\"ERC20InvalidSender\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"ERC20InvalidSpender\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"assets\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"max\",\"type\":\"uint256\"}],\"name\":\"ERC4626ExceededMaxDeposit\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"max\",\"type\":\"uint256\"}],\"name\":\"ERC4626ExceededMaxMint\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"max\",\"type\":\"uint256\"}],\"name\":\"ERC4626ExceededMaxRedeem\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"assets\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"max\",\"type\":\"uint256\"}],\"name\":\"ERC4626ExceededMaxWithdraw\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"FailedInnerCall\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"min\",\"type\":\"uint256\"}],\"name\":\"LessThanMinDeposit\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MathOverflowedMulDiv\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"OwnableInvalidOwner\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"OwnableUnauthorizedAccount\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"}],\"name\":\"SafeERC20FailedOperation\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ZeroAddress\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"assets\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"minimumDepositAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"maximumTotalAssets\",\"type\":\"uint256\"}],\"name\":\"DepositParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"oldDispatcher\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"newDispatcher\",\"type\":\"address\"}],\"name\":\"DispatcherUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"treasury\",\"type\":\"address\"}],\"name\":\"TreasuryUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"assets\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"name\":\"Withdraw\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"asset\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"assetsBalanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"name\":\"convertToAssets\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"assets\",\"type\":\"uint256\"}],\"name\":\"convertToShares\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"assets\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"}],\"name\":\"deposit\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"depositParameters\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"dispatcher\",\"outputs\":[{\"internalType\":\"contract Dispatcher\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"maxDeposit\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"}],\"name\":\"maxMint\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"maxRedeem\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"maxWithdraw\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"maximumTotalAssets\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"minimumDepositAmount\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"}],\"name\":\"mint\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"assets\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"assets\",\"type\":\"uint256\"}],\"name\":\"previewDeposit\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"name\":\"previewMint\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"name\":\"previewRedeem\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"assets\",\"type\":\"uint256\"}],\"name\":\"previewWithdraw\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"redeem\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalAssets\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"treasury\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_minimumDepositAmount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_maximumTotalAssets\",\"type\":\"uint256\"}],\"name\":\"updateDepositParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract Dispatcher\",\"name\":\"newDispatcher\",\"type\":\"address\"}],\"name\":\"updateDispatcher\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newTreasury\",\"type\":\"address\"}],\"name\":\"updateTreasury\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"assets\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"withdraw\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"ERC-4626 is a standard to optimize and unify the technical parameters of yield-bearing vaults. This contract facilitates the minting and burning of shares (stBTC), which are represented as standard ERC20 tokens, providing a seamless exchange with tBTC tokens.\",\"errors\":{\"AddressEmptyCode(address)\":[{\"details\":\"There's no code at `target` (it is not a contract).\"}],\"AddressInsufficientBalance(address)\":[{\"details\":\"The ETH balance of the account is not enough to perform the operation.\"}],\"ERC20InsufficientAllowance(address,uint256,uint256)\":[{\"details\":\"Indicates a failure with the `spender`\\u2019s `allowance`. Used in transfers.\",\"params\":{\"allowance\":\"Amount of tokens a `spender` is allowed to operate with.\",\"needed\":\"Minimum amount required to perform a transfer.\",\"spender\":\"Address that may be allowed to operate on tokens without being their owner.\"}}],\"ERC20InsufficientBalance(address,uint256,uint256)\":[{\"details\":\"Indicates an error related to the current `balance` of a `sender`. Used in transfers.\",\"params\":{\"balance\":\"Current balance for the interacting account.\",\"needed\":\"Minimum amount required to perform a transfer.\",\"sender\":\"Address whose tokens are being transferred.\"}}],\"ERC20InvalidApprover(address)\":[{\"details\":\"Indicates a failure with the `approver` of a token to be approved. Used in approvals.\",\"params\":{\"approver\":\"Address initiating an approval operation.\"}}],\"ERC20InvalidReceiver(address)\":[{\"details\":\"Indicates a failure with the token `receiver`. Used in transfers.\",\"params\":{\"receiver\":\"Address to which tokens are being transferred.\"}}],\"ERC20InvalidSender(address)\":[{\"details\":\"Indicates a failure with the token `sender`. Used in transfers.\",\"params\":{\"sender\":\"Address whose tokens are being transferred.\"}}],\"ERC20InvalidSpender(address)\":[{\"details\":\"Indicates a failure with the `spender` to be approved. Used in approvals.\",\"params\":{\"spender\":\"Address that may be allowed to operate on tokens without being their owner.\"}}],\"ERC4626ExceededMaxDeposit(address,uint256,uint256)\":[{\"details\":\"Attempted to deposit more assets than the max amount for `receiver`.\"}],\"ERC4626ExceededMaxMint(address,uint256,uint256)\":[{\"details\":\"Attempted to mint more shares than the max amount for `receiver`.\"}],\"ERC4626ExceededMaxRedeem(address,uint256,uint256)\":[{\"details\":\"Attempted to redeem more shares than the max amount for `receiver`.\"}],\"ERC4626ExceededMaxWithdraw(address,uint256,uint256)\":[{\"details\":\"Attempted to withdraw more assets than the max amount for `receiver`.\"}],\"FailedInnerCall()\":[{\"details\":\"A call to an address target failed. The target may have reverted.\"}],\"LessThanMinDeposit(uint256,uint256)\":[{\"params\":{\"amount\":\"Amount to check.\",\"min\":\"Minimum amount to check 'amount' against.\"}}],\"MathOverflowedMulDiv()\":[{\"details\":\"Muldiv operation overflow.\"}],\"OwnableInvalidOwner(address)\":[{\"details\":\"The owner is not a valid owner account. (eg. `address(0)`)\"}],\"OwnableUnauthorizedAccount(address)\":[{\"details\":\"The caller account is not authorized to perform an operation.\"}],\"SafeERC20FailedOperation(address)\":[{\"details\":\"An operation with an ERC20 token failed.\"}]},\"events\":{\"Approval(address,address,uint256)\":{\"details\":\"Emitted when the allowance of a `spender` for an `owner` is set by a call to {approve}. `value` is the new allowance.\"},\"DepositParametersUpdated(uint256,uint256)\":{\"params\":{\"maximumTotalAssets\":\"New value of the maximum total assets amount.\",\"minimumDepositAmount\":\"New value of the minimum deposit amount.\"}},\"DispatcherUpdated(address,address)\":{\"params\":{\"newDispatcher\":\"Address of the new dispatcher contract.\",\"oldDispatcher\":\"Address of the old dispatcher contract.\"}},\"Transfer(address,address,uint256)\":{\"details\":\"Emitted when `value` tokens are moved from one account (`from`) to another (`to`). Note that `value` may be zero.\"},\"TreasuryUpdated(address)\":{\"params\":{\"treasury\":\"New treasury wallet address.\"}}},\"kind\":\"dev\",\"methods\":{\"allowance(address,address)\":{\"details\":\"See {IERC20-allowance}.\"},\"approve(address,uint256)\":{\"details\":\"See {IERC20-approve}. NOTE: If `value` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address.\"},\"asset()\":{\"details\":\"See {IERC4626-asset}. \"},\"assetsBalanceOf(address)\":{\"params\":{\"account\":\"Owner of shares.\"},\"returns\":{\"_0\":\"Assets amount.\"}},\"balanceOf(address)\":{\"details\":\"See {IERC20-balanceOf}.\"},\"convertToAssets(uint256)\":{\"details\":\"See {IERC4626-convertToAssets}. \"},\"convertToShares(uint256)\":{\"details\":\"See {IERC4626-convertToShares}. \"},\"decimals()\":{\"details\":\"Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This \\\"original\\\" value is cached during construction of the vault contract. If this read operation fails (e.g., the asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals. See {IERC20Metadata-decimals}.\"},\"deposit(uint256,address)\":{\"details\":\"Takes into account a deposit parameter, minimum deposit amount, which determines the minimum amount for a single deposit operation. The amount of the assets has to be pre-approved in the tBTC contract.\",\"params\":{\"assets\":\"Approved amount of tBTC tokens to deposit.\",\"receiver\":\"The address to which the shares will be minted.\"},\"returns\":{\"_0\":\"Minted shares.\"}},\"depositParameters()\":{\"returns\":{\"_0\":\"Returns deposit parameters.\"}},\"maxDeposit(address)\":{\"details\":\"When the remaining amount of unused limit is less than the minimum deposit amount, this function returns 0.\",\"returns\":{\"_0\":\"The maximum amount of tBTC token that can be deposited into Acre protocol for the receiver.\"}},\"maxMint(address)\":{\"details\":\"Since the stBTC contract limits the maximum total tBTC tokens this function converts the maximum deposit amount to shares.\",\"returns\":{\"_0\":\"The maximum amount of the vault shares.\"}},\"maxRedeem(address)\":{\"details\":\"See {IERC4626-maxRedeem}. \"},\"maxWithdraw(address)\":{\"details\":\"See {IERC4626-maxWithdraw}. \"},\"mint(uint256,address)\":{\"details\":\"Takes into account a deposit parameter, minimum deposit amount, which determines the minimum amount for a single deposit operation. The amount of the assets has to be pre-approved in the tBTC contract. The msg.sender is required to grant approval for tBTC transfer. To determine the total assets amount necessary for approval corresponding to a given share amount, use the `previewMint` function.\",\"params\":{\"receiver\":\"The address to which the shares will be minted.\",\"shares\":\"Amount of shares to mint.\"}},\"name()\":{\"details\":\"Returns the name of the token.\"},\"owner()\":{\"details\":\"Returns the address of the current owner.\"},\"previewDeposit(uint256)\":{\"details\":\"See {IERC4626-previewDeposit}. \"},\"previewMint(uint256)\":{\"details\":\"See {IERC4626-previewMint}. \"},\"previewRedeem(uint256)\":{\"details\":\"See {IERC4626-previewRedeem}. \"},\"previewWithdraw(uint256)\":{\"details\":\"See {IERC4626-previewWithdraw}. \"},\"redeem(uint256,address,address)\":{\"details\":\"See {IERC4626-redeem}. \"},\"renounceOwnership()\":{\"details\":\"Leaves the contract without owner. It will not be possible to call `onlyOwner` functions. Can only be called by the current owner. NOTE: Renouncing ownership will leave the contract without an owner, thereby disabling any functionality that is only available to the owner.\"},\"symbol()\":{\"details\":\"Returns the symbol of the token, usually a shorter version of the name.\"},\"totalAssets()\":{\"details\":\"See {IERC4626-totalAssets}. \"},\"totalSupply()\":{\"details\":\"See {IERC20-totalSupply}.\"},\"transfer(address,uint256)\":{\"details\":\"See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `value`.\"},\"transferFrom(address,address,uint256)\":{\"details\":\"See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `value`. - the caller must have allowance for ``from``'s tokens of at least `value`.\"},\"transferOwnership(address)\":{\"details\":\"Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner.\"},\"updateDepositParameters(uint256,uint256)\":{\"details\":\"To disable the limit for deposits, set the maximum total assets to maximum (`type(uint256).max`).\",\"params\":{\"_maximumTotalAssets\":\"New value of the maximum total assets amount. It is the maximum amount of the tBTC token that the Acre protocol can hold.\",\"_minimumDepositAmount\":\"New value of the minimum deposit amount. It is the minimum amount for a single deposit operation.\"}},\"updateDispatcher(address)\":{\"params\":{\"newDispatcher\":\"Address of the new dispatcher contract.\"}},\"updateTreasury(address)\":{\"params\":{\"newTreasury\":\"New treasury wallet address.\"}},\"withdraw(uint256,address,address)\":{\"details\":\"See {IERC4626-withdraw}. \"}},\"title\":\"stBTC\",\"version\":1},\"userdoc\":{\"errors\":{\"DisallowedAddress()\":[{\"notice\":\"Reverts if the address is disallowed.\"}],\"LessThanMinDeposit(uint256,uint256)\":[{\"notice\":\"Reverts if the amount is less than the minimum deposit amount.\"}],\"ZeroAddress()\":[{\"notice\":\"Reverts if the address is zero.\"}]},\"events\":{\"DepositParametersUpdated(uint256,uint256)\":{\"notice\":\"Emitted when deposit parameters are updated.\"},\"DispatcherUpdated(address,address)\":{\"notice\":\"Emitted when the dispatcher contract is updated.\"},\"TreasuryUpdated(address)\":{\"notice\":\"Emitted when the treasury wallet address is updated.\"}},\"kind\":\"user\",\"methods\":{\"assetsBalanceOf(address)\":{\"notice\":\"Returns value of assets that would be exchanged for the amount of shares owned by the `account`.\"},\"deposit(uint256,address)\":{\"notice\":\"Mints shares to receiver by depositing exactly amount of tBTC tokens.\"},\"dispatcher()\":{\"notice\":\"Dispatcher contract that routes tBTC from stBTC to a given vault and back.\"},\"maxDeposit(address)\":{\"notice\":\"Returns the maximum amount of the tBTC token that can be deposited into the vault for the receiver through a deposit call. It takes into account the deposit parameter, maximum total assets, which determines the total amount of tBTC token held by Acre protocol.\"},\"maxMint(address)\":{\"notice\":\"Returns the maximum amount of the vault shares that can be minted for the receiver, through a mint call.\"},\"maximumTotalAssets()\":{\"notice\":\"Maximum total amount of tBTC token held by Acre protocol.\"},\"minimumDepositAmount()\":{\"notice\":\"Minimum amount for a single deposit operation.\"},\"mint(uint256,address)\":{\"notice\":\"Mints shares to receiver by depositing tBTC tokens.\"},\"treasury()\":{\"notice\":\"Address of the treasury wallet, where fees should be transferred to.\"},\"updateDepositParameters(uint256,uint256)\":{\"notice\":\"Updates deposit parameters.\"},\"updateDispatcher(address)\":{\"notice\":\"Updates the dispatcher contract and gives it an unlimited allowance to transfer staked tBTC.\"},\"updateTreasury(address)\":{\"notice\":\"Updates treasury wallet address.\"}},\"notice\":\"This contract implements the ERC-4626 tokenized vault standard. By staking tBTC, users acquire a liquid staking token called stBTC, commonly referred to as \\\"shares\\\". The staked tBTC is securely deposited into Acre's vaults, where it generates yield over time. Users have the flexibility to redeem stBTC, enabling them to withdraw their staked tBTC along with the accrued yield.\",\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/stBTC.sol\":\"stBTC\"},\"evmVersion\":\"paris\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":1000},\"remappings\":[]},\"sources\":{\"@openzeppelin/contracts/access/Ownable.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)\\n\\npragma solidity ^0.8.20;\\n\\nimport {Context} from \\\"../utils/Context.sol\\\";\\n\\n/**\\n * @dev Contract module which provides a basic access control mechanism, where\\n * there is an account (an owner) that can be granted exclusive access to\\n * specific functions.\\n *\\n * The initial owner is set to the address provided by the deployer. This can\\n * later be changed with {transferOwnership}.\\n *\\n * This module is used through inheritance. It will make available the modifier\\n * `onlyOwner`, which can be applied to your functions to restrict their use to\\n * the owner.\\n */\\nabstract contract Ownable is Context {\\n address private _owner;\\n\\n /**\\n * @dev The caller account is not authorized to perform an operation.\\n */\\n error OwnableUnauthorizedAccount(address account);\\n\\n /**\\n * @dev The owner is not a valid owner account. (eg. `address(0)`)\\n */\\n error OwnableInvalidOwner(address owner);\\n\\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\\n\\n /**\\n * @dev Initializes the contract setting the address provided by the deployer as the initial owner.\\n */\\n constructor(address initialOwner) {\\n if (initialOwner == address(0)) {\\n revert OwnableInvalidOwner(address(0));\\n }\\n _transferOwnership(initialOwner);\\n }\\n\\n /**\\n * @dev Throws if called by any account other than the owner.\\n */\\n modifier onlyOwner() {\\n _checkOwner();\\n _;\\n }\\n\\n /**\\n * @dev Returns the address of the current owner.\\n */\\n function owner() public view virtual returns (address) {\\n return _owner;\\n }\\n\\n /**\\n * @dev Throws if the sender is not the owner.\\n */\\n function _checkOwner() internal view virtual {\\n if (owner() != _msgSender()) {\\n revert OwnableUnauthorizedAccount(_msgSender());\\n }\\n }\\n\\n /**\\n * @dev Leaves the contract without owner. It will not be possible to call\\n * `onlyOwner` functions. Can only be called by the current owner.\\n *\\n * NOTE: Renouncing ownership will leave the contract without an owner,\\n * thereby disabling any functionality that is only available to the owner.\\n */\\n function renounceOwnership() public virtual onlyOwner {\\n _transferOwnership(address(0));\\n }\\n\\n /**\\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\\n * Can only be called by the current owner.\\n */\\n function transferOwnership(address newOwner) public virtual onlyOwner {\\n if (newOwner == address(0)) {\\n revert OwnableInvalidOwner(address(0));\\n }\\n _transferOwnership(newOwner);\\n }\\n\\n /**\\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\\n * Internal function without access restriction.\\n */\\n function _transferOwnership(address newOwner) internal virtual {\\n address oldOwner = _owner;\\n _owner = newOwner;\\n emit OwnershipTransferred(oldOwner, newOwner);\\n }\\n}\\n\",\"keccak256\":\"0xff6d0bb2e285473e5311d9d3caacb525ae3538a80758c10649a4d61029b017bb\",\"license\":\"MIT\"},\"@openzeppelin/contracts/interfaces/IERC4626.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC4626.sol)\\n\\npragma solidity ^0.8.20;\\n\\nimport {IERC20} from \\\"../token/ERC20/IERC20.sol\\\";\\nimport {IERC20Metadata} from \\\"../token/ERC20/extensions/IERC20Metadata.sol\\\";\\n\\n/**\\n * @dev Interface of the ERC4626 \\\"Tokenized Vault Standard\\\", as defined in\\n * https://eips.ethereum.org/EIPS/eip-4626[ERC-4626].\\n */\\ninterface IERC4626 is IERC20, IERC20Metadata {\\n event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);\\n\\n event Withdraw(\\n address indexed sender,\\n address indexed receiver,\\n address indexed owner,\\n uint256 assets,\\n uint256 shares\\n );\\n\\n /**\\n * @dev Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing.\\n *\\n * - MUST be an ERC-20 token contract.\\n * - MUST NOT revert.\\n */\\n function asset() external view returns (address assetTokenAddress);\\n\\n /**\\n * @dev Returns the total amount of the underlying asset that is \\u201cmanaged\\u201d by Vault.\\n *\\n * - SHOULD include any compounding that occurs from yield.\\n * - MUST be inclusive of any fees that are charged against assets in the Vault.\\n * - MUST NOT revert.\\n */\\n function totalAssets() external view returns (uint256 totalManagedAssets);\\n\\n /**\\n * @dev Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal\\n * scenario where all the conditions are met.\\n *\\n * - MUST NOT be inclusive of any fees that are charged against assets in the Vault.\\n * - MUST NOT show any variations depending on the caller.\\n * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.\\n * - MUST NOT revert.\\n *\\n * NOTE: This calculation MAY NOT reflect the \\u201cper-user\\u201d price-per-share, and instead should reflect the\\n * \\u201caverage-user\\u2019s\\u201d price-per-share, meaning what the average user should expect to see when exchanging to and\\n * from.\\n */\\n function convertToShares(uint256 assets) external view returns (uint256 shares);\\n\\n /**\\n * @dev Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal\\n * scenario where all the conditions are met.\\n *\\n * - MUST NOT be inclusive of any fees that are charged against assets in the Vault.\\n * - MUST NOT show any variations depending on the caller.\\n * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.\\n * - MUST NOT revert.\\n *\\n * NOTE: This calculation MAY NOT reflect the \\u201cper-user\\u201d price-per-share, and instead should reflect the\\n * \\u201caverage-user\\u2019s\\u201d price-per-share, meaning what the average user should expect to see when exchanging to and\\n * from.\\n */\\n function convertToAssets(uint256 shares) external view returns (uint256 assets);\\n\\n /**\\n * @dev Returns the maximum amount of the underlying asset that can be deposited into the Vault for the receiver,\\n * through a deposit call.\\n *\\n * - MUST return a limited value if receiver is subject to some deposit limit.\\n * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited.\\n * - MUST NOT revert.\\n */\\n function maxDeposit(address receiver) external view returns (uint256 maxAssets);\\n\\n /**\\n * @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given\\n * current on-chain conditions.\\n *\\n * - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit\\n * call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called\\n * in the same transaction.\\n * - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the\\n * deposit would be accepted, regardless if the user has enough tokens approved, etc.\\n * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees.\\n * - MUST NOT revert.\\n *\\n * NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in\\n * share price or some other type of condition, meaning the depositor will lose assets by depositing.\\n */\\n function previewDeposit(uint256 assets) external view returns (uint256 shares);\\n\\n /**\\n * @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens.\\n *\\n * - MUST emit the Deposit event.\\n * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the\\n * deposit execution, and are accounted for during deposit.\\n * - MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not\\n * approving enough underlying tokens to the Vault contract, etc).\\n *\\n * NOTE: most implementations will require pre-approval of the Vault with the Vault\\u2019s underlying asset token.\\n */\\n function deposit(uint256 assets, address receiver) external returns (uint256 shares);\\n\\n /**\\n * @dev Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call.\\n * - MUST return a limited value if receiver is subject to some mint limit.\\n * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted.\\n * - MUST NOT revert.\\n */\\n function maxMint(address receiver) external view returns (uint256 maxShares);\\n\\n /**\\n * @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given\\n * current on-chain conditions.\\n *\\n * - MUST return as close to and no fewer than the exact amount of assets that would be deposited in a mint call\\n * in the same transaction. I.e. mint should return the same or fewer assets as previewMint if called in the\\n * same transaction.\\n * - MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint\\n * would be accepted, regardless if the user has enough tokens approved, etc.\\n * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees.\\n * - MUST NOT revert.\\n *\\n * NOTE: any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in\\n * share price or some other type of condition, meaning the depositor will lose assets by minting.\\n */\\n function previewMint(uint256 shares) external view returns (uint256 assets);\\n\\n /**\\n * @dev Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens.\\n *\\n * - MUST emit the Deposit event.\\n * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint\\n * execution, and are accounted for during mint.\\n * - MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not\\n * approving enough underlying tokens to the Vault contract, etc).\\n *\\n * NOTE: most implementations will require pre-approval of the Vault with the Vault\\u2019s underlying asset token.\\n */\\n function mint(uint256 shares, address receiver) external returns (uint256 assets);\\n\\n /**\\n * @dev Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the\\n * Vault, through a withdraw call.\\n *\\n * - MUST return a limited value if owner is subject to some withdrawal limit or timelock.\\n * - MUST NOT revert.\\n */\\n function maxWithdraw(address owner) external view returns (uint256 maxAssets);\\n\\n /**\\n * @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block,\\n * given current on-chain conditions.\\n *\\n * - MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw\\n * call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if\\n * called\\n * in the same transaction.\\n * - MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though\\n * the withdrawal would be accepted, regardless if the user has enough shares, etc.\\n * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees.\\n * - MUST NOT revert.\\n *\\n * NOTE: any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in\\n * share price or some other type of condition, meaning the depositor will lose assets by depositing.\\n */\\n function previewWithdraw(uint256 assets) external view returns (uint256 shares);\\n\\n /**\\n * @dev Burns shares from owner and sends exactly assets of underlying tokens to receiver.\\n *\\n * - MUST emit the Withdraw event.\\n * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the\\n * withdraw execution, and are accounted for during withdraw.\\n * - MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner\\n * not having enough shares, etc).\\n *\\n * Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed.\\n * Those methods should be performed separately.\\n */\\n function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);\\n\\n /**\\n * @dev Returns the maximum amount of Vault shares that can be redeemed from the owner balance in the Vault,\\n * through a redeem call.\\n *\\n * - MUST return a limited value if owner is subject to some withdrawal limit or timelock.\\n * - MUST return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock.\\n * - MUST NOT revert.\\n */\\n function maxRedeem(address owner) external view returns (uint256 maxShares);\\n\\n /**\\n * @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block,\\n * given current on-chain conditions.\\n *\\n * - MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call\\n * in the same transaction. I.e. redeem should return the same or more assets as previewRedeem if called in the\\n * same transaction.\\n * - MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the\\n * redemption would be accepted, regardless if the user has enough shares, etc.\\n * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees.\\n * - MUST NOT revert.\\n *\\n * NOTE: any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in\\n * share price or some other type of condition, meaning the depositor will lose assets by redeeming.\\n */\\n function previewRedeem(uint256 shares) external view returns (uint256 assets);\\n\\n /**\\n * @dev Burns exactly shares from owner and sends assets of underlying tokens to receiver.\\n *\\n * - MUST emit the Withdraw event.\\n * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the\\n * redeem execution, and are accounted for during redeem.\\n * - MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner\\n * not having enough shares, etc).\\n *\\n * NOTE: some implementations will require pre-requesting to the Vault before a withdrawal may be performed.\\n * Those methods should be performed separately.\\n */\\n function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);\\n}\\n\",\"keccak256\":\"0x207f64371bc0fcc5be86713aa5da109a870cc3a6da50e93b64ee881e369b593d\",\"license\":\"MIT\"},\"@openzeppelin/contracts/interfaces/draft-IERC6093.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/draft-IERC6093.sol)\\npragma solidity ^0.8.20;\\n\\n/**\\n * @dev Standard ERC20 Errors\\n * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC20 tokens.\\n */\\ninterface IERC20Errors {\\n /**\\n * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.\\n * @param sender Address whose tokens are being transferred.\\n * @param balance Current balance for the interacting account.\\n * @param needed Minimum amount required to perform a transfer.\\n */\\n error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);\\n\\n /**\\n * @dev Indicates a failure with the token `sender`. Used in transfers.\\n * @param sender Address whose tokens are being transferred.\\n */\\n error ERC20InvalidSender(address sender);\\n\\n /**\\n * @dev Indicates a failure with the token `receiver`. Used in transfers.\\n * @param receiver Address to which tokens are being transferred.\\n */\\n error ERC20InvalidReceiver(address receiver);\\n\\n /**\\n * @dev Indicates a failure with the `spender`\\u2019s `allowance`. Used in transfers.\\n * @param spender Address that may be allowed to operate on tokens without being their owner.\\n * @param allowance Amount of tokens a `spender` is allowed to operate with.\\n * @param needed Minimum amount required to perform a transfer.\\n */\\n error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);\\n\\n /**\\n * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.\\n * @param approver Address initiating an approval operation.\\n */\\n error ERC20InvalidApprover(address approver);\\n\\n /**\\n * @dev Indicates a failure with the `spender` to be approved. Used in approvals.\\n * @param spender Address that may be allowed to operate on tokens without being their owner.\\n */\\n error ERC20InvalidSpender(address spender);\\n}\\n\\n/**\\n * @dev Standard ERC721 Errors\\n * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC721 tokens.\\n */\\ninterface IERC721Errors {\\n /**\\n * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in EIP-20.\\n * Used in balance queries.\\n * @param owner Address of the current owner of a token.\\n */\\n error ERC721InvalidOwner(address owner);\\n\\n /**\\n * @dev Indicates a `tokenId` whose `owner` is the zero address.\\n * @param tokenId Identifier number of a token.\\n */\\n error ERC721NonexistentToken(uint256 tokenId);\\n\\n /**\\n * @dev Indicates an error related to the ownership over a particular token. Used in transfers.\\n * @param sender Address whose tokens are being transferred.\\n * @param tokenId Identifier number of a token.\\n * @param owner Address of the current owner of a token.\\n */\\n error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner);\\n\\n /**\\n * @dev Indicates a failure with the token `sender`. Used in transfers.\\n * @param sender Address whose tokens are being transferred.\\n */\\n error ERC721InvalidSender(address sender);\\n\\n /**\\n * @dev Indicates a failure with the token `receiver`. Used in transfers.\\n * @param receiver Address to which tokens are being transferred.\\n */\\n error ERC721InvalidReceiver(address receiver);\\n\\n /**\\n * @dev Indicates a failure with the `operator`\\u2019s approval. Used in transfers.\\n * @param operator Address that may be allowed to operate on tokens without being their owner.\\n * @param tokenId Identifier number of a token.\\n */\\n error ERC721InsufficientApproval(address operator, uint256 tokenId);\\n\\n /**\\n * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.\\n * @param approver Address initiating an approval operation.\\n */\\n error ERC721InvalidApprover(address approver);\\n\\n /**\\n * @dev Indicates a failure with the `operator` to be approved. Used in approvals.\\n * @param operator Address that may be allowed to operate on tokens without being their owner.\\n */\\n error ERC721InvalidOperator(address operator);\\n}\\n\\n/**\\n * @dev Standard ERC1155 Errors\\n * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC1155 tokens.\\n */\\ninterface IERC1155Errors {\\n /**\\n * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.\\n * @param sender Address whose tokens are being transferred.\\n * @param balance Current balance for the interacting account.\\n * @param needed Minimum amount required to perform a transfer.\\n * @param tokenId Identifier number of a token.\\n */\\n error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId);\\n\\n /**\\n * @dev Indicates a failure with the token `sender`. Used in transfers.\\n * @param sender Address whose tokens are being transferred.\\n */\\n error ERC1155InvalidSender(address sender);\\n\\n /**\\n * @dev Indicates a failure with the token `receiver`. Used in transfers.\\n * @param receiver Address to which tokens are being transferred.\\n */\\n error ERC1155InvalidReceiver(address receiver);\\n\\n /**\\n * @dev Indicates a failure with the `operator`\\u2019s approval. Used in transfers.\\n * @param operator Address that may be allowed to operate on tokens without being their owner.\\n * @param owner Address of the current owner of a token.\\n */\\n error ERC1155MissingApprovalForAll(address operator, address owner);\\n\\n /**\\n * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.\\n * @param approver Address initiating an approval operation.\\n */\\n error ERC1155InvalidApprover(address approver);\\n\\n /**\\n * @dev Indicates a failure with the `operator` to be approved. Used in approvals.\\n * @param operator Address that may be allowed to operate on tokens without being their owner.\\n */\\n error ERC1155InvalidOperator(address operator);\\n\\n /**\\n * @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation.\\n * Used in batch transfers.\\n * @param idsLength Length of the array of token identifiers\\n * @param valuesLength Length of the array of token amounts\\n */\\n error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength);\\n}\\n\",\"keccak256\":\"0x60c65f701957fdd6faea1acb0bb45825791d473693ed9ecb34726fdfaa849dd7\",\"license\":\"MIT\"},\"@openzeppelin/contracts/token/ERC20/ERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/ERC20.sol)\\n\\npragma solidity ^0.8.20;\\n\\nimport {IERC20} from \\\"./IERC20.sol\\\";\\nimport {IERC20Metadata} from \\\"./extensions/IERC20Metadata.sol\\\";\\nimport {Context} from \\\"../../utils/Context.sol\\\";\\nimport {IERC20Errors} from \\\"../../interfaces/draft-IERC6093.sol\\\";\\n\\n/**\\n * @dev Implementation of the {IERC20} interface.\\n *\\n * This implementation is agnostic to the way tokens are created. This means\\n * that a supply mechanism has to be added in a derived contract using {_mint}.\\n *\\n * TIP: For a detailed writeup see our guide\\n * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How\\n * to implement supply mechanisms].\\n *\\n * The default value of {decimals} is 18. To change this, you should override\\n * this function so it returns a different value.\\n *\\n * We have followed general OpenZeppelin Contracts guidelines: functions revert\\n * instead returning `false` on failure. This behavior is nonetheless\\n * conventional and does not conflict with the expectations of ERC20\\n * applications.\\n *\\n * Additionally, an {Approval} event is emitted on calls to {transferFrom}.\\n * This allows applications to reconstruct the allowance for all accounts just\\n * by listening to said events. Other implementations of the EIP may not emit\\n * these events, as it isn't required by the specification.\\n */\\nabstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {\\n mapping(address account => uint256) private _balances;\\n\\n mapping(address account => mapping(address spender => uint256)) private _allowances;\\n\\n uint256 private _totalSupply;\\n\\n string private _name;\\n string private _symbol;\\n\\n /**\\n * @dev Sets the values for {name} and {symbol}.\\n *\\n * All two of these values are immutable: they can only be set once during\\n * construction.\\n */\\n constructor(string memory name_, string memory symbol_) {\\n _name = name_;\\n _symbol = symbol_;\\n }\\n\\n /**\\n * @dev Returns the name of the token.\\n */\\n function name() public view virtual returns (string memory) {\\n return _name;\\n }\\n\\n /**\\n * @dev Returns the symbol of the token, usually a shorter version of the\\n * name.\\n */\\n function symbol() public view virtual returns (string memory) {\\n return _symbol;\\n }\\n\\n /**\\n * @dev Returns the number of decimals used to get its user representation.\\n * For example, if `decimals` equals `2`, a balance of `505` tokens should\\n * be displayed to a user as `5.05` (`505 / 10 ** 2`).\\n *\\n * Tokens usually opt for a value of 18, imitating the relationship between\\n * Ether and Wei. This is the default value returned by this function, unless\\n * it's overridden.\\n *\\n * NOTE: This information is only used for _display_ purposes: it in\\n * no way affects any of the arithmetic of the contract, including\\n * {IERC20-balanceOf} and {IERC20-transfer}.\\n */\\n function decimals() public view virtual returns (uint8) {\\n return 18;\\n }\\n\\n /**\\n * @dev See {IERC20-totalSupply}.\\n */\\n function totalSupply() public view virtual returns (uint256) {\\n return _totalSupply;\\n }\\n\\n /**\\n * @dev See {IERC20-balanceOf}.\\n */\\n function balanceOf(address account) public view virtual returns (uint256) {\\n return _balances[account];\\n }\\n\\n /**\\n * @dev See {IERC20-transfer}.\\n *\\n * Requirements:\\n *\\n * - `to` cannot be the zero address.\\n * - the caller must have a balance of at least `value`.\\n */\\n function transfer(address to, uint256 value) public virtual returns (bool) {\\n address owner = _msgSender();\\n _transfer(owner, to, value);\\n return true;\\n }\\n\\n /**\\n * @dev See {IERC20-allowance}.\\n */\\n function allowance(address owner, address spender) public view virtual returns (uint256) {\\n return _allowances[owner][spender];\\n }\\n\\n /**\\n * @dev See {IERC20-approve}.\\n *\\n * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on\\n * `transferFrom`. This is semantically equivalent to an infinite approval.\\n *\\n * Requirements:\\n *\\n * - `spender` cannot be the zero address.\\n */\\n function approve(address spender, uint256 value) public virtual returns (bool) {\\n address owner = _msgSender();\\n _approve(owner, spender, value);\\n return true;\\n }\\n\\n /**\\n * @dev See {IERC20-transferFrom}.\\n *\\n * Emits an {Approval} event indicating the updated allowance. This is not\\n * required by the EIP. See the note at the beginning of {ERC20}.\\n *\\n * NOTE: Does not update the allowance if the current allowance\\n * is the maximum `uint256`.\\n *\\n * Requirements:\\n *\\n * - `from` and `to` cannot be the zero address.\\n * - `from` must have a balance of at least `value`.\\n * - the caller must have allowance for ``from``'s tokens of at least\\n * `value`.\\n */\\n function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {\\n address spender = _msgSender();\\n _spendAllowance(from, spender, value);\\n _transfer(from, to, value);\\n return true;\\n }\\n\\n /**\\n * @dev Moves a `value` amount of tokens from `from` to `to`.\\n *\\n * This internal function is equivalent to {transfer}, and can be used to\\n * e.g. implement automatic token fees, slashing mechanisms, etc.\\n *\\n * Emits a {Transfer} event.\\n *\\n * NOTE: This function is not virtual, {_update} should be overridden instead.\\n */\\n function _transfer(address from, address to, uint256 value) internal {\\n if (from == address(0)) {\\n revert ERC20InvalidSender(address(0));\\n }\\n if (to == address(0)) {\\n revert ERC20InvalidReceiver(address(0));\\n }\\n _update(from, to, value);\\n }\\n\\n /**\\n * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`\\n * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding\\n * this function.\\n *\\n * Emits a {Transfer} event.\\n */\\n function _update(address from, address to, uint256 value) internal virtual {\\n if (from == address(0)) {\\n // Overflow check required: The rest of the code assumes that totalSupply never overflows\\n _totalSupply += value;\\n } else {\\n uint256 fromBalance = _balances[from];\\n if (fromBalance < value) {\\n revert ERC20InsufficientBalance(from, fromBalance, value);\\n }\\n unchecked {\\n // Overflow not possible: value <= fromBalance <= totalSupply.\\n _balances[from] = fromBalance - value;\\n }\\n }\\n\\n if (to == address(0)) {\\n unchecked {\\n // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.\\n _totalSupply -= value;\\n }\\n } else {\\n unchecked {\\n // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.\\n _balances[to] += value;\\n }\\n }\\n\\n emit Transfer(from, to, value);\\n }\\n\\n /**\\n * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).\\n * Relies on the `_update` mechanism\\n *\\n * Emits a {Transfer} event with `from` set to the zero address.\\n *\\n * NOTE: This function is not virtual, {_update} should be overridden instead.\\n */\\n function _mint(address account, uint256 value) internal {\\n if (account == address(0)) {\\n revert ERC20InvalidReceiver(address(0));\\n }\\n _update(address(0), account, value);\\n }\\n\\n /**\\n * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.\\n * Relies on the `_update` mechanism.\\n *\\n * Emits a {Transfer} event with `to` set to the zero address.\\n *\\n * NOTE: This function is not virtual, {_update} should be overridden instead\\n */\\n function _burn(address account, uint256 value) internal {\\n if (account == address(0)) {\\n revert ERC20InvalidSender(address(0));\\n }\\n _update(account, address(0), value);\\n }\\n\\n /**\\n * @dev Sets `value` as the allowance of `spender` over the `owner` s tokens.\\n *\\n * This internal function is equivalent to `approve`, and can be used to\\n * e.g. set automatic allowances for certain subsystems, etc.\\n *\\n * Emits an {Approval} event.\\n *\\n * Requirements:\\n *\\n * - `owner` cannot be the zero address.\\n * - `spender` cannot be the zero address.\\n *\\n * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.\\n */\\n function _approve(address owner, address spender, uint256 value) internal {\\n _approve(owner, spender, value, true);\\n }\\n\\n /**\\n * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.\\n *\\n * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by\\n * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any\\n * `Approval` event during `transferFrom` operations.\\n *\\n * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to\\n * true using the following override:\\n * ```\\n * function _approve(address owner, address spender, uint256 value, bool) internal virtual override {\\n * super._approve(owner, spender, value, true);\\n * }\\n * ```\\n *\\n * Requirements are the same as {_approve}.\\n */\\n function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {\\n if (owner == address(0)) {\\n revert ERC20InvalidApprover(address(0));\\n }\\n if (spender == address(0)) {\\n revert ERC20InvalidSpender(address(0));\\n }\\n _allowances[owner][spender] = value;\\n if (emitEvent) {\\n emit Approval(owner, spender, value);\\n }\\n }\\n\\n /**\\n * @dev Updates `owner` s allowance for `spender` based on spent `value`.\\n *\\n * Does not update the allowance value in case of infinite allowance.\\n * Revert if not enough allowance is available.\\n *\\n * Does not emit an {Approval} event.\\n */\\n function _spendAllowance(address owner, address spender, uint256 value) internal virtual {\\n uint256 currentAllowance = allowance(owner, spender);\\n if (currentAllowance != type(uint256).max) {\\n if (currentAllowance < value) {\\n revert ERC20InsufficientAllowance(spender, currentAllowance, value);\\n }\\n unchecked {\\n _approve(owner, spender, currentAllowance - value, false);\\n }\\n }\\n }\\n}\\n\",\"keccak256\":\"0xc3e1fa9d1987f8d349dfb4d6fe93bf2ca014b52ba335cfac30bfe71e357e6f80\",\"license\":\"MIT\"},\"@openzeppelin/contracts/token/ERC20/IERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)\\n\\npragma solidity ^0.8.20;\\n\\n/**\\n * @dev Interface of the ERC20 standard as defined in the EIP.\\n */\\ninterface IERC20 {\\n /**\\n * @dev Emitted when `value` tokens are moved from one account (`from`) to\\n * another (`to`).\\n *\\n * Note that `value` may be zero.\\n */\\n event Transfer(address indexed from, address indexed to, uint256 value);\\n\\n /**\\n * @dev Emitted when the allowance of a `spender` for an `owner` is set by\\n * a call to {approve}. `value` is the new allowance.\\n */\\n event Approval(address indexed owner, address indexed spender, uint256 value);\\n\\n /**\\n * @dev Returns the value of tokens in existence.\\n */\\n function totalSupply() external view returns (uint256);\\n\\n /**\\n * @dev Returns the value of tokens owned by `account`.\\n */\\n function balanceOf(address account) external view returns (uint256);\\n\\n /**\\n * @dev Moves a `value` amount of tokens from the caller's account to `to`.\\n *\\n * Returns a boolean value indicating whether the operation succeeded.\\n *\\n * Emits a {Transfer} event.\\n */\\n function transfer(address to, uint256 value) external returns (bool);\\n\\n /**\\n * @dev Returns the remaining number of tokens that `spender` will be\\n * allowed to spend on behalf of `owner` through {transferFrom}. This is\\n * zero by default.\\n *\\n * This value changes when {approve} or {transferFrom} are called.\\n */\\n function allowance(address owner, address spender) external view returns (uint256);\\n\\n /**\\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\\n * caller's tokens.\\n *\\n * Returns a boolean value indicating whether the operation succeeded.\\n *\\n * IMPORTANT: Beware that changing an allowance with this method brings the risk\\n * that someone may use both the old and the new allowance by unfortunate\\n * transaction ordering. One possible solution to mitigate this race\\n * condition is to first reduce the spender's allowance to 0 and set the\\n * desired value afterwards:\\n * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\\n *\\n * Emits an {Approval} event.\\n */\\n function approve(address spender, uint256 value) external returns (bool);\\n\\n /**\\n * @dev Moves a `value` amount of tokens from `from` to `to` using the\\n * allowance mechanism. `value` is then deducted from the caller's\\n * allowance.\\n *\\n * Returns a boolean value indicating whether the operation succeeded.\\n *\\n * Emits a {Transfer} event.\\n */\\n function transferFrom(address from, address to, uint256 value) external returns (bool);\\n}\\n\",\"keccak256\":\"0xc6a8ff0ea489379b61faa647490411b80102578440ab9d84e9a957cc12164e70\",\"license\":\"MIT\"},\"@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC4626.sol)\\n\\npragma solidity ^0.8.20;\\n\\nimport {IERC20, IERC20Metadata, ERC20} from \\\"../ERC20.sol\\\";\\nimport {SafeERC20} from \\\"../utils/SafeERC20.sol\\\";\\nimport {IERC4626} from \\\"../../../interfaces/IERC4626.sol\\\";\\nimport {Math} from \\\"../../../utils/math/Math.sol\\\";\\n\\n/**\\n * @dev Implementation of the ERC4626 \\\"Tokenized Vault Standard\\\" as defined in\\n * https://eips.ethereum.org/EIPS/eip-4626[EIP-4626].\\n *\\n * This extension allows the minting and burning of \\\"shares\\\" (represented using the ERC20 inheritance) in exchange for\\n * underlying \\\"assets\\\" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends\\n * the ERC20 standard. Any additional extensions included along it would affect the \\\"shares\\\" token represented by this\\n * contract and not the \\\"assets\\\" token which is an independent contract.\\n *\\n * [CAUTION]\\n * ====\\n * In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen through frontrunning\\n * with a \\\"donation\\\" to the vault that inflates the price of a share. This is variously known as a donation or inflation\\n * attack and is essentially a problem of slippage. Vault deployers can protect against this attack by making an initial\\n * deposit of a non-trivial amount of the asset, such that price manipulation becomes infeasible. Withdrawals may\\n * similarly be affected by slippage. Users can protect against this attack as well as unexpected slippage in general by\\n * verifying the amount received is as expected, using a wrapper that performs these checks such as\\n * https://github.com/fei-protocol/ERC4626#erc4626router-and-base[ERC4626Router].\\n *\\n * Since v4.9, this implementation uses virtual assets and shares to mitigate that risk. The `_decimalsOffset()`\\n * corresponds to an offset in the decimal representation between the underlying asset's decimals and the vault\\n * decimals. This offset also determines the rate of virtual shares to virtual assets in the vault, which itself\\n * determines the initial exchange rate. While not fully preventing the attack, analysis shows that the default offset\\n * (0) makes it non-profitable, as a result of the value being captured by the virtual shares (out of the attacker's\\n * donation) matching the attacker's expected gains. With a larger offset, the attack becomes orders of magnitude more\\n * expensive than it is profitable. More details about the underlying math can be found\\n * xref:erc4626.adoc#inflation-attack[here].\\n *\\n * The drawback of this approach is that the virtual shares do capture (a very small) part of the value being accrued\\n * to the vault. Also, if the vault experiences losses, the users try to exit the vault, the virtual shares and assets\\n * will cause the first user to exit to experience reduced losses in detriment to the last users that will experience\\n * bigger losses. Developers willing to revert back to the pre-v4.9 behavior just need to override the\\n * `_convertToShares` and `_convertToAssets` functions.\\n *\\n * To learn more, check out our xref:ROOT:erc4626.adoc[ERC-4626 guide].\\n * ====\\n */\\nabstract contract ERC4626 is ERC20, IERC4626 {\\n using Math for uint256;\\n\\n IERC20 private immutable _asset;\\n uint8 private immutable _underlyingDecimals;\\n\\n /**\\n * @dev Attempted to deposit more assets than the max amount for `receiver`.\\n */\\n error ERC4626ExceededMaxDeposit(address receiver, uint256 assets, uint256 max);\\n\\n /**\\n * @dev Attempted to mint more shares than the max amount for `receiver`.\\n */\\n error ERC4626ExceededMaxMint(address receiver, uint256 shares, uint256 max);\\n\\n /**\\n * @dev Attempted to withdraw more assets than the max amount for `receiver`.\\n */\\n error ERC4626ExceededMaxWithdraw(address owner, uint256 assets, uint256 max);\\n\\n /**\\n * @dev Attempted to redeem more shares than the max amount for `receiver`.\\n */\\n error ERC4626ExceededMaxRedeem(address owner, uint256 shares, uint256 max);\\n\\n /**\\n * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777).\\n */\\n constructor(IERC20 asset_) {\\n (bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);\\n _underlyingDecimals = success ? assetDecimals : 18;\\n _asset = asset_;\\n }\\n\\n /**\\n * @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way.\\n */\\n function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool, uint8) {\\n (bool success, bytes memory encodedDecimals) = address(asset_).staticcall(\\n abi.encodeCall(IERC20Metadata.decimals, ())\\n );\\n if (success && encodedDecimals.length >= 32) {\\n uint256 returnedDecimals = abi.decode(encodedDecimals, (uint256));\\n if (returnedDecimals <= type(uint8).max) {\\n return (true, uint8(returnedDecimals));\\n }\\n }\\n return (false, 0);\\n }\\n\\n /**\\n * @dev Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This\\n * \\\"original\\\" value is cached during construction of the vault contract. If this read operation fails (e.g., the\\n * asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals.\\n *\\n * See {IERC20Metadata-decimals}.\\n */\\n function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) {\\n return _underlyingDecimals + _decimalsOffset();\\n }\\n\\n /** @dev See {IERC4626-asset}. */\\n function asset() public view virtual returns (address) {\\n return address(_asset);\\n }\\n\\n /** @dev See {IERC4626-totalAssets}. */\\n function totalAssets() public view virtual returns (uint256) {\\n return _asset.balanceOf(address(this));\\n }\\n\\n /** @dev See {IERC4626-convertToShares}. */\\n function convertToShares(uint256 assets) public view virtual returns (uint256) {\\n return _convertToShares(assets, Math.Rounding.Floor);\\n }\\n\\n /** @dev See {IERC4626-convertToAssets}. */\\n function convertToAssets(uint256 shares) public view virtual returns (uint256) {\\n return _convertToAssets(shares, Math.Rounding.Floor);\\n }\\n\\n /** @dev See {IERC4626-maxDeposit}. */\\n function maxDeposit(address) public view virtual returns (uint256) {\\n return type(uint256).max;\\n }\\n\\n /** @dev See {IERC4626-maxMint}. */\\n function maxMint(address) public view virtual returns (uint256) {\\n return type(uint256).max;\\n }\\n\\n /** @dev See {IERC4626-maxWithdraw}. */\\n function maxWithdraw(address owner) public view virtual returns (uint256) {\\n return _convertToAssets(balanceOf(owner), Math.Rounding.Floor);\\n }\\n\\n /** @dev See {IERC4626-maxRedeem}. */\\n function maxRedeem(address owner) public view virtual returns (uint256) {\\n return balanceOf(owner);\\n }\\n\\n /** @dev See {IERC4626-previewDeposit}. */\\n function previewDeposit(uint256 assets) public view virtual returns (uint256) {\\n return _convertToShares(assets, Math.Rounding.Floor);\\n }\\n\\n /** @dev See {IERC4626-previewMint}. */\\n function previewMint(uint256 shares) public view virtual returns (uint256) {\\n return _convertToAssets(shares, Math.Rounding.Ceil);\\n }\\n\\n /** @dev See {IERC4626-previewWithdraw}. */\\n function previewWithdraw(uint256 assets) public view virtual returns (uint256) {\\n return _convertToShares(assets, Math.Rounding.Ceil);\\n }\\n\\n /** @dev See {IERC4626-previewRedeem}. */\\n function previewRedeem(uint256 shares) public view virtual returns (uint256) {\\n return _convertToAssets(shares, Math.Rounding.Floor);\\n }\\n\\n /** @dev See {IERC4626-deposit}. */\\n function deposit(uint256 assets, address receiver) public virtual returns (uint256) {\\n uint256 maxAssets = maxDeposit(receiver);\\n if (assets > maxAssets) {\\n revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets);\\n }\\n\\n uint256 shares = previewDeposit(assets);\\n _deposit(_msgSender(), receiver, assets, shares);\\n\\n return shares;\\n }\\n\\n /** @dev See {IERC4626-mint}.\\n *\\n * As opposed to {deposit}, minting is allowed even if the vault is in a state where the price of a share is zero.\\n * In this case, the shares will be minted without requiring any assets to be deposited.\\n */\\n function mint(uint256 shares, address receiver) public virtual returns (uint256) {\\n uint256 maxShares = maxMint(receiver);\\n if (shares > maxShares) {\\n revert ERC4626ExceededMaxMint(receiver, shares, maxShares);\\n }\\n\\n uint256 assets = previewMint(shares);\\n _deposit(_msgSender(), receiver, assets, shares);\\n\\n return assets;\\n }\\n\\n /** @dev See {IERC4626-withdraw}. */\\n function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) {\\n uint256 maxAssets = maxWithdraw(owner);\\n if (assets > maxAssets) {\\n revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);\\n }\\n\\n uint256 shares = previewWithdraw(assets);\\n _withdraw(_msgSender(), receiver, owner, assets, shares);\\n\\n return shares;\\n }\\n\\n /** @dev See {IERC4626-redeem}. */\\n function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) {\\n uint256 maxShares = maxRedeem(owner);\\n if (shares > maxShares) {\\n revert ERC4626ExceededMaxRedeem(owner, shares, maxShares);\\n }\\n\\n uint256 assets = previewRedeem(shares);\\n _withdraw(_msgSender(), receiver, owner, assets, shares);\\n\\n return assets;\\n }\\n\\n /**\\n * @dev Internal conversion function (from assets to shares) with support for rounding direction.\\n */\\n function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {\\n return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding);\\n }\\n\\n /**\\n * @dev Internal conversion function (from shares to assets) with support for rounding direction.\\n */\\n function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) {\\n return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding);\\n }\\n\\n /**\\n * @dev Deposit/mint common workflow.\\n */\\n function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual {\\n // If _asset is ERC777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the\\n // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,\\n // calls the vault, which is assumed not malicious.\\n //\\n // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the\\n // assets are transferred and before the shares are minted, which is a valid state.\\n // slither-disable-next-line reentrancy-no-eth\\n SafeERC20.safeTransferFrom(_asset, caller, address(this), assets);\\n _mint(receiver, shares);\\n\\n emit Deposit(caller, receiver, assets, shares);\\n }\\n\\n /**\\n * @dev Withdraw/redeem common workflow.\\n */\\n function _withdraw(\\n address caller,\\n address receiver,\\n address owner,\\n uint256 assets,\\n uint256 shares\\n ) internal virtual {\\n if (caller != owner) {\\n _spendAllowance(owner, caller, shares);\\n }\\n\\n // If _asset is ERC777, `transfer` can trigger a reentrancy AFTER the transfer happens through the\\n // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,\\n // calls the vault, which is assumed not malicious.\\n //\\n // Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the\\n // shares are burned and after the assets are transferred, which is a valid state.\\n _burn(owner, shares);\\n SafeERC20.safeTransfer(_asset, receiver, assets);\\n\\n emit Withdraw(caller, receiver, owner, assets, shares);\\n }\\n\\n function _decimalsOffset() internal view virtual returns (uint8) {\\n return 0;\\n }\\n}\\n\",\"keccak256\":\"0x1837547e04d5fe5334eeb77a345683c22995f1e7aa033020757ddf83a80fc72d\",\"license\":\"MIT\"},\"@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Metadata.sol)\\n\\npragma solidity ^0.8.20;\\n\\nimport {IERC20} from \\\"../IERC20.sol\\\";\\n\\n/**\\n * @dev Interface for the optional metadata functions from the ERC20 standard.\\n */\\ninterface IERC20Metadata is IERC20 {\\n /**\\n * @dev Returns the name of the token.\\n */\\n function name() external view returns (string memory);\\n\\n /**\\n * @dev Returns the symbol of the token.\\n */\\n function symbol() external view returns (string memory);\\n\\n /**\\n * @dev Returns the decimals places of the token.\\n */\\n function decimals() external view returns (uint8);\\n}\\n\",\"keccak256\":\"0xaa761817f6cd7892fcf158b3c776b34551cde36f48ff9703d53898bc45a94ea2\",\"license\":\"MIT\"},\"@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Permit.sol)\\n\\npragma solidity ^0.8.20;\\n\\n/**\\n * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in\\n * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612].\\n *\\n * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by\\n * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't\\n * need to send a transaction, and thus is not required to hold Ether at all.\\n *\\n * ==== Security Considerations\\n *\\n * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature\\n * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be\\n * considered as an intention to spend the allowance in any specific way. The second is that because permits have\\n * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should\\n * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be\\n * generally recommended is:\\n *\\n * ```solidity\\n * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public {\\n * try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {}\\n * doThing(..., value);\\n * }\\n *\\n * function doThing(..., uint256 value) public {\\n * token.safeTransferFrom(msg.sender, address(this), value);\\n * ...\\n * }\\n * ```\\n *\\n * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of\\n * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also\\n * {SafeERC20-safeTransferFrom}).\\n *\\n * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so\\n * contracts should have entry points that don't rely on permit.\\n */\\ninterface IERC20Permit {\\n /**\\n * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens,\\n * given ``owner``'s signed approval.\\n *\\n * IMPORTANT: The same issues {IERC20-approve} has related to transaction\\n * ordering also apply here.\\n *\\n * Emits an {Approval} event.\\n *\\n * Requirements:\\n *\\n * - `spender` cannot be the zero address.\\n * - `deadline` must be a timestamp in the future.\\n * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`\\n * over the EIP712-formatted function arguments.\\n * - the signature must use ``owner``'s current nonce (see {nonces}).\\n *\\n * For more information on the signature format, see the\\n * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP\\n * section].\\n *\\n * CAUTION: See Security Considerations above.\\n */\\n function permit(\\n address owner,\\n address spender,\\n uint256 value,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) external;\\n\\n /**\\n * @dev Returns the current nonce for `owner`. This value must be\\n * included whenever a signature is generated for {permit}.\\n *\\n * Every successful call to {permit} increases ``owner``'s nonce by one. This\\n * prevents a signature from being used multiple times.\\n */\\n function nonces(address owner) external view returns (uint256);\\n\\n /**\\n * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}.\\n */\\n // solhint-disable-next-line func-name-mixedcase\\n function DOMAIN_SEPARATOR() external view returns (bytes32);\\n}\\n\",\"keccak256\":\"0x6008dabfe393240d73d7dd7688033f72740d570aa422254d29a7dce8568f3aff\",\"license\":\"MIT\"},\"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/utils/SafeERC20.sol)\\n\\npragma solidity ^0.8.20;\\n\\nimport {IERC20} from \\\"../IERC20.sol\\\";\\nimport {IERC20Permit} from \\\"../extensions/IERC20Permit.sol\\\";\\nimport {Address} from \\\"../../../utils/Address.sol\\\";\\n\\n/**\\n * @title SafeERC20\\n * @dev Wrappers around ERC20 operations that throw on failure (when the token\\n * contract returns false). Tokens that return no value (and instead revert or\\n * throw on failure) are also supported, non-reverting calls are assumed to be\\n * successful.\\n * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,\\n * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.\\n */\\nlibrary SafeERC20 {\\n using Address for address;\\n\\n /**\\n * @dev An operation with an ERC20 token failed.\\n */\\n error SafeERC20FailedOperation(address token);\\n\\n /**\\n * @dev Indicates a failed `decreaseAllowance` request.\\n */\\n error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);\\n\\n /**\\n * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,\\n * non-reverting calls are assumed to be successful.\\n */\\n function safeTransfer(IERC20 token, address to, uint256 value) internal {\\n _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));\\n }\\n\\n /**\\n * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the\\n * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.\\n */\\n function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {\\n _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));\\n }\\n\\n /**\\n * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,\\n * non-reverting calls are assumed to be successful.\\n */\\n function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {\\n uint256 oldAllowance = token.allowance(address(this), spender);\\n forceApprove(token, spender, oldAllowance + value);\\n }\\n\\n /**\\n * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no\\n * value, non-reverting calls are assumed to be successful.\\n */\\n function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {\\n unchecked {\\n uint256 currentAllowance = token.allowance(address(this), spender);\\n if (currentAllowance < requestedDecrease) {\\n revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);\\n }\\n forceApprove(token, spender, currentAllowance - requestedDecrease);\\n }\\n }\\n\\n /**\\n * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,\\n * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval\\n * to be set to zero before setting it to a non-zero value, such as USDT.\\n */\\n function forceApprove(IERC20 token, address spender, uint256 value) internal {\\n bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));\\n\\n if (!_callOptionalReturnBool(token, approvalCall)) {\\n _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));\\n _callOptionalReturn(token, approvalCall);\\n }\\n }\\n\\n /**\\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\\n * on the return value: the return value is optional (but if data is returned, it must not be false).\\n * @param token The token targeted by the call.\\n * @param data The call data (encoded using abi.encode or one of its variants).\\n */\\n function _callOptionalReturn(IERC20 token, bytes memory data) private {\\n // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since\\n // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that\\n // the target address contains contract code and also asserts for success in the low-level call.\\n\\n bytes memory returndata = address(token).functionCall(data);\\n if (returndata.length != 0 && !abi.decode(returndata, (bool))) {\\n revert SafeERC20FailedOperation(address(token));\\n }\\n }\\n\\n /**\\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\\n * on the return value: the return value is optional (but if data is returned, it must not be false).\\n * @param token The token targeted by the call.\\n * @param data The call data (encoded using abi.encode or one of its variants).\\n *\\n * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead.\\n */\\n function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {\\n // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since\\n // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false\\n // and not revert is the subcall reverts.\\n\\n (bool success, bytes memory returndata) = address(token).call(data);\\n return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0;\\n }\\n}\\n\",\"keccak256\":\"0x37bb49513c49c87c4642a891b13b63571bc87013dde806617aa1efb54605f386\",\"license\":\"MIT\"},\"@openzeppelin/contracts/utils/Address.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol)\\n\\npragma solidity ^0.8.20;\\n\\n/**\\n * @dev Collection of functions related to the address type\\n */\\nlibrary Address {\\n /**\\n * @dev The ETH balance of the account is not enough to perform the operation.\\n */\\n error AddressInsufficientBalance(address account);\\n\\n /**\\n * @dev There's no code at `target` (it is not a contract).\\n */\\n error AddressEmptyCode(address target);\\n\\n /**\\n * @dev A call to an address target failed. The target may have reverted.\\n */\\n error FailedInnerCall();\\n\\n /**\\n * @dev Replacement for Solidity's `transfer`: sends `amount` wei to\\n * `recipient`, forwarding all available gas and reverting on errors.\\n *\\n * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost\\n * of certain opcodes, possibly making contracts go over the 2300 gas limit\\n * imposed by `transfer`, making them unable to receive funds via\\n * `transfer`. {sendValue} removes this limitation.\\n *\\n * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].\\n *\\n * IMPORTANT: because control is transferred to `recipient`, care must be\\n * taken to not create reentrancy vulnerabilities. Consider using\\n * {ReentrancyGuard} or the\\n * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].\\n */\\n function sendValue(address payable recipient, uint256 amount) internal {\\n if (address(this).balance < amount) {\\n revert AddressInsufficientBalance(address(this));\\n }\\n\\n (bool success, ) = recipient.call{value: amount}(\\\"\\\");\\n if (!success) {\\n revert FailedInnerCall();\\n }\\n }\\n\\n /**\\n * @dev Performs a Solidity function call using a low level `call`. A\\n * plain `call` is an unsafe replacement for a function call: use this\\n * function instead.\\n *\\n * If `target` reverts with a revert reason or custom error, it is bubbled\\n * up by this function (like regular Solidity function calls). However, if\\n * the call reverted with no returned reason, this function reverts with a\\n * {FailedInnerCall} error.\\n *\\n * Returns the raw returned data. To convert to the expected return value,\\n * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].\\n *\\n * Requirements:\\n *\\n * - `target` must be a contract.\\n * - calling `target` with `data` must not revert.\\n */\\n function functionCall(address target, bytes memory data) internal returns (bytes memory) {\\n return functionCallWithValue(target, data, 0);\\n }\\n\\n /**\\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],\\n * but also transferring `value` wei to `target`.\\n *\\n * Requirements:\\n *\\n * - the calling contract must have an ETH balance of at least `value`.\\n * - the called Solidity function must be `payable`.\\n */\\n function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {\\n if (address(this).balance < value) {\\n revert AddressInsufficientBalance(address(this));\\n }\\n (bool success, bytes memory returndata) = target.call{value: value}(data);\\n return verifyCallResultFromTarget(target, success, returndata);\\n }\\n\\n /**\\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],\\n * but performing a static call.\\n */\\n function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {\\n (bool success, bytes memory returndata) = target.staticcall(data);\\n return verifyCallResultFromTarget(target, success, returndata);\\n }\\n\\n /**\\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],\\n * but performing a delegate call.\\n */\\n function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {\\n (bool success, bytes memory returndata) = target.delegatecall(data);\\n return verifyCallResultFromTarget(target, success, returndata);\\n }\\n\\n /**\\n * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target\\n * was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an\\n * unsuccessful call.\\n */\\n function verifyCallResultFromTarget(\\n address target,\\n bool success,\\n bytes memory returndata\\n ) internal view returns (bytes memory) {\\n if (!success) {\\n _revert(returndata);\\n } else {\\n // only check if target is a contract if the call was successful and the return data is empty\\n // otherwise we already know that it was a contract\\n if (returndata.length == 0 && target.code.length == 0) {\\n revert AddressEmptyCode(target);\\n }\\n return returndata;\\n }\\n }\\n\\n /**\\n * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the\\n * revert reason or with a default {FailedInnerCall} error.\\n */\\n function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) {\\n if (!success) {\\n _revert(returndata);\\n } else {\\n return returndata;\\n }\\n }\\n\\n /**\\n * @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}.\\n */\\n function _revert(bytes memory returndata) private pure {\\n // Look for revert reason and bubble it up if present\\n if (returndata.length > 0) {\\n // The easiest way to bubble the revert reason is using memory via assembly\\n /// @solidity memory-safe-assembly\\n assembly {\\n let returndata_size := mload(returndata)\\n revert(add(32, returndata), returndata_size)\\n }\\n } else {\\n revert FailedInnerCall();\\n }\\n }\\n}\\n\",\"keccak256\":\"0xaf28a975a78550e45f65e559a3ad6a5ad43b9b8a37366999abd1b7084eb70721\",\"license\":\"MIT\"},\"@openzeppelin/contracts/utils/Context.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// OpenZeppelin Contracts (last updated v5.0.0) (utils/Context.sol)\\n\\npragma solidity ^0.8.20;\\n\\n/**\\n * @dev Provides information about the current execution context, including the\\n * sender of the transaction and its data. While these are generally available\\n * via msg.sender and msg.data, they should not be accessed in such a direct\\n * manner, since when dealing with meta-transactions the account sending and\\n * paying for execution may not be the actual sender (as far as an application\\n * is concerned).\\n *\\n * This contract is only required for intermediate, library-like contracts.\\n */\\nabstract contract Context {\\n function _msgSender() internal view virtual returns (address) {\\n return msg.sender;\\n }\\n\\n function _msgData() internal view virtual returns (bytes calldata) {\\n return msg.data;\\n }\\n}\\n\",\"keccak256\":\"0x75a4ee64c68dbd5f38bddd06e664a64c8271b4caa554fb6f0607dfd672bb4bf3\",\"license\":\"MIT\"},\"@openzeppelin/contracts/utils/math/Math.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/Math.sol)\\n\\npragma solidity ^0.8.20;\\n\\n/**\\n * @dev Standard math utilities missing in the Solidity language.\\n */\\nlibrary Math {\\n /**\\n * @dev Muldiv operation overflow.\\n */\\n error MathOverflowedMulDiv();\\n\\n enum Rounding {\\n Floor, // Toward negative infinity\\n Ceil, // Toward positive infinity\\n Trunc, // Toward zero\\n Expand // Away from zero\\n }\\n\\n /**\\n * @dev Returns the addition of two unsigned integers, with an overflow flag.\\n */\\n function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) {\\n unchecked {\\n uint256 c = a + b;\\n if (c < a) return (false, 0);\\n return (true, c);\\n }\\n }\\n\\n /**\\n * @dev Returns the subtraction of two unsigned integers, with an overflow flag.\\n */\\n function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) {\\n unchecked {\\n if (b > a) return (false, 0);\\n return (true, a - b);\\n }\\n }\\n\\n /**\\n * @dev Returns the multiplication of two unsigned integers, with an overflow flag.\\n */\\n function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) {\\n unchecked {\\n // Gas optimization: this is cheaper than requiring 'a' not being zero, but the\\n // benefit is lost if 'b' is also tested.\\n // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522\\n if (a == 0) return (true, 0);\\n uint256 c = a * b;\\n if (c / a != b) return (false, 0);\\n return (true, c);\\n }\\n }\\n\\n /**\\n * @dev Returns the division of two unsigned integers, with a division by zero flag.\\n */\\n function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) {\\n unchecked {\\n if (b == 0) return (false, 0);\\n return (true, a / b);\\n }\\n }\\n\\n /**\\n * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag.\\n */\\n function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) {\\n unchecked {\\n if (b == 0) return (false, 0);\\n return (true, a % b);\\n }\\n }\\n\\n /**\\n * @dev Returns the largest of two numbers.\\n */\\n function max(uint256 a, uint256 b) internal pure returns (uint256) {\\n return a > b ? a : b;\\n }\\n\\n /**\\n * @dev Returns the smallest of two numbers.\\n */\\n function min(uint256 a, uint256 b) internal pure returns (uint256) {\\n return a < b ? a : b;\\n }\\n\\n /**\\n * @dev Returns the average of two numbers. The result is rounded towards\\n * zero.\\n */\\n function average(uint256 a, uint256 b) internal pure returns (uint256) {\\n // (a + b) / 2 can overflow.\\n return (a & b) + (a ^ b) / 2;\\n }\\n\\n /**\\n * @dev Returns the ceiling of the division of two numbers.\\n *\\n * This differs from standard division with `/` in that it rounds towards infinity instead\\n * of rounding towards zero.\\n */\\n function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {\\n if (b == 0) {\\n // Guarantee the same behavior as in a regular Solidity division.\\n return a / b;\\n }\\n\\n // (a + b - 1) / b can overflow on addition, so we distribute.\\n return a == 0 ? 0 : (a - 1) / b + 1;\\n }\\n\\n /**\\n * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or\\n * denominator == 0.\\n * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by\\n * Uniswap Labs also under MIT license.\\n */\\n function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {\\n unchecked {\\n // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use\\n // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256\\n // variables such that product = prod1 * 2^256 + prod0.\\n uint256 prod0 = x * y; // Least significant 256 bits of the product\\n uint256 prod1; // Most significant 256 bits of the product\\n assembly {\\n let mm := mulmod(x, y, not(0))\\n prod1 := sub(sub(mm, prod0), lt(mm, prod0))\\n }\\n\\n // Handle non-overflow cases, 256 by 256 division.\\n if (prod1 == 0) {\\n // Solidity will revert if denominator == 0, unlike the div opcode on its own.\\n // The surrounding unchecked block does not change this fact.\\n // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.\\n return prod0 / denominator;\\n }\\n\\n // Make sure the result is less than 2^256. Also prevents denominator == 0.\\n if (denominator <= prod1) {\\n revert MathOverflowedMulDiv();\\n }\\n\\n ///////////////////////////////////////////////\\n // 512 by 256 division.\\n ///////////////////////////////////////////////\\n\\n // Make division exact by subtracting the remainder from [prod1 prod0].\\n uint256 remainder;\\n assembly {\\n // Compute remainder using mulmod.\\n remainder := mulmod(x, y, denominator)\\n\\n // Subtract 256 bit number from 512 bit number.\\n prod1 := sub(prod1, gt(remainder, prod0))\\n prod0 := sub(prod0, remainder)\\n }\\n\\n // Factor powers of two out of denominator and compute largest power of two divisor of denominator.\\n // Always >= 1. See https://cs.stackexchange.com/q/138556/92363.\\n\\n uint256 twos = denominator & (0 - denominator);\\n assembly {\\n // Divide denominator by twos.\\n denominator := div(denominator, twos)\\n\\n // Divide [prod1 prod0] by twos.\\n prod0 := div(prod0, twos)\\n\\n // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one.\\n twos := add(div(sub(0, twos), twos), 1)\\n }\\n\\n // Shift in bits from prod1 into prod0.\\n prod0 |= prod1 * twos;\\n\\n // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such\\n // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for\\n // four bits. That is, denominator * inv = 1 mod 2^4.\\n uint256 inverse = (3 * denominator) ^ 2;\\n\\n // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also\\n // works in modular arithmetic, doubling the correct bits in each step.\\n inverse *= 2 - denominator * inverse; // inverse mod 2^8\\n inverse *= 2 - denominator * inverse; // inverse mod 2^16\\n inverse *= 2 - denominator * inverse; // inverse mod 2^32\\n inverse *= 2 - denominator * inverse; // inverse mod 2^64\\n inverse *= 2 - denominator * inverse; // inverse mod 2^128\\n inverse *= 2 - denominator * inverse; // inverse mod 2^256\\n\\n // Because the division is now exact we can divide by multiplying with the modular inverse of denominator.\\n // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is\\n // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1\\n // is no longer required.\\n result = prod0 * inverse;\\n return result;\\n }\\n }\\n\\n /**\\n * @notice Calculates x * y / denominator with full precision, following the selected rounding direction.\\n */\\n function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {\\n uint256 result = mulDiv(x, y, denominator);\\n if (unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0) {\\n result += 1;\\n }\\n return result;\\n }\\n\\n /**\\n * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded\\n * towards zero.\\n *\\n * Inspired by Henry S. Warren, Jr.'s \\\"Hacker's Delight\\\" (Chapter 11).\\n */\\n function sqrt(uint256 a) internal pure returns (uint256) {\\n if (a == 0) {\\n return 0;\\n }\\n\\n // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target.\\n //\\n // We know that the \\\"msb\\\" (most significant bit) of our target number `a` is a power of 2 such that we have\\n // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`.\\n //\\n // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)`\\n // \\u2192 `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))`\\n // \\u2192 `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)`\\n //\\n // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit.\\n uint256 result = 1 << (log2(a) >> 1);\\n\\n // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128,\\n // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at\\n // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision\\n // into the expected uint128 result.\\n unchecked {\\n result = (result + a / result) >> 1;\\n result = (result + a / result) >> 1;\\n result = (result + a / result) >> 1;\\n result = (result + a / result) >> 1;\\n result = (result + a / result) >> 1;\\n result = (result + a / result) >> 1;\\n result = (result + a / result) >> 1;\\n return min(result, a / result);\\n }\\n }\\n\\n /**\\n * @notice Calculates sqrt(a), following the selected rounding direction.\\n */\\n function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {\\n unchecked {\\n uint256 result = sqrt(a);\\n return result + (unsignedRoundsUp(rounding) && result * result < a ? 1 : 0);\\n }\\n }\\n\\n /**\\n * @dev Return the log in base 2 of a positive value rounded towards zero.\\n * Returns 0 if given 0.\\n */\\n function log2(uint256 value) internal pure returns (uint256) {\\n uint256 result = 0;\\n unchecked {\\n if (value >> 128 > 0) {\\n value >>= 128;\\n result += 128;\\n }\\n if (value >> 64 > 0) {\\n value >>= 64;\\n result += 64;\\n }\\n if (value >> 32 > 0) {\\n value >>= 32;\\n result += 32;\\n }\\n if (value >> 16 > 0) {\\n value >>= 16;\\n result += 16;\\n }\\n if (value >> 8 > 0) {\\n value >>= 8;\\n result += 8;\\n }\\n if (value >> 4 > 0) {\\n value >>= 4;\\n result += 4;\\n }\\n if (value >> 2 > 0) {\\n value >>= 2;\\n result += 2;\\n }\\n if (value >> 1 > 0) {\\n result += 1;\\n }\\n }\\n return result;\\n }\\n\\n /**\\n * @dev Return the log in base 2, following the selected rounding direction, of a positive value.\\n * Returns 0 if given 0.\\n */\\n function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {\\n unchecked {\\n uint256 result = log2(value);\\n return result + (unsignedRoundsUp(rounding) && 1 << result < value ? 1 : 0);\\n }\\n }\\n\\n /**\\n * @dev Return the log in base 10 of a positive value rounded towards zero.\\n * Returns 0 if given 0.\\n */\\n function log10(uint256 value) internal pure returns (uint256) {\\n uint256 result = 0;\\n unchecked {\\n if (value >= 10 ** 64) {\\n value /= 10 ** 64;\\n result += 64;\\n }\\n if (value >= 10 ** 32) {\\n value /= 10 ** 32;\\n result += 32;\\n }\\n if (value >= 10 ** 16) {\\n value /= 10 ** 16;\\n result += 16;\\n }\\n if (value >= 10 ** 8) {\\n value /= 10 ** 8;\\n result += 8;\\n }\\n if (value >= 10 ** 4) {\\n value /= 10 ** 4;\\n result += 4;\\n }\\n if (value >= 10 ** 2) {\\n value /= 10 ** 2;\\n result += 2;\\n }\\n if (value >= 10 ** 1) {\\n result += 1;\\n }\\n }\\n return result;\\n }\\n\\n /**\\n * @dev Return the log in base 10, following the selected rounding direction, of a positive value.\\n * Returns 0 if given 0.\\n */\\n function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {\\n unchecked {\\n uint256 result = log10(value);\\n return result + (unsignedRoundsUp(rounding) && 10 ** result < value ? 1 : 0);\\n }\\n }\\n\\n /**\\n * @dev Return the log in base 256 of a positive value rounded towards zero.\\n * Returns 0 if given 0.\\n *\\n * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.\\n */\\n function log256(uint256 value) internal pure returns (uint256) {\\n uint256 result = 0;\\n unchecked {\\n if (value >> 128 > 0) {\\n value >>= 128;\\n result += 16;\\n }\\n if (value >> 64 > 0) {\\n value >>= 64;\\n result += 8;\\n }\\n if (value >> 32 > 0) {\\n value >>= 32;\\n result += 4;\\n }\\n if (value >> 16 > 0) {\\n value >>= 16;\\n result += 2;\\n }\\n if (value >> 8 > 0) {\\n result += 1;\\n }\\n }\\n return result;\\n }\\n\\n /**\\n * @dev Return the log in base 256, following the selected rounding direction, of a positive value.\\n * Returns 0 if given 0.\\n */\\n function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {\\n unchecked {\\n uint256 result = log256(value);\\n return result + (unsignedRoundsUp(rounding) && 1 << (result << 3) < value ? 1 : 0);\\n }\\n }\\n\\n /**\\n * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.\\n */\\n function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {\\n return uint8(rounding) % 2 == 1;\\n }\\n}\\n\",\"keccak256\":\"0x005ec64c6313f0555d59e278f9a7a5ab2db5bdc72a027f255a37c327af1ec02d\",\"license\":\"MIT\"},\"contracts/Dispatcher.sol\":{\"content\":\"// SPDX-License-Identifier: GPL-3.0-only\\npragma solidity ^0.8.21;\\n\\nimport \\\"@openzeppelin/contracts/access/Ownable.sol\\\";\\nimport \\\"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\\\";\\nimport \\\"@openzeppelin/contracts/interfaces/IERC4626.sol\\\";\\nimport \\\"./Router.sol\\\";\\nimport \\\"./stBTC.sol\\\";\\n\\n/// @title Dispatcher\\n/// @notice Dispatcher is a contract that routes tBTC from stBTC to\\n/// yield vaults and back. Vaults supply yield strategies with tBTC that\\n/// generate yield for Bitcoin holders.\\ncontract Dispatcher is Router, Ownable {\\n using SafeERC20 for IERC20;\\n\\n /// Struct holds information about a vault.\\n struct VaultInfo {\\n bool authorized;\\n }\\n\\n /// The main stBTC contract holding tBTC deposited by stakers.\\n stBTC public immutable stbtc;\\n /// tBTC token contract.\\n IERC20 public immutable tbtc;\\n /// Address of the maintainer bot.\\n address public maintainer;\\n\\n /// Authorized Yield Vaults that implement ERC4626 standard. These\\n /// vaults deposit assets to yield strategies, e.g. Uniswap V3\\n /// WBTC/TBTC pool. Vault can be a part of Acre ecosystem or can be\\n /// implemented externally. As long as it complies with ERC4626\\n /// standard and is authorized by the owner it can be plugged into\\n /// Acre.\\n address[] public vaults;\\n /// Mapping of vaults to their information.\\n mapping(address => VaultInfo) public vaultsInfo;\\n\\n /// Emitted when a vault is authorized.\\n /// @param vault Address of the vault.\\n event VaultAuthorized(address indexed vault);\\n\\n /// Emitted when a vault is deauthorized.\\n /// @param vault Address of the vault.\\n event VaultDeauthorized(address indexed vault);\\n\\n /// Emitted when tBTC is routed to a vault.\\n /// @param vault Address of the vault.\\n /// @param amount Amount of tBTC.\\n /// @param sharesOut Amount of received shares.\\n event DepositAllocated(\\n address indexed vault,\\n uint256 amount,\\n uint256 sharesOut\\n );\\n\\n /// Emitted when the maintainer address is updated.\\n /// @param maintainer Address of the new maintainer.\\n event MaintainerUpdated(address indexed maintainer);\\n\\n /// Reverts if the vault is already authorized.\\n error VaultAlreadyAuthorized();\\n\\n /// Reverts if the vault is not authorized.\\n error VaultUnauthorized();\\n\\n /// Reverts if the caller is not the maintainer.\\n error NotMaintainer();\\n\\n /// Reverts if the address is zero.\\n error ZeroAddress();\\n\\n /// Modifier that reverts if the caller is not the maintainer.\\n modifier onlyMaintainer() {\\n if (msg.sender != maintainer) {\\n revert NotMaintainer();\\n }\\n _;\\n }\\n\\n constructor(stBTC _stbtc, IERC20 _tbtc) Ownable(msg.sender) {\\n stbtc = _stbtc;\\n tbtc = _tbtc;\\n }\\n\\n /// @notice Adds a vault to the list of authorized vaults.\\n /// @param vault Address of the vault to add.\\n function authorizeVault(address vault) external onlyOwner {\\n if (isVaultAuthorized(vault)) {\\n revert VaultAlreadyAuthorized();\\n }\\n\\n vaults.push(vault);\\n vaultsInfo[vault].authorized = true;\\n\\n emit VaultAuthorized(vault);\\n }\\n\\n /// @notice Removes a vault from the list of authorized vaults.\\n /// @param vault Address of the vault to remove.\\n function deauthorizeVault(address vault) external onlyOwner {\\n if (!isVaultAuthorized(vault)) {\\n revert VaultUnauthorized();\\n }\\n\\n vaultsInfo[vault].authorized = false;\\n\\n for (uint256 i = 0; i < vaults.length; i++) {\\n if (vaults[i] == vault) {\\n vaults[i] = vaults[vaults.length - 1];\\n // slither-disable-next-line costly-loop\\n vaults.pop();\\n break;\\n }\\n }\\n\\n emit VaultDeauthorized(vault);\\n }\\n\\n /// @notice Updates the maintainer address.\\n /// @param newMaintainer Address of the new maintainer.\\n function updateMaintainer(address newMaintainer) external onlyOwner {\\n if (newMaintainer == address(0)) {\\n revert ZeroAddress();\\n }\\n\\n maintainer = newMaintainer;\\n\\n emit MaintainerUpdated(maintainer);\\n }\\n\\n /// TODO: make this function internal once the allocation distribution is\\n /// implemented\\n /// @notice Routes tBTC from stBTC to a vault. Can be called by the maintainer\\n /// only.\\n /// @param vault Address of the vault to route the assets to.\\n /// @param amount Amount of tBTC to deposit.\\n /// @param minSharesOut Minimum amount of shares to receive.\\n function depositToVault(\\n address vault,\\n uint256 amount,\\n uint256 minSharesOut\\n ) public onlyMaintainer {\\n if (!isVaultAuthorized(vault)) {\\n revert VaultUnauthorized();\\n }\\n\\n // slither-disable-next-line arbitrary-send-erc20\\n tbtc.safeTransferFrom(address(stbtc), address(this), amount);\\n tbtc.forceApprove(address(vault), amount);\\n\\n uint256 sharesOut = deposit(\\n IERC4626(vault),\\n address(stbtc),\\n amount,\\n minSharesOut\\n );\\n // slither-disable-next-line reentrancy-events\\n emit DepositAllocated(vault, amount, sharesOut);\\n }\\n\\n /// @notice Returns the list of authorized vaults.\\n function getVaults() public view returns (address[] memory) {\\n return vaults;\\n }\\n\\n /// @notice Returns true if the vault is authorized.\\n /// @param vault Address of the vault to check.\\n function isVaultAuthorized(address vault) public view returns (bool) {\\n return vaultsInfo[vault].authorized;\\n }\\n\\n /// TODO: implement redeem() / withdraw() functions\\n}\\n\",\"keccak256\":\"0x0dd77692ab8c6059afa7b4a90f26733560b53fb66192e9912a2064103c5d41fb\",\"license\":\"GPL-3.0-only\"},\"contracts/Router.sol\":{\"content\":\"// SPDX-License-Identifier: GPL-3.0-only\\npragma solidity ^0.8.21;\\n\\nimport \\\"@openzeppelin/contracts/interfaces/IERC4626.sol\\\";\\n\\n/// @title Router\\n/// @notice Router is a contract that routes tBTC from stBTC to\\n/// a given vault and back. Vaults supply yield strategies with tBTC that\\n/// generate yield for Bitcoin holders.\\nabstract contract Router {\\n /// Thrown when amount of shares received is below the min set by caller.\\n /// @param vault Address of the vault.\\n /// @param sharesOut Amount of received shares.\\n /// @param minSharesOut Minimum amount of shares expected to receive.\\n error MinSharesError(\\n address vault,\\n uint256 sharesOut,\\n uint256 minSharesOut\\n );\\n\\n /// @notice Routes funds from stBTC to a vault. The amount of tBTC to\\n /// Shares of deposited tBTC are minted to the stBTC contract.\\n /// @param vault Address of the vault to route the funds to.\\n /// @param receiver Address of the receiver of the shares.\\n /// @param amount Amount of tBTC to deposit.\\n /// @param minSharesOut Minimum amount of shares to receive.\\n function deposit(\\n IERC4626 vault,\\n address receiver,\\n uint256 amount,\\n uint256 minSharesOut\\n ) internal returns (uint256 sharesOut) {\\n if ((sharesOut = vault.deposit(amount, receiver)) < minSharesOut) {\\n revert MinSharesError(address(vault), sharesOut, minSharesOut);\\n }\\n }\\n}\\n\",\"keccak256\":\"0x520e27c613f1234755c23402524b048a81abd15222d7f318504992d490248812\",\"license\":\"GPL-3.0-only\"},\"contracts/stBTC.sol\":{\"content\":\"// SPDX-License-Identifier: GPL-3.0-only\\npragma solidity ^0.8.21;\\n\\nimport \\\"@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol\\\";\\nimport \\\"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\\\";\\nimport \\\"@openzeppelin/contracts/access/Ownable.sol\\\";\\nimport \\\"./Dispatcher.sol\\\";\\n\\n/// @title stBTC\\n/// @notice This contract implements the ERC-4626 tokenized vault standard. By\\n/// staking tBTC, users acquire a liquid staking token called stBTC,\\n/// commonly referred to as \\\"shares\\\". The staked tBTC is securely\\n/// deposited into Acre's vaults, where it generates yield over time.\\n/// Users have the flexibility to redeem stBTC, enabling them to\\n/// withdraw their staked tBTC along with the accrued yield.\\n/// @dev ERC-4626 is a standard to optimize and unify the technical parameters\\n/// of yield-bearing vaults. This contract facilitates the minting and\\n/// burning of shares (stBTC), which are represented as standard ERC20\\n/// tokens, providing a seamless exchange with tBTC tokens.\\ncontract stBTC is ERC4626, Ownable {\\n using SafeERC20 for IERC20;\\n\\n /// Dispatcher contract that routes tBTC from stBTC to a given vault and back.\\n Dispatcher public dispatcher;\\n\\n /// Address of the treasury wallet, where fees should be transferred to.\\n address public treasury;\\n\\n /// Minimum amount for a single deposit operation.\\n uint256 public minimumDepositAmount;\\n /// Maximum total amount of tBTC token held by Acre protocol.\\n uint256 public maximumTotalAssets;\\n\\n /// Emitted when the treasury wallet address is updated.\\n /// @param treasury New treasury wallet address.\\n event TreasuryUpdated(address treasury);\\n\\n /// Emitted when deposit parameters are updated.\\n /// @param minimumDepositAmount New value of the minimum deposit amount.\\n /// @param maximumTotalAssets New value of the maximum total assets amount.\\n event DepositParametersUpdated(\\n uint256 minimumDepositAmount,\\n uint256 maximumTotalAssets\\n );\\n\\n /// Emitted when the dispatcher contract is updated.\\n /// @param oldDispatcher Address of the old dispatcher contract.\\n /// @param newDispatcher Address of the new dispatcher contract.\\n event DispatcherUpdated(address oldDispatcher, address newDispatcher);\\n\\n /// Reverts if the amount is less than the minimum deposit amount.\\n /// @param amount Amount to check.\\n /// @param min Minimum amount to check 'amount' against.\\n error LessThanMinDeposit(uint256 amount, uint256 min);\\n\\n /// Reverts if the address is zero.\\n error ZeroAddress();\\n\\n /// Reverts if the address is disallowed.\\n error DisallowedAddress();\\n\\n constructor(\\n IERC20 _tbtc,\\n address _treasury\\n ) ERC4626(_tbtc) ERC20(\\\"Acre Staked Bitcoin\\\", \\\"stBTC\\\") Ownable(msg.sender) {\\n if (address(_treasury) == address(0)) {\\n revert ZeroAddress();\\n }\\n treasury = _treasury;\\n // TODO: Revisit the exact values closer to the launch.\\n minimumDepositAmount = 0.001 * 1e18; // 0.001 tBTC\\n maximumTotalAssets = 25 * 1e18; // 25 tBTC\\n }\\n\\n /// @notice Updates treasury wallet address.\\n /// @param newTreasury New treasury wallet address.\\n function updateTreasury(address newTreasury) external onlyOwner {\\n // TODO: Introduce a parameters update process.\\n if (newTreasury == address(0)) {\\n revert ZeroAddress();\\n }\\n if (newTreasury == address(this)) {\\n revert DisallowedAddress();\\n }\\n treasury = newTreasury;\\n\\n emit TreasuryUpdated(newTreasury);\\n }\\n\\n /// @notice Updates deposit parameters.\\n /// @dev To disable the limit for deposits, set the maximum total assets to\\n /// maximum (`type(uint256).max`).\\n /// @param _minimumDepositAmount New value of the minimum deposit amount. It\\n /// is the minimum amount for a single deposit operation.\\n /// @param _maximumTotalAssets New value of the maximum total assets amount.\\n /// It is the maximum amount of the tBTC token that the Acre protocol\\n /// can hold.\\n function updateDepositParameters(\\n uint256 _minimumDepositAmount,\\n uint256 _maximumTotalAssets\\n ) external onlyOwner {\\n // TODO: Introduce a parameters update process.\\n minimumDepositAmount = _minimumDepositAmount;\\n maximumTotalAssets = _maximumTotalAssets;\\n\\n emit DepositParametersUpdated(\\n _minimumDepositAmount,\\n _maximumTotalAssets\\n );\\n }\\n\\n // TODO: Implement a governed upgrade process that initiates an update and\\n // then finalizes it after a delay.\\n /// @notice Updates the dispatcher contract and gives it an unlimited\\n /// allowance to transfer staked tBTC.\\n /// @param newDispatcher Address of the new dispatcher contract.\\n function updateDispatcher(Dispatcher newDispatcher) external onlyOwner {\\n if (address(newDispatcher) == address(0)) {\\n revert ZeroAddress();\\n }\\n\\n address oldDispatcher = address(dispatcher);\\n\\n emit DispatcherUpdated(oldDispatcher, address(newDispatcher));\\n dispatcher = newDispatcher;\\n\\n // TODO: Once withdrawal/rebalancing is implemented, we need to revoke the\\n // approval of the vaults share tokens from the old dispatcher and approve\\n // a new dispatcher to manage the share tokens.\\n\\n if (oldDispatcher != address(0)) {\\n // Setting allowance to zero for the old dispatcher\\n IERC20(asset()).forceApprove(oldDispatcher, 0);\\n }\\n\\n // Setting allowance to max for the new dispatcher\\n IERC20(asset()).forceApprove(address(dispatcher), type(uint256).max);\\n }\\n\\n /// @notice Mints shares to receiver by depositing exactly amount of\\n /// tBTC tokens.\\n /// @dev Takes into account a deposit parameter, minimum deposit amount,\\n /// which determines the minimum amount for a single deposit operation.\\n /// The amount of the assets has to be pre-approved in the tBTC\\n /// contract.\\n /// @param assets Approved amount of tBTC tokens to deposit.\\n /// @param receiver The address to which the shares will be minted.\\n /// @return Minted shares.\\n function deposit(\\n uint256 assets,\\n address receiver\\n ) public override returns (uint256) {\\n if (assets < minimumDepositAmount) {\\n revert LessThanMinDeposit(assets, minimumDepositAmount);\\n }\\n\\n return super.deposit(assets, receiver);\\n }\\n\\n /// @notice Mints shares to receiver by depositing tBTC tokens.\\n /// @dev Takes into account a deposit parameter, minimum deposit amount,\\n /// which determines the minimum amount for a single deposit operation.\\n /// The amount of the assets has to be pre-approved in the tBTC\\n /// contract.\\n /// The msg.sender is required to grant approval for tBTC transfer.\\n /// To determine the total assets amount necessary for approval\\n /// corresponding to a given share amount, use the `previewMint` function.\\n /// @param shares Amount of shares to mint.\\n /// @param receiver The address to which the shares will be minted.\\n function mint(\\n uint256 shares,\\n address receiver\\n ) public override returns (uint256 assets) {\\n if ((assets = super.mint(shares, receiver)) < minimumDepositAmount) {\\n revert LessThanMinDeposit(assets, minimumDepositAmount);\\n }\\n }\\n\\n /// @notice Returns value of assets that would be exchanged for the amount of\\n /// shares owned by the `account`.\\n /// @param account Owner of shares.\\n /// @return Assets amount.\\n function assetsBalanceOf(address account) public view returns (uint256) {\\n return convertToAssets(balanceOf(account));\\n }\\n\\n /// @notice Returns the maximum amount of the tBTC token that can be\\n /// deposited into the vault for the receiver through a deposit\\n /// call. It takes into account the deposit parameter, maximum total\\n /// assets, which determines the total amount of tBTC token held by\\n /// Acre protocol.\\n /// @dev When the remaining amount of unused limit is less than the minimum\\n /// deposit amount, this function returns 0.\\n /// @return The maximum amount of tBTC token that can be deposited into\\n /// Acre protocol for the receiver.\\n function maxDeposit(address) public view override returns (uint256) {\\n if (maximumTotalAssets == type(uint256).max) {\\n return type(uint256).max;\\n }\\n\\n uint256 _totalAssets = totalAssets();\\n\\n return\\n _totalAssets >= maximumTotalAssets\\n ? 0\\n : maximumTotalAssets - _totalAssets;\\n }\\n\\n /// @notice Returns the maximum amount of the vault shares that can be\\n /// minted for the receiver, through a mint call.\\n /// @dev Since the stBTC contract limits the maximum total tBTC tokens this\\n /// function converts the maximum deposit amount to shares.\\n /// @return The maximum amount of the vault shares.\\n function maxMint(address receiver) public view override returns (uint256) {\\n uint256 _maxDeposit = maxDeposit(receiver);\\n\\n // slither-disable-next-line incorrect-equality\\n return\\n _maxDeposit == type(uint256).max\\n ? type(uint256).max\\n : convertToShares(_maxDeposit);\\n }\\n\\n /// @return Returns deposit parameters.\\n function depositParameters() public view returns (uint256, uint256) {\\n return (minimumDepositAmount, maximumTotalAssets);\\n }\\n}\\n\",\"keccak256\":\"0x8fa5224df372d53f1d86ee41132cb614d45916ba0b715b967d8d308b67a2a855\",\"license\":\"GPL-3.0-only\"}},\"version\":1}", + "bytecode": "0x60c06040523480156200001157600080fd5b506040516200221a3803806200221a8339810160408190526200003491620002d5565b33826040518060400160405280601381526020017f41637265205374616b656420426974636f696e0000000000000000000000000081525060405180604001604052806005815260200164737442544360d81b81525081600390816200009b9190620003b9565b506004620000aa8282620003b9565b505050600080620000c1836200018560201b60201c565b9150915081620000d3576012620000d5565b805b60ff1660a05250506001600160a01b0390811660805281166200011257604051631e4fbdf760e01b81526000600482015260240160405180910390fd5b6200011d816200026a565b506001600160a01b038116620001465760405163d92e233d60e01b815260040160405180910390fd5b600780546001600160a01b0319166001600160a01b03929092169190911790555066038d7ea4c6800060085568015af1d78b58c40000600955620004d0565b60408051600481526024810182526020810180516001600160e01b031663313ce56760e01b17905290516000918291829182916001600160a01b03871691620001ce9162000485565b600060405180830381855afa9150503d80600081146200020b576040519150601f19603f3d011682016040523d82523d6000602084013e62000210565b606091505b50915091508180156200022557506020815110155b156200025d57600081806020019051810190620002439190620004b6565b905060ff81116200025b576001969095509350505050565b505b5060009485945092505050565b600580546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b6001600160a01b0381168114620002d257600080fd5b50565b60008060408385031215620002e957600080fd5b8251620002f681620002bc565b60208401519092506200030981620002bc565b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b600181811c908216806200033f57607f821691505b6020821081036200036057634e487b7160e01b600052602260045260246000fd5b50919050565b601f821115620003b457600081815260208120601f850160051c810160208610156200038f5750805b601f850160051c820191505b81811015620003b0578281556001016200039b565b5050505b505050565b81516001600160401b03811115620003d557620003d562000314565b620003ed81620003e684546200032a565b8462000366565b602080601f8311600181146200042557600084156200040c5750858301515b600019600386901b1c1916600185901b178555620003b0565b600085815260208120601f198616915b82811015620004565788860151825594840194600190910190840162000435565b5085821015620004755787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b6000825160005b81811015620004a857602081860181015185830152016200048c565b506000920191825250919050565b600060208284031215620004c957600080fd5b5051919050565b60805160a051611d0162000519600039600061070d0152600081816103350152818161059d0152818161082c01528181610871015281816110b501526114ff0152611d016000f3fe608060405234801561001057600080fd5b50600436106102775760003560e01c80637f51bb1f11610160578063c42b64d0116100d8578063d905777e1161008c578063e9c092e311610071578063e9c092e314610546578063ef8b30f7146104c1578063f2fde38b1461055957600080fd5b8063d905777e146104fa578063dd62ed3e1461050d57600080fd5b8063c6e6f592116100bd578063c6e6f592146104c1578063cb7e9057146104d4578063ce96cb77146104e757600080fd5b8063c42b64d014610493578063c63d75b6146104ae57600080fd5b8063995bd0c11161012f578063b3d7f6b911610114578063b3d7f6b91461045a578063b460af941461046d578063ba0876521461048057600080fd5b8063995bd0c114610434578063a9059cbb1461044757600080fd5b80637f51bb1f146103f55780638da5cb5b1461040857806394bf804d1461041957806395d89b411461042c57600080fd5b806338d52e0f116101f3578063543bda75116101c25780636e553f65116101a75780636e553f65146103b157806370a08231146103c4578063715018a6146103ed57600080fd5b8063543bda751461039557806361d027b31461039e57600080fd5b806338d52e0f14610333578063402d267d1461036d57806342af1198146103805780634cdad506146102ac57600080fd5b8063095ea7b31161024a57806318160ddd1161022f57806318160ddd146102fe57806323b872dd14610306578063313ce5671461031957600080fd5b8063095ea7b3146102c85780630a28a477146102eb57600080fd5b806301e1d1141461027c57806306fdde031461029757806307a2d13a146102ac578063080c279a146102bf575b600080fd5b61028461056c565b6040519081526020015b60405180910390f35b61029f610615565b60405161028e91906118e9565b6102846102ba36600461191c565b6106a7565b61028460085481565b6102db6102d636600461194a565b6106ba565b604051901515815260200161028e565b6102846102f936600461191c565b6106d2565b600254610284565b6102db610314366004611976565b6106df565b610321610705565b60405160ff909116815260200161028e565b7f00000000000000000000000000000000000000000000000000000000000000005b6040516001600160a01b03909116815260200161028e565b61028461037b3660046119b7565b610731565b61039361038e3660046119b7565b610779565b005b61028460095481565b600754610355906001600160a01b031681565b6102846103bf3660046119d4565b610899565b6102846103d23660046119b7565b6001600160a01b031660009081526020819052604090205490565b6103936108df565b6103936104033660046119b7565b6108f3565b6005546001600160a01b0316610355565b6102846104273660046119d4565b6109c5565b61029f610a04565b6102846104423660046119b7565b610a13565b6102db61045536600461194a565b610a35565b61028461046836600461191c565b610a43565b61028461047b366004611a04565b610a50565b61028461048e366004611a04565b610ad4565b6008546009546040805192835260208301919091520161028e565b6102846104bc3660046119b7565b610b4f565b6102846104cf36600461191c565b610b79565b600654610355906001600160a01b031681565b6102846104f53660046119b7565b610b86565b6102846105083660046119b7565b610baa565b61028461051b366004611a46565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b610393610554366004611a74565b610bc8565b6103936105673660046119b7565b610c17565b6040517f70a082310000000000000000000000000000000000000000000000000000000081523060048201526000907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a0823190602401602060405180830381865afa1580156105ec573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106109190611a96565b905090565b60606003805461062490611aaf565b80601f016020809104026020016040519081016040528092919081815260200182805461065090611aaf565b801561069d5780601f106106725761010080835404028352916020019161069d565b820191906000526020600020905b81548152906001019060200180831161068057829003601f168201915b5050505050905090565b60006106b4826000610c6e565b92915050565b6000336106c8818585610ca8565b5060019392505050565b60006106b4826001610cba565b6000336106ed858285610cea565b6106f8858585610d81565b60019150505b9392505050565b6000610610817f0000000000000000000000000000000000000000000000000000000000000000611aff565b6000600019600954036107475750600019919050565b600061075161056c565b9050600954811015610770578060095461076b9190611b18565b6106fe565b60009392505050565b610781610de0565b6001600160a01b0381166107a85760405163d92e233d60e01b815260040160405180910390fd5b600654604080516001600160a01b0392831680825292841660208201527ff6e490308a0c67484cf1082cb89a68053c7a7bbf474247e8b634957e2ce2f296910160405180910390a16006805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b038481169190911790915581161561085c5761085c8160007f00000000000000000000000000000000000000000000000000000000000000005b6001600160a01b03169190610e26565b600654610895906001600160a01b03166000197f000000000000000000000000000000000000000000000000000000000000000061084c565b5050565b60006008548310156108d5576008546040516314d6e77b60e31b81526108cc918591600401918252602082015260400190565b60405180910390fd5b6106fe8383610f27565b6108e7610de0565b6108f16000610fa9565b565b6108fb610de0565b6001600160a01b0381166109225760405163d92e233d60e01b815260040160405180910390fd5b306001600160a01b03821603610964576040517f593230f700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6007805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0383169081179091556040519081527f7dae230f18360d76a040c81f050aa14eb9d6dc7901b20fc5d855e2a20fe814d19060200160405180910390a150565b60006008546109d48484611008565b91508110156106b4576008546040516314d6e77b60e31b81526108cc918391600401918252602082015260400190565b60606004805461062490611aaf565b6001600160a01b0381166000908152602081905260408120546106b4906106a7565b6000336106c8818585610d81565b60006106b4826001610c6e565b600080610a5c83610b86565b905080851115610ab1576040517ffe9cceec0000000000000000000000000000000000000000000000000000000081526001600160a01b038416600482015260248101869052604481018290526064016108cc565b6000610abc866106d2565b9050610acb3386868985611082565b95945050505050565b600080610ae083610baa565b905080851115610b35576040517fb94abeec0000000000000000000000000000000000000000000000000000000081526001600160a01b038416600482015260248101869052604481018290526064016108cc565b6000610b40866106a7565b9050610acb338686848a611082565b600080610b5b83610731565b90506000198114610b6f5761076b81610b79565b6000199392505050565b60006106b4826000610cba565b6001600160a01b0381166000908152602081905260408120546106b4906000610c6e565b6001600160a01b0381166000908152602081905260408120546106b4565b610bd0610de0565b6008829055600981905560408051838152602081018390527f5543b97d9277fef57d70edac6c4c10f4426957b886dfe97286c01d2218a53280910160405180910390a15050565b610c1f610de0565b6001600160a01b038116610c62576040517f1e4fbdf7000000000000000000000000000000000000000000000000000000008152600060048201526024016108cc565b610c6b81610fa9565b50565b60006106fe610c7b61056c565b610c86906001611b2b565b610c926000600a611c22565b600254610c9f9190611b2b565b85919085611142565b610cb58383836001611191565b505050565b60006106fe610cca82600a611c22565b600254610cd79190611b2b565b610cdf61056c565b610c9f906001611b2b565b6001600160a01b038381166000908152600160209081526040808320938616835292905220546000198114610d7b5781811015610d6c576040517ffb8f41b20000000000000000000000000000000000000000000000000000000081526001600160a01b038416600482015260248101829052604481018390526064016108cc565b610d7b84848484036000611191565b50505050565b6001600160a01b038316610dab57604051634b637e8f60e11b8152600060048201526024016108cc565b6001600160a01b038216610dd55760405163ec442f0560e01b8152600060048201526024016108cc565b610cb5838383611298565b6005546001600160a01b031633146108f1576040517f118cdaa70000000000000000000000000000000000000000000000000000000081523360048201526024016108cc565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f095ea7b300000000000000000000000000000000000000000000000000000000179052610ea584826113db565b610d7b576040516001600160a01b03848116602483015260006044830152610f1d91869182169063095ea7b3906064015b604051602081830303815290604052915060e01b6020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505061147e565b610d7b848261147e565b600080610f3383610731565b905080841115610f88576040517f79012fb20000000000000000000000000000000000000000000000000000000081526001600160a01b038416600482015260248101859052604481018290526064016108cc565b6000610f9385610b79565b9050610fa1338587846114fa565b949350505050565b600580546001600160a01b0383811673ffffffffffffffffffffffffffffffffffffffff19831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b60008061101483610b4f565b905080841115611069576040517f284ff6670000000000000000000000000000000000000000000000000000000081526001600160a01b038416600482015260248101859052604481018290526064016108cc565b600061107485610a43565b9050610fa1338583886114fa565b826001600160a01b0316856001600160a01b0316146110a6576110a6838683610cea565b6110b0838261157e565b6110db7f000000000000000000000000000000000000000000000000000000000000000085846115b4565b826001600160a01b0316846001600160a01b0316866001600160a01b03167ffbde797d201c681b91056529119e0b02407c7bb96a4a2c75c01fc9667232c8db8585604051611133929190918252602082015260400190565b60405180910390a45050505050565b6000806111508686866115e5565b905061115b836116c2565b801561117757506000848061117257611172611c31565b868809115b15610acb57611187600182611b2b565b9695505050505050565b6001600160a01b0384166111d4576040517fe602df05000000000000000000000000000000000000000000000000000000008152600060048201526024016108cc565b6001600160a01b038316611217576040517f94280d62000000000000000000000000000000000000000000000000000000008152600060048201526024016108cc565b6001600160a01b0380851660009081526001602090815260408083209387168352929052208290558015610d7b57826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161128a91815260200190565b60405180910390a350505050565b6001600160a01b0383166112c35780600260008282546112b89190611b2b565b9091555061134e9050565b6001600160a01b0383166000908152602081905260409020548181101561132f576040517fe450d38c0000000000000000000000000000000000000000000000000000000081526001600160a01b038516600482015260248101829052604481018390526064016108cc565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b03821661136a57600280548290039055611389565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516113ce91815260200190565b60405180910390a3505050565b6000806000846001600160a01b0316846040516113f89190611c47565b6000604051808303816000865af19150503d8060008114611435576040519150601f19603f3d011682016040523d82523d6000602084013e61143a565b606091505b50915091508180156114645750805115806114645750808060200190518101906114649190611c63565b8015610acb5750505050506001600160a01b03163b151590565b60006114936001600160a01b038416836116ef565b905080516000141580156114b85750808060200190518101906114b69190611c63565b155b15610cb5576040517f5274afe70000000000000000000000000000000000000000000000000000000081526001600160a01b03841660048201526024016108cc565b6115267f00000000000000000000000000000000000000000000000000000000000000008530856116fd565b6115308382611736565b826001600160a01b0316846001600160a01b03167fdcbc1c05240f31ff3ad067ef1ee35ce4997762752e3a095284754544f4c709d7848460405161128a929190918252602082015260400190565b6001600160a01b0382166115a857604051634b637e8f60e11b8152600060048201526024016108cc565b61089582600083611298565b6040516001600160a01b03838116602483015260448201839052610cb591859182169063a9059cbb90606401610ed6565b600083830281600019858709828110838203039150508060000361161c5783828161161257611612611c31565b04925050506106fe565b808411611655576040517f227bc15300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6000848688096000868103871696879004966002600389028118808a02820302808a02820302808a02820302808a02820302808a02820302808a02909103029181900381900460010186841190950394909402919094039290920491909117919091029150509392505050565b600060028260038111156116d8576116d8611c85565b6116e29190611c9b565b60ff166001149050919050565b60606106fe8383600061176c565b6040516001600160a01b038481166024830152838116604483015260648201839052610d7b9186918216906323b872dd90608401610ed6565b6001600160a01b0382166117605760405163ec442f0560e01b8152600060048201526024016108cc565b61089560008383611298565b6060814710156117aa576040517fcd7860590000000000000000000000000000000000000000000000000000000081523060048201526024016108cc565b600080856001600160a01b031684866040516117c69190611c47565b60006040518083038185875af1925050503d8060008114611803576040519150601f19603f3d011682016040523d82523d6000602084013e611808565b606091505b50915091506111878683836060826118235761076b82611883565b815115801561183a57506001600160a01b0384163b155b1561187c576040517f9996b3150000000000000000000000000000000000000000000000000000000081526001600160a01b03851660048201526024016108cc565b50806106fe565b8051156118935780518082602001fd5b6040517f1425ea4200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60005b838110156118e05781810151838201526020016118c8565b50506000910152565b60208152600082518060208401526119088160408501602087016118c5565b601f01601f19169190910160400192915050565b60006020828403121561192e57600080fd5b5035919050565b6001600160a01b0381168114610c6b57600080fd5b6000806040838503121561195d57600080fd5b823561196881611935565b946020939093013593505050565b60008060006060848603121561198b57600080fd5b833561199681611935565b925060208401356119a681611935565b929592945050506040919091013590565b6000602082840312156119c957600080fd5b81356106fe81611935565b600080604083850312156119e757600080fd5b8235915060208301356119f981611935565b809150509250929050565b600080600060608486031215611a1957600080fd5b833592506020840135611a2b81611935565b91506040840135611a3b81611935565b809150509250925092565b60008060408385031215611a5957600080fd5b8235611a6481611935565b915060208301356119f981611935565b60008060408385031215611a8757600080fd5b50508035926020909101359150565b600060208284031215611aa857600080fd5b5051919050565b600181811c90821680611ac357607f821691505b602082108103611ae357634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b60ff81811683821601908111156106b4576106b4611ae9565b818103818111156106b4576106b4611ae9565b808201808211156106b4576106b4611ae9565b600181815b80851115611b79578160001904821115611b5f57611b5f611ae9565b80851615611b6c57918102915b93841c9390800290611b43565b509250929050565b600082611b90575060016106b4565b81611b9d575060006106b4565b8160018114611bb35760028114611bbd57611bd9565b60019150506106b4565b60ff841115611bce57611bce611ae9565b50506001821b6106b4565b5060208310610133831016604e8410600b8410161715611bfc575081810a6106b4565b611c068383611b3e565b8060001904821115611c1a57611c1a611ae9565b029392505050565b60006106fe60ff841683611b81565b634e487b7160e01b600052601260045260246000fd5b60008251611c598184602087016118c5565b9190910192915050565b600060208284031215611c7557600080fd5b815180151581146106fe57600080fd5b634e487b7160e01b600052602160045260246000fd5b600060ff831680611cbc57634e487b7160e01b600052601260045260246000fd5b8060ff8416069150509291505056fea2646970667358221220613e87e5d7e469eae628fc2f45b68eaf8d314fc3fbd6b5de50baf2a5e5c5a97664736f6c63430008150033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106102775760003560e01c80637f51bb1f11610160578063c42b64d0116100d8578063d905777e1161008c578063e9c092e311610071578063e9c092e314610546578063ef8b30f7146104c1578063f2fde38b1461055957600080fd5b8063d905777e146104fa578063dd62ed3e1461050d57600080fd5b8063c6e6f592116100bd578063c6e6f592146104c1578063cb7e9057146104d4578063ce96cb77146104e757600080fd5b8063c42b64d014610493578063c63d75b6146104ae57600080fd5b8063995bd0c11161012f578063b3d7f6b911610114578063b3d7f6b91461045a578063b460af941461046d578063ba0876521461048057600080fd5b8063995bd0c114610434578063a9059cbb1461044757600080fd5b80637f51bb1f146103f55780638da5cb5b1461040857806394bf804d1461041957806395d89b411461042c57600080fd5b806338d52e0f116101f3578063543bda75116101c25780636e553f65116101a75780636e553f65146103b157806370a08231146103c4578063715018a6146103ed57600080fd5b8063543bda751461039557806361d027b31461039e57600080fd5b806338d52e0f14610333578063402d267d1461036d57806342af1198146103805780634cdad506146102ac57600080fd5b8063095ea7b31161024a57806318160ddd1161022f57806318160ddd146102fe57806323b872dd14610306578063313ce5671461031957600080fd5b8063095ea7b3146102c85780630a28a477146102eb57600080fd5b806301e1d1141461027c57806306fdde031461029757806307a2d13a146102ac578063080c279a146102bf575b600080fd5b61028461056c565b6040519081526020015b60405180910390f35b61029f610615565b60405161028e91906118e9565b6102846102ba36600461191c565b6106a7565b61028460085481565b6102db6102d636600461194a565b6106ba565b604051901515815260200161028e565b6102846102f936600461191c565b6106d2565b600254610284565b6102db610314366004611976565b6106df565b610321610705565b60405160ff909116815260200161028e565b7f00000000000000000000000000000000000000000000000000000000000000005b6040516001600160a01b03909116815260200161028e565b61028461037b3660046119b7565b610731565b61039361038e3660046119b7565b610779565b005b61028460095481565b600754610355906001600160a01b031681565b6102846103bf3660046119d4565b610899565b6102846103d23660046119b7565b6001600160a01b031660009081526020819052604090205490565b6103936108df565b6103936104033660046119b7565b6108f3565b6005546001600160a01b0316610355565b6102846104273660046119d4565b6109c5565b61029f610a04565b6102846104423660046119b7565b610a13565b6102db61045536600461194a565b610a35565b61028461046836600461191c565b610a43565b61028461047b366004611a04565b610a50565b61028461048e366004611a04565b610ad4565b6008546009546040805192835260208301919091520161028e565b6102846104bc3660046119b7565b610b4f565b6102846104cf36600461191c565b610b79565b600654610355906001600160a01b031681565b6102846104f53660046119b7565b610b86565b6102846105083660046119b7565b610baa565b61028461051b366004611a46565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b610393610554366004611a74565b610bc8565b6103936105673660046119b7565b610c17565b6040517f70a082310000000000000000000000000000000000000000000000000000000081523060048201526000907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a0823190602401602060405180830381865afa1580156105ec573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106109190611a96565b905090565b60606003805461062490611aaf565b80601f016020809104026020016040519081016040528092919081815260200182805461065090611aaf565b801561069d5780601f106106725761010080835404028352916020019161069d565b820191906000526020600020905b81548152906001019060200180831161068057829003601f168201915b5050505050905090565b60006106b4826000610c6e565b92915050565b6000336106c8818585610ca8565b5060019392505050565b60006106b4826001610cba565b6000336106ed858285610cea565b6106f8858585610d81565b60019150505b9392505050565b6000610610817f0000000000000000000000000000000000000000000000000000000000000000611aff565b6000600019600954036107475750600019919050565b600061075161056c565b9050600954811015610770578060095461076b9190611b18565b6106fe565b60009392505050565b610781610de0565b6001600160a01b0381166107a85760405163d92e233d60e01b815260040160405180910390fd5b600654604080516001600160a01b0392831680825292841660208201527ff6e490308a0c67484cf1082cb89a68053c7a7bbf474247e8b634957e2ce2f296910160405180910390a16006805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b038481169190911790915581161561085c5761085c8160007f00000000000000000000000000000000000000000000000000000000000000005b6001600160a01b03169190610e26565b600654610895906001600160a01b03166000197f000000000000000000000000000000000000000000000000000000000000000061084c565b5050565b60006008548310156108d5576008546040516314d6e77b60e31b81526108cc918591600401918252602082015260400190565b60405180910390fd5b6106fe8383610f27565b6108e7610de0565b6108f16000610fa9565b565b6108fb610de0565b6001600160a01b0381166109225760405163d92e233d60e01b815260040160405180910390fd5b306001600160a01b03821603610964576040517f593230f700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6007805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0383169081179091556040519081527f7dae230f18360d76a040c81f050aa14eb9d6dc7901b20fc5d855e2a20fe814d19060200160405180910390a150565b60006008546109d48484611008565b91508110156106b4576008546040516314d6e77b60e31b81526108cc918391600401918252602082015260400190565b60606004805461062490611aaf565b6001600160a01b0381166000908152602081905260408120546106b4906106a7565b6000336106c8818585610d81565b60006106b4826001610c6e565b600080610a5c83610b86565b905080851115610ab1576040517ffe9cceec0000000000000000000000000000000000000000000000000000000081526001600160a01b038416600482015260248101869052604481018290526064016108cc565b6000610abc866106d2565b9050610acb3386868985611082565b95945050505050565b600080610ae083610baa565b905080851115610b35576040517fb94abeec0000000000000000000000000000000000000000000000000000000081526001600160a01b038416600482015260248101869052604481018290526064016108cc565b6000610b40866106a7565b9050610acb338686848a611082565b600080610b5b83610731565b90506000198114610b6f5761076b81610b79565b6000199392505050565b60006106b4826000610cba565b6001600160a01b0381166000908152602081905260408120546106b4906000610c6e565b6001600160a01b0381166000908152602081905260408120546106b4565b610bd0610de0565b6008829055600981905560408051838152602081018390527f5543b97d9277fef57d70edac6c4c10f4426957b886dfe97286c01d2218a53280910160405180910390a15050565b610c1f610de0565b6001600160a01b038116610c62576040517f1e4fbdf7000000000000000000000000000000000000000000000000000000008152600060048201526024016108cc565b610c6b81610fa9565b50565b60006106fe610c7b61056c565b610c86906001611b2b565b610c926000600a611c22565b600254610c9f9190611b2b565b85919085611142565b610cb58383836001611191565b505050565b60006106fe610cca82600a611c22565b600254610cd79190611b2b565b610cdf61056c565b610c9f906001611b2b565b6001600160a01b038381166000908152600160209081526040808320938616835292905220546000198114610d7b5781811015610d6c576040517ffb8f41b20000000000000000000000000000000000000000000000000000000081526001600160a01b038416600482015260248101829052604481018390526064016108cc565b610d7b84848484036000611191565b50505050565b6001600160a01b038316610dab57604051634b637e8f60e11b8152600060048201526024016108cc565b6001600160a01b038216610dd55760405163ec442f0560e01b8152600060048201526024016108cc565b610cb5838383611298565b6005546001600160a01b031633146108f1576040517f118cdaa70000000000000000000000000000000000000000000000000000000081523360048201526024016108cc565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f095ea7b300000000000000000000000000000000000000000000000000000000179052610ea584826113db565b610d7b576040516001600160a01b03848116602483015260006044830152610f1d91869182169063095ea7b3906064015b604051602081830303815290604052915060e01b6020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505061147e565b610d7b848261147e565b600080610f3383610731565b905080841115610f88576040517f79012fb20000000000000000000000000000000000000000000000000000000081526001600160a01b038416600482015260248101859052604481018290526064016108cc565b6000610f9385610b79565b9050610fa1338587846114fa565b949350505050565b600580546001600160a01b0383811673ffffffffffffffffffffffffffffffffffffffff19831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b60008061101483610b4f565b905080841115611069576040517f284ff6670000000000000000000000000000000000000000000000000000000081526001600160a01b038416600482015260248101859052604481018290526064016108cc565b600061107485610a43565b9050610fa1338583886114fa565b826001600160a01b0316856001600160a01b0316146110a6576110a6838683610cea565b6110b0838261157e565b6110db7f000000000000000000000000000000000000000000000000000000000000000085846115b4565b826001600160a01b0316846001600160a01b0316866001600160a01b03167ffbde797d201c681b91056529119e0b02407c7bb96a4a2c75c01fc9667232c8db8585604051611133929190918252602082015260400190565b60405180910390a45050505050565b6000806111508686866115e5565b905061115b836116c2565b801561117757506000848061117257611172611c31565b868809115b15610acb57611187600182611b2b565b9695505050505050565b6001600160a01b0384166111d4576040517fe602df05000000000000000000000000000000000000000000000000000000008152600060048201526024016108cc565b6001600160a01b038316611217576040517f94280d62000000000000000000000000000000000000000000000000000000008152600060048201526024016108cc565b6001600160a01b0380851660009081526001602090815260408083209387168352929052208290558015610d7b57826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161128a91815260200190565b60405180910390a350505050565b6001600160a01b0383166112c35780600260008282546112b89190611b2b565b9091555061134e9050565b6001600160a01b0383166000908152602081905260409020548181101561132f576040517fe450d38c0000000000000000000000000000000000000000000000000000000081526001600160a01b038516600482015260248101829052604481018390526064016108cc565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b03821661136a57600280548290039055611389565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516113ce91815260200190565b60405180910390a3505050565b6000806000846001600160a01b0316846040516113f89190611c47565b6000604051808303816000865af19150503d8060008114611435576040519150601f19603f3d011682016040523d82523d6000602084013e61143a565b606091505b50915091508180156114645750805115806114645750808060200190518101906114649190611c63565b8015610acb5750505050506001600160a01b03163b151590565b60006114936001600160a01b038416836116ef565b905080516000141580156114b85750808060200190518101906114b69190611c63565b155b15610cb5576040517f5274afe70000000000000000000000000000000000000000000000000000000081526001600160a01b03841660048201526024016108cc565b6115267f00000000000000000000000000000000000000000000000000000000000000008530856116fd565b6115308382611736565b826001600160a01b0316846001600160a01b03167fdcbc1c05240f31ff3ad067ef1ee35ce4997762752e3a095284754544f4c709d7848460405161128a929190918252602082015260400190565b6001600160a01b0382166115a857604051634b637e8f60e11b8152600060048201526024016108cc565b61089582600083611298565b6040516001600160a01b03838116602483015260448201839052610cb591859182169063a9059cbb90606401610ed6565b600083830281600019858709828110838203039150508060000361161c5783828161161257611612611c31565b04925050506106fe565b808411611655576040517f227bc15300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6000848688096000868103871696879004966002600389028118808a02820302808a02820302808a02820302808a02820302808a02820302808a02909103029181900381900460010186841190950394909402919094039290920491909117919091029150509392505050565b600060028260038111156116d8576116d8611c85565b6116e29190611c9b565b60ff166001149050919050565b60606106fe8383600061176c565b6040516001600160a01b038481166024830152838116604483015260648201839052610d7b9186918216906323b872dd90608401610ed6565b6001600160a01b0382166117605760405163ec442f0560e01b8152600060048201526024016108cc565b61089560008383611298565b6060814710156117aa576040517fcd7860590000000000000000000000000000000000000000000000000000000081523060048201526024016108cc565b600080856001600160a01b031684866040516117c69190611c47565b60006040518083038185875af1925050503d8060008114611803576040519150601f19603f3d011682016040523d82523d6000602084013e611808565b606091505b50915091506111878683836060826118235761076b82611883565b815115801561183a57506001600160a01b0384163b155b1561187c576040517f9996b3150000000000000000000000000000000000000000000000000000000081526001600160a01b03851660048201526024016108cc565b50806106fe565b8051156118935780518082602001fd5b6040517f1425ea4200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60005b838110156118e05781810151838201526020016118c8565b50506000910152565b60208152600082518060208401526119088160408501602087016118c5565b601f01601f19169190910160400192915050565b60006020828403121561192e57600080fd5b5035919050565b6001600160a01b0381168114610c6b57600080fd5b6000806040838503121561195d57600080fd5b823561196881611935565b946020939093013593505050565b60008060006060848603121561198b57600080fd5b833561199681611935565b925060208401356119a681611935565b929592945050506040919091013590565b6000602082840312156119c957600080fd5b81356106fe81611935565b600080604083850312156119e757600080fd5b8235915060208301356119f981611935565b809150509250929050565b600080600060608486031215611a1957600080fd5b833592506020840135611a2b81611935565b91506040840135611a3b81611935565b809150509250925092565b60008060408385031215611a5957600080fd5b8235611a6481611935565b915060208301356119f981611935565b60008060408385031215611a8757600080fd5b50508035926020909101359150565b600060208284031215611aa857600080fd5b5051919050565b600181811c90821680611ac357607f821691505b602082108103611ae357634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b60ff81811683821601908111156106b4576106b4611ae9565b818103818111156106b4576106b4611ae9565b808201808211156106b4576106b4611ae9565b600181815b80851115611b79578160001904821115611b5f57611b5f611ae9565b80851615611b6c57918102915b93841c9390800290611b43565b509250929050565b600082611b90575060016106b4565b81611b9d575060006106b4565b8160018114611bb35760028114611bbd57611bd9565b60019150506106b4565b60ff841115611bce57611bce611ae9565b50506001821b6106b4565b5060208310610133831016604e8410600b8410161715611bfc575081810a6106b4565b611c068383611b3e565b8060001904821115611c1a57611c1a611ae9565b029392505050565b60006106fe60ff841683611b81565b634e487b7160e01b600052601260045260246000fd5b60008251611c598184602087016118c5565b9190910192915050565b600060208284031215611c7557600080fd5b815180151581146106fe57600080fd5b634e487b7160e01b600052602160045260246000fd5b600060ff831680611cbc57634e487b7160e01b600052601260045260246000fd5b8060ff8416069150509291505056fea2646970667358221220613e87e5d7e469eae628fc2f45b68eaf8d314fc3fbd6b5de50baf2a5e5c5a97664736f6c63430008150033", + "devdoc": { + "details": "ERC-4626 is a standard to optimize and unify the technical parameters of yield-bearing vaults. This contract facilitates the minting and burning of shares (stBTC), which are represented as standard ERC20 tokens, providing a seamless exchange with tBTC tokens.", + "errors": { + "AddressEmptyCode(address)": [ + { + "details": "There's no code at `target` (it is not a contract)." + } + ], + "AddressInsufficientBalance(address)": [ + { + "details": "The ETH balance of the account is not enough to perform the operation." + } + ], + "ERC20InsufficientAllowance(address,uint256,uint256)": [ + { + "details": "Indicates a failure with the `spender`’s `allowance`. Used in transfers.", + "params": { + "allowance": "Amount of tokens a `spender` is allowed to operate with.", + "needed": "Minimum amount required to perform a transfer.", + "spender": "Address that may be allowed to operate on tokens without being their owner." + } + } + ], + "ERC20InsufficientBalance(address,uint256,uint256)": [ + { + "details": "Indicates an error related to the current `balance` of a `sender`. Used in transfers.", + "params": { + "balance": "Current balance for the interacting account.", + "needed": "Minimum amount required to perform a transfer.", + "sender": "Address whose tokens are being transferred." + } + } + ], + "ERC20InvalidApprover(address)": [ + { + "details": "Indicates a failure with the `approver` of a token to be approved. Used in approvals.", + "params": { + "approver": "Address initiating an approval operation." + } + } + ], + "ERC20InvalidReceiver(address)": [ + { + "details": "Indicates a failure with the token `receiver`. Used in transfers.", + "params": { + "receiver": "Address to which tokens are being transferred." + } + } + ], + "ERC20InvalidSender(address)": [ + { + "details": "Indicates a failure with the token `sender`. Used in transfers.", + "params": { + "sender": "Address whose tokens are being transferred." + } + } + ], + "ERC20InvalidSpender(address)": [ + { + "details": "Indicates a failure with the `spender` to be approved. Used in approvals.", + "params": { + "spender": "Address that may be allowed to operate on tokens without being their owner." + } + } + ], + "ERC4626ExceededMaxDeposit(address,uint256,uint256)": [ + { + "details": "Attempted to deposit more assets than the max amount for `receiver`." + } + ], + "ERC4626ExceededMaxMint(address,uint256,uint256)": [ + { + "details": "Attempted to mint more shares than the max amount for `receiver`." + } + ], + "ERC4626ExceededMaxRedeem(address,uint256,uint256)": [ + { + "details": "Attempted to redeem more shares than the max amount for `receiver`." + } + ], + "ERC4626ExceededMaxWithdraw(address,uint256,uint256)": [ + { + "details": "Attempted to withdraw more assets than the max amount for `receiver`." + } + ], + "FailedInnerCall()": [ + { + "details": "A call to an address target failed. The target may have reverted." + } + ], + "LessThanMinDeposit(uint256,uint256)": [ + { + "params": { + "amount": "Amount to check.", + "min": "Minimum amount to check 'amount' against." + } + } + ], + "MathOverflowedMulDiv()": [ + { + "details": "Muldiv operation overflow." + } + ], + "OwnableInvalidOwner(address)": [ + { + "details": "The owner is not a valid owner account. (eg. `address(0)`)" + } + ], + "OwnableUnauthorizedAccount(address)": [ + { + "details": "The caller account is not authorized to perform an operation." + } + ], + "SafeERC20FailedOperation(address)": [ + { + "details": "An operation with an ERC20 token failed." + } + ] + }, + "events": { + "Approval(address,address,uint256)": { + "details": "Emitted when the allowance of a `spender` for an `owner` is set by a call to {approve}. `value` is the new allowance." + }, + "DepositParametersUpdated(uint256,uint256)": { + "params": { + "maximumTotalAssets": "New value of the maximum total assets amount.", + "minimumDepositAmount": "New value of the minimum deposit amount." + } + }, + "DispatcherUpdated(address,address)": { + "params": { + "newDispatcher": "Address of the new dispatcher contract.", + "oldDispatcher": "Address of the old dispatcher contract." + } + }, + "Transfer(address,address,uint256)": { + "details": "Emitted when `value` tokens are moved from one account (`from`) to another (`to`). Note that `value` may be zero." + }, + "TreasuryUpdated(address)": { + "params": { + "treasury": "New treasury wallet address." + } + } + }, + "kind": "dev", + "methods": { + "allowance(address,address)": { + "details": "See {IERC20-allowance}." + }, + "approve(address,uint256)": { + "details": "See {IERC20-approve}. NOTE: If `value` is the maximum `uint256`, the allowance is not updated on `transferFrom`. This is semantically equivalent to an infinite approval. Requirements: - `spender` cannot be the zero address." + }, + "asset()": { + "details": "See {IERC4626-asset}. " + }, + "assetsBalanceOf(address)": { + "params": { + "account": "Owner of shares." + }, + "returns": { + "_0": "Assets amount." + } + }, + "balanceOf(address)": { + "details": "See {IERC20-balanceOf}." + }, + "convertToAssets(uint256)": { + "details": "See {IERC4626-convertToAssets}. " + }, + "convertToShares(uint256)": { + "details": "See {IERC4626-convertToShares}. " + }, + "decimals()": { + "details": "Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This \"original\" value is cached during construction of the vault contract. If this read operation fails (e.g., the asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals. See {IERC20Metadata-decimals}." + }, + "deposit(uint256,address)": { + "details": "Takes into account a deposit parameter, minimum deposit amount, which determines the minimum amount for a single deposit operation. The amount of the assets has to be pre-approved in the tBTC contract.", + "params": { + "assets": "Approved amount of tBTC tokens to deposit.", + "receiver": "The address to which the shares will be minted." + }, + "returns": { + "_0": "Minted shares." + } + }, + "depositParameters()": { + "returns": { + "_0": "Returns deposit parameters." + } + }, + "maxDeposit(address)": { + "details": "When the remaining amount of unused limit is less than the minimum deposit amount, this function returns 0.", + "returns": { + "_0": "The maximum amount of tBTC token that can be deposited into Acre protocol for the receiver." + } + }, + "maxMint(address)": { + "details": "Since the stBTC contract limits the maximum total tBTC tokens this function converts the maximum deposit amount to shares.", + "returns": { + "_0": "The maximum amount of the vault shares." + } + }, + "maxRedeem(address)": { + "details": "See {IERC4626-maxRedeem}. " + }, + "maxWithdraw(address)": { + "details": "See {IERC4626-maxWithdraw}. " + }, + "mint(uint256,address)": { + "details": "Takes into account a deposit parameter, minimum deposit amount, which determines the minimum amount for a single deposit operation. The amount of the assets has to be pre-approved in the tBTC contract. The msg.sender is required to grant approval for tBTC transfer. To determine the total assets amount necessary for approval corresponding to a given share amount, use the `previewMint` function.", + "params": { + "receiver": "The address to which the shares will be minted.", + "shares": "Amount of shares to mint." + } + }, + "name()": { + "details": "Returns the name of the token." + }, + "owner()": { + "details": "Returns the address of the current owner." + }, + "previewDeposit(uint256)": { + "details": "See {IERC4626-previewDeposit}. " + }, + "previewMint(uint256)": { + "details": "See {IERC4626-previewMint}. " + }, + "previewRedeem(uint256)": { + "details": "See {IERC4626-previewRedeem}. " + }, + "previewWithdraw(uint256)": { + "details": "See {IERC4626-previewWithdraw}. " + }, + "redeem(uint256,address,address)": { + "details": "See {IERC4626-redeem}. " + }, + "renounceOwnership()": { + "details": "Leaves the contract without owner. It will not be possible to call `onlyOwner` functions. Can only be called by the current owner. NOTE: Renouncing ownership will leave the contract without an owner, thereby disabling any functionality that is only available to the owner." + }, + "symbol()": { + "details": "Returns the symbol of the token, usually a shorter version of the name." + }, + "totalAssets()": { + "details": "See {IERC4626-totalAssets}. " + }, + "totalSupply()": { + "details": "See {IERC20-totalSupply}." + }, + "transfer(address,uint256)": { + "details": "See {IERC20-transfer}. Requirements: - `to` cannot be the zero address. - the caller must have a balance of at least `value`." + }, + "transferFrom(address,address,uint256)": { + "details": "See {IERC20-transferFrom}. Emits an {Approval} event indicating the updated allowance. This is not required by the EIP. See the note at the beginning of {ERC20}. NOTE: Does not update the allowance if the current allowance is the maximum `uint256`. Requirements: - `from` and `to` cannot be the zero address. - `from` must have a balance of at least `value`. - the caller must have allowance for ``from``'s tokens of at least `value`." + }, + "transferOwnership(address)": { + "details": "Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner." + }, + "updateDepositParameters(uint256,uint256)": { + "details": "To disable the limit for deposits, set the maximum total assets to maximum (`type(uint256).max`).", + "params": { + "_maximumTotalAssets": "New value of the maximum total assets amount. It is the maximum amount of the tBTC token that the Acre protocol can hold.", + "_minimumDepositAmount": "New value of the minimum deposit amount. It is the minimum amount for a single deposit operation." + } + }, + "updateDispatcher(address)": { + "params": { + "newDispatcher": "Address of the new dispatcher contract." + } + }, + "updateTreasury(address)": { + "params": { + "newTreasury": "New treasury wallet address." + } + }, + "withdraw(uint256,address,address)": { + "details": "See {IERC4626-withdraw}. " + } + }, + "title": "stBTC", + "version": 1 + }, + "userdoc": { + "errors": { + "DisallowedAddress()": [ + { + "notice": "Reverts if the address is disallowed." + } + ], + "LessThanMinDeposit(uint256,uint256)": [ + { + "notice": "Reverts if the amount is less than the minimum deposit amount." + } + ], + "ZeroAddress()": [ + { + "notice": "Reverts if the address is zero." + } + ] + }, + "events": { + "DepositParametersUpdated(uint256,uint256)": { + "notice": "Emitted when deposit parameters are updated." + }, + "DispatcherUpdated(address,address)": { + "notice": "Emitted when the dispatcher contract is updated." + }, + "TreasuryUpdated(address)": { + "notice": "Emitted when the treasury wallet address is updated." + } + }, + "kind": "user", + "methods": { + "assetsBalanceOf(address)": { + "notice": "Returns value of assets that would be exchanged for the amount of shares owned by the `account`." + }, + "deposit(uint256,address)": { + "notice": "Mints shares to receiver by depositing exactly amount of tBTC tokens." + }, + "dispatcher()": { + "notice": "Dispatcher contract that routes tBTC from stBTC to a given vault and back." + }, + "maxDeposit(address)": { + "notice": "Returns the maximum amount of the tBTC token that can be deposited into the vault for the receiver through a deposit call. It takes into account the deposit parameter, maximum total assets, which determines the total amount of tBTC token held by Acre protocol." + }, + "maxMint(address)": { + "notice": "Returns the maximum amount of the vault shares that can be minted for the receiver, through a mint call." + }, + "maximumTotalAssets()": { + "notice": "Maximum total amount of tBTC token held by Acre protocol." + }, + "minimumDepositAmount()": { + "notice": "Minimum amount for a single deposit operation." + }, + "mint(uint256,address)": { + "notice": "Mints shares to receiver by depositing tBTC tokens." + }, + "treasury()": { + "notice": "Address of the treasury wallet, where fees should be transferred to." + }, + "updateDepositParameters(uint256,uint256)": { + "notice": "Updates deposit parameters." + }, + "updateDispatcher(address)": { + "notice": "Updates the dispatcher contract and gives it an unlimited allowance to transfer staked tBTC." + }, + "updateTreasury(address)": { + "notice": "Updates treasury wallet address." + } + }, + "notice": "This contract implements the ERC-4626 tokenized vault standard. By staking tBTC, users acquire a liquid staking token called stBTC, commonly referred to as \"shares\". The staked tBTC is securely deposited into Acre's vaults, where it generates yield over time. Users have the flexibility to redeem stBTC, enabling them to withdraw their staked tBTC along with the accrued yield.", + "version": 1 + }, + "storageLayout": { + "storage": [ + { + "astId": 3600, + "contract": "contracts/stBTC.sol:stBTC", + "label": "_balances", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 3606, + "contract": "contracts/stBTC.sol:stBTC", + "label": "_allowances", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))" + }, + { + "astId": 3608, + "contract": "contracts/stBTC.sol:stBTC", + "label": "_totalSupply", + "offset": 0, + "slot": "2", + "type": "t_uint256" + }, + { + "astId": 3610, + "contract": "contracts/stBTC.sol:stBTC", + "label": "_name", + "offset": 0, + "slot": "3", + "type": "t_string_storage" + }, + { + "astId": 3612, + "contract": "contracts/stBTC.sol:stBTC", + "label": "_symbol", + "offset": 0, + "slot": "4", + "type": "t_string_storage" + }, + { + "astId": 3131, + "contract": "contracts/stBTC.sol:stBTC", + "label": "_owner", + "offset": 0, + "slot": "5", + "type": "t_address" + }, + { + "astId": 6958, + "contract": "contracts/stBTC.sol:stBTC", + "label": "dispatcher", + "offset": 0, + "slot": "6", + "type": "t_contract(Dispatcher)6868" + }, + { + "astId": 6961, + "contract": "contracts/stBTC.sol:stBTC", + "label": "treasury", + "offset": 0, + "slot": "7", + "type": "t_address" + }, + { + "astId": 6964, + "contract": "contracts/stBTC.sol:stBTC", + "label": "minimumDepositAmount", + "offset": 0, + "slot": "8", + "type": "t_uint256" + }, + { + "astId": 6967, + "contract": "contracts/stBTC.sol:stBTC", + "label": "maximumTotalAssets", + "offset": 0, + "slot": "9", + "type": "t_uint256" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_contract(Dispatcher)6868": { + "encoding": "inplace", + "label": "contract Dispatcher", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_uint256)" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_string_storage": { + "encoding": "bytes", + "label": "string", + "numberOfBytes": "32" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } +} diff --git a/sdk/src/lib/ethereum/index.ts b/sdk/src/lib/ethereum/index.ts index 874aef6ad..64e487e6c 100644 --- a/sdk/src/lib/ethereum/index.ts +++ b/sdk/src/lib/ethereum/index.ts @@ -2,6 +2,7 @@ import { AcreContracts } from "../contracts" import { EthereumSigner } from "./contract" import { EthereumTBTCDepositor } from "./tbtc-depositor" import { EthereumNetwork } from "./network" +import { EthereumStBTC } from "./stbtc" export * from "./eip712-signer" export * from "./tbtc-depositor" @@ -13,8 +14,9 @@ function getEthereumContracts( network: EthereumNetwork, ): AcreContracts { const tbtcDepositor = new EthereumTBTCDepositor({ signer }, network) + const stBTC = new EthereumStBTC({ signer }, network) - return { tbtcDepositor } + return { tbtcDepositor, stBTC } } export { getEthereumContracts, EthereumNetwork } diff --git a/sdk/src/lib/ethereum/stbtc.ts b/sdk/src/lib/ethereum/stbtc.ts new file mode 100644 index 000000000..a5087f2a6 --- /dev/null +++ b/sdk/src/lib/ethereum/stbtc.ts @@ -0,0 +1,49 @@ +import { StBTC as StBTCTypechain } from "@acre-btc/core/typechain/contracts/StBTC" +import stBTC from "./artifacts/sepolia/stBTC.json" +import { + EthersContractConfig, + EthersContractDeployment, + EthersContractWrapper, +} from "./contract" +import { ChainIdentifier, StBTC } from "../contracts" +import { EthereumNetwork } from "./network" + +class EthereumStBTC + // @ts-expect-error TODO: Figure out why type generated by typechain does not + // satisfy the constraint `Contract`. Error: `Property '[internal]' is missing + // in type 'StBTC' but required in type 'BaseContract'`. + extends EthersContractWrapper + implements StBTC +{ + constructor(config: EthersContractConfig, network: EthereumNetwork) { + let artifact: EthersContractDeployment + + switch (network) { + case "sepolia": + artifact = stBTC + break + case "mainnet": + default: + throw new Error("Unsupported network") + } + + super(config, artifact) + } + + /** + * @see {StBTC#balanceOf} + */ + balanceOf(identifier: ChainIdentifier): Promise { + return this.instance.balanceOf(`0x${identifier.identifierHex}`) + } + + /** + * @see {StBTC#assetsBalanceOf} + */ + assetsBalanceOf(identifier: ChainIdentifier): Promise { + return this.instance.assetsBalanceOf(`0x${identifier.identifierHex}`) + } +} + +// eslint-disable-next-line import/prefer-default-export +export { EthereumStBTC } diff --git a/sdk/src/lib/ethereum/tbtc-depositor.ts b/sdk/src/lib/ethereum/tbtc-depositor.ts index d46ddd310..7ce185d09 100644 --- a/sdk/src/lib/ethereum/tbtc-depositor.ts +++ b/sdk/src/lib/ethereum/tbtc-depositor.ts @@ -1,5 +1,5 @@ import { packRevealDepositParameters } from "@keep-network/tbtc-v2.ts" -import { TbtcDepositor as TbtcDepositorTypechain } from "@acre-btc/core/typechain/contracts/TbtcDepositor" +import { AcreBitcoinDepositor as AcreBitcoinDepositorTypechain } from "@acre-btc/core/typechain/contracts/AcreBitcoinDepositor" import { ZeroAddress, dataSlice, @@ -26,14 +26,16 @@ import { EthereumNetwork } from "./network" import SepoliaTbtcDepositor from "./artifacts/sepolia/TbtcDepositor.json" +// TODO: Rename TBTCDepositor to AcreBitcoinDepositor + /** * Ethereum implementation of the TBTCDepositor. */ class EthereumTBTCDepositor // @ts-expect-error TODO: Figure out why type generated by typechain does not // satisfy the constraint `Contract`. Error: `Property '[internal]' is missing - // in type 'TbtcDepositor' but required in type 'Contract'`. - extends EthersContractWrapper + // in type 'AcreBitcoinDepositor' but required in type 'Contract'`. + extends EthersContractWrapper implements TBTCDepositor { constructor(config: EthersContractConfig, network: EthereumNetwork) { @@ -86,7 +88,7 @@ class EthereumTBTCDepositor const { staker, referral } = this.decodeExtraData(extraData) - const tx = await this.instance.initializeStakeRequest( + const tx = await this.instance.initializeStake( fundingTx, reveal, `0x${staker.identifierHex}`, diff --git a/sdk/src/modules/staking/index.ts b/sdk/src/modules/staking/index.ts index f4fd6b181..623f9a18d 100644 --- a/sdk/src/modules/staking/index.ts +++ b/sdk/src/modules/staking/index.ts @@ -62,6 +62,22 @@ class StakingModule { deposit, ) } + + /** + * @param identifier The generic chain identifier. + * @returns Value of the basis for calculating final BTC balance. + */ + sharesBalance(identifier: ChainIdentifier) { + return this.#contracts.stBTC.balanceOf(identifier) + } + + /** + * @param identifier The generic chain identifier. + * @returns Maximum withdraw value. + */ + estimatedBitcoinBalance(identifier: ChainIdentifier) { + return this.#contracts.stBTC.assetsBalanceOf(identifier) + } } export { StakingModule, StakeInitialization } diff --git a/sdk/test/lib/ethereum/stbtc.test.ts b/sdk/test/lib/ethereum/stbtc.test.ts new file mode 100644 index 000000000..4d6fa531d --- /dev/null +++ b/sdk/test/lib/ethereum/stbtc.test.ts @@ -0,0 +1,73 @@ +import ethers, { Contract } from "ethers" +import { EthereumStBTC } from "../../../src/lib/ethereum/stbtc" +import { EthereumAddress, EthereumSigner } from "../../../src" + +jest.mock("ethers", (): object => ({ + Contract: jest.fn(), + ...jest.requireActual("ethers"), +})) + +describe("stbtc", () => { + let stbtc: EthereumStBTC + const staker = EthereumAddress.from(ethers.Wallet.createRandom().address) + + const mockedContractInstance = { + balanceOf: jest.fn(), + assetsBalanceOf: jest.fn(), + } + + beforeAll(() => { + jest + .spyOn(ethers, "Contract") + .mockImplementationOnce( + () => mockedContractInstance as unknown as Contract, + ) + + stbtc = new EthereumStBTC( + { + signer: {} as EthereumSigner, + }, + "sepolia", + ) + }) + + describe("balanceOf", () => { + const expectedResult = 4294967295n + let result: bigint + + beforeAll(async () => { + mockedContractInstance.balanceOf.mockResolvedValue(expectedResult) + result = await stbtc.balanceOf(staker) + }) + + it("should call ethers contract instance", () => { + expect(mockedContractInstance.balanceOf).toHaveBeenCalledWith( + `0x${staker.identifierHex}`, + ) + }) + + it("should return balance of stBTC tokens", () => { + expect(result).toEqual(expectedResult) + }) + }) + + describe("assetsBalanceOf", () => { + const expectedResult = 4294967295n + let result: bigint + + beforeAll(async () => { + mockedContractInstance.assetsBalanceOf.mockResolvedValue(expectedResult) + result = await stbtc.assetsBalanceOf(staker) + }) + + it("should call ethers contract instance", () => { + expect(mockedContractInstance.assetsBalanceOf).toHaveBeenCalledWith( + `0x${staker.identifierHex}`, + ) + }) + + it("should return value of assets that would be exchanged for the amount of shares owned by the staker ", () => { + expect(result).toEqual(expectedResult) + }) + }) +}) diff --git a/sdk/test/lib/ethereum/tbtc-depositor.test.ts b/sdk/test/lib/ethereum/tbtc-depositor.test.ts index 2edca897f..a631bbefe 100644 --- a/sdk/test/lib/ethereum/tbtc-depositor.test.ts +++ b/sdk/test/lib/ethereum/tbtc-depositor.test.ts @@ -21,7 +21,7 @@ describe("TBTCDepositor", () => { const mockedContractInstance = { tbtcVault: jest.fn().mockImplementation(() => vaultAddress.identifierHex), - initializeStakeRequest: jest.fn(), + initializeStake: jest.fn(), } let depositor: EthereumTBTCDepositor let depositorAddress: EthereumAddress @@ -103,7 +103,7 @@ describe("TBTCDepositor", () => { let result: Hex beforeAll(async () => { - mockedContractInstance.initializeStakeRequest.mockReturnValue({ + mockedContractInstance.initializeStake.mockReturnValue({ hash: mockedTx.toPrefixedString(), }) @@ -154,9 +154,7 @@ describe("TBTCDepositor", () => { vault: `0x${vaultAddress.identifierHex}`, } - expect( - mockedContractInstance.initializeStakeRequest, - ).toHaveBeenCalledWith( + expect(mockedContractInstance.initializeStake).toHaveBeenCalledWith( btcTxInfo, revealInfo, `0x${staker.identifierHex}`, diff --git a/sdk/test/modules/staking.test.ts b/sdk/test/modules/staking.test.ts index ee355cb89..aeb64f1db 100644 --- a/sdk/test/modules/staking.test.ts +++ b/sdk/test/modules/staking.test.ts @@ -276,4 +276,43 @@ describe("Staking", () => { }) }) }) + + describe("sharesBalance", () => { + const { staker } = stakingModuleData.initializeStake + const expectedResult = 4294967295n + let result: bigint + + beforeAll(async () => { + contracts.stBTC.balanceOf = jest.fn().mockResolvedValue(expectedResult) + result = await staking.sharesBalance(staker) + }) + + it("should get balance of stBTC", () => { + expect(contracts.stBTC.balanceOf).toHaveBeenCalledWith(staker) + }) + + it("should return value of the basis for calculating final BTC balance", () => { + expect(result).toEqual(expectedResult) + }) + }) + + describe("estimatedBitcoinBalance", () => { + const expectedResult = 4294967295n + const { staker } = stakingModuleData.initializeStake + let result: bigint + beforeAll(async () => { + contracts.stBTC.assetsBalanceOf = jest + .fn() + .mockResolvedValue(expectedResult) + result = await staking.estimatedBitcoinBalance(staker) + }) + + it("should get staker's balance of tBTC tokens in vault ", () => { + expect(contracts.stBTC.assetsBalanceOf).toHaveBeenCalledWith(staker) + }) + + it("should return maximum withdraw value", () => { + expect(result).toEqual(expectedResult) + }) + }) }) diff --git a/sdk/test/utils/mock-acre-contracts.ts b/sdk/test/utils/mock-acre-contracts.ts index 111554b39..31e462f70 100644 --- a/sdk/test/utils/mock-acre-contracts.ts +++ b/sdk/test/utils/mock-acre-contracts.ts @@ -1,9 +1,11 @@ -import { AcreContracts, TBTCDepositor } from "../../src/lib/contracts" +import { AcreContracts, StBTC, TBTCDepositor } from "../../src/lib/contracts" // eslint-disable-next-line import/prefer-default-export export class MockAcreContracts implements AcreContracts { public readonly tbtcDepositor: TBTCDepositor + public readonly stBTC: StBTC + constructor() { this.tbtcDepositor = { getChainIdentifier: jest.fn(), @@ -12,5 +14,10 @@ export class MockAcreContracts implements AcreContracts { encodeExtraData: jest.fn(), revealDeposit: jest.fn(), } as TBTCDepositor + + this.stBTC = { + balanceOf: jest.fn(), + assetsBalanceOf: jest.fn(), + } as StBTC } }