diff --git a/.github/workflows/core.yaml b/.github/workflows/core.yaml index a0501e05b..6b431ba4e 100644 --- a/.github/workflows/core.yaml +++ b/.github/workflows/core.yaml @@ -13,7 +13,7 @@ defaults: working-directory: ./core jobs: - core-format: + core-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -30,10 +30,20 @@ jobs: - name: Install Dependencies run: pnpm install --prefer-offline --frozen-lockfile - - name: Format - run: pnpm run format + - name: Build + run: pnpm run build - core-build: + - name: Upload Build Artifacts + uses: actions/upload-artifact@v3 + with: + name: core-build + path: | + core/build/ + core/typechain/ + if-no-files-found: error + + core-format: + needs: [core-build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -50,17 +60,14 @@ jobs: - name: Install Dependencies run: pnpm install --prefer-offline --frozen-lockfile - - name: Build - run: pnpm run build - - - name: Upload Build Artifacts - uses: actions/upload-artifact@v3 + - name: Download Build Artifacts + uses: actions/download-artifact@v3 with: name: core-build - path: | - core/build/ - core/typechain/ - if-no-files-found: error + path: core/ + + - name: Format + run: pnpm run format core-slither: needs: [core-build] diff --git a/core/.eslintrc b/core/.eslintrc index 39419600b..8090228c6 100644 --- a/core/.eslintrc +++ b/core/.eslintrc @@ -13,5 +13,19 @@ ] } ] - } + }, + "overrides": [ + { + "files": ["deploy/*.ts"], + "rules": { + "@typescript-eslint/unbound-method": "off" + } + }, + { + "files": ["*.test.ts"], + "rules": { + "@typescript-eslint/no-unused-expressions": "off" + } + } + ] } diff --git a/core/.gitignore b/core/.gitignore index 38618816c..1f9e31a2d 100644 --- a/core/.gitignore +++ b/core/.gitignore @@ -3,4 +3,5 @@ build/ cache/ export.json export/ +gen/ typechain/ diff --git a/core/contracts/Acre.sol b/core/contracts/Acre.sol index 967896c3e..01545e1a2 100644 --- a/core/contracts/Acre.sol +++ b/core/contracts/Acre.sol @@ -2,6 +2,9 @@ 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 "./Dispatcher.sol"; /// @title Acre /// @notice This contract implements the ERC-4626 tokenized vault standard. By @@ -14,12 +17,160 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.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 Acre is ERC4626 { +contract Acre is ERC4626, Ownable { + using SafeERC20 for IERC20; + + /// Dispatcher contract that routes tBTC from Acre to a given vault and back. + Dispatcher public dispatcher; + + /// Address of the treasury wallet, where fees should be transferred to. + address public treasury; + + /// Minimum amount for a single deposit operation. The value should be set + /// low enough so the deposits routed through TbtcDepositor 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. + uint256 public maximumTotalAssets; + + /// Emitted when a referral is used. + /// @param referral Used for referral program. + /// @param assets Amount of tBTC tokens staked. event StakeReferral(uint16 indexed referral, uint256 assets); + /// Emitted when the treasury wallet address is updated. + /// @param treasury New treasury wallet address. + event TreasuryUpdated(address treasury); + + /// Emitted when deposit parameters are updated. + /// @param minimumDepositAmount New value of the minimum deposit amount. + /// @param maximumTotalAssets New value of the maximum total assets amount. + event DepositParametersUpdated( + uint256 minimumDepositAmount, + uint256 maximumTotalAssets + ); + + /// Emitted when the dispatcher contract is updated. + /// @param oldDispatcher Address of the old dispatcher contract. + /// @param newDispatcher Address of the new dispatcher contract. + event DispatcherUpdated(address oldDispatcher, address newDispatcher); + + /// Reverts if the amount is less than the minimum deposit amount. + /// @param amount Amount to check. + /// @param min Minimum amount to check 'amount' against. + error DepositAmountLessThanMin(uint256 amount, uint256 min); + + /// Reverts if the address is zero. + error ZeroAddress(); + constructor( - IERC20 tbtc - ) ERC4626(tbtc) ERC20("Acre Staked Bitcoin", "stBTC") {} + IERC20 _tbtc, + address _treasury + ) ERC4626(_tbtc) ERC20("Acre Staked Bitcoin", "stBTC") Ownable(msg.sender) { + treasury = _treasury; + // TODO: Revisit the exact values closer to the launch. + minimumDepositAmount = 0.001 * 1e18; // 0.001 tBTC + maximumTotalAssets = 25 * 1e18; // 25 tBTC + } + + /// @notice Updates treasury wallet address. + /// @param newTreasury New treasury wallet address. + function updateTreasury(address newTreasury) external onlyOwner { + // TODO: Introduce a parameters update process. + treasury = newTreasury; + + emit TreasuryUpdated(newTreasury); + } + + /// @notice Updates deposit parameters. + /// @dev To disable the limit for deposits, set the maximum total assets to + /// maximum (`type(uint256).max`). + /// @param _minimumDepositAmount New value of the minimum deposit amount. It + /// is the minimum amount for a single deposit operation. + /// @param _maximumTotalAssets New value of the maximum total assets amount. + /// It is the maximum amount of the tBTC token that the Acre can + /// hold. + function updateDepositParameters( + uint256 _minimumDepositAmount, + uint256 _maximumTotalAssets + ) external onlyOwner { + // TODO: Introduce a parameters update process. + minimumDepositAmount = _minimumDepositAmount; + maximumTotalAssets = _maximumTotalAssets; + + emit DepositParametersUpdated( + _minimumDepositAmount, + _maximumTotalAssets + ); + } + + // TODO: Implement a governed upgrade process that initiates an update and + // then finalizes it after a delay. + /// @notice Updates the dispatcher contract and gives it an unlimited + /// allowance to transfer staked tBTC. + /// @param newDispatcher Address of the new dispatcher contract. + function updateDispatcher(Dispatcher newDispatcher) external onlyOwner { + if (address(newDispatcher) == address(0)) { + revert ZeroAddress(); + } + + address oldDispatcher = address(dispatcher); + + emit DispatcherUpdated(oldDispatcher, address(newDispatcher)); + dispatcher = newDispatcher; + + // TODO: Once withdrawal/rebalancing is implemented, we need to revoke the + // approval of the vaults share tokens from the old dispatcher and approve + // a new dispatcher to manage the share tokens. + + if (oldDispatcher != address(0)) { + // Setting allowance to zero for the old dispatcher + IERC20(asset()).forceApprove(oldDispatcher, 0); + } + + // Setting allowance to max for the new dispatcher + IERC20(asset()).forceApprove(address(dispatcher), type(uint256).max); + } + + /// @notice Mints shares to receiver by depositing exactly amount of + /// tBTC tokens. + /// @dev 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. + /// @param assets Approved amount of tBTC tokens to deposit. + /// @param receiver The address to which the shares will be minted. + /// @return Minted shares. + function deposit( + uint256 assets, + address receiver + ) public override returns (uint256) { + if (assets < minimumDepositAmount) { + revert DepositAmountLessThanMin(assets, minimumDepositAmount); + } + + return super.deposit(assets, receiver); + } + + /// @notice Mints shares to receiver by depositing tBTC tokens. + /// @dev 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. + /// @param shares Amount of shares to mint. To get the amount of share use + /// `previewMint`. + /// @param receiver The address to which the shares will be minted. + function mint( + uint256 shares, + address receiver + ) public override returns (uint256 assets) { + if ((assets = super.mint(shares, receiver)) < minimumDepositAmount) { + revert DepositAmountLessThanMin(assets, minimumDepositAmount); + } + } /// @notice Stakes a given amount of tBTC token and mints shares to a /// receiver. @@ -43,4 +194,43 @@ contract Acre is ERC4626 { return shares; } + + /// @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. + /// @return The maximum amount of the tBTC token. + function maxDeposit(address) public view override returns (uint256) { + if (maximumTotalAssets == type(uint256).max) { + return type(uint256).max; + } + + uint256 _totalAssets = totalAssets(); + + return + _totalAssets >= maximumTotalAssets + ? 0 + : maximumTotalAssets - _totalAssets; + } + + /// @notice Returns the maximum amount of the vault shares that can be + /// minted for the receiver, through a mint call. + /// @dev Since the Acre contract limits the maximum total tBTC tokens this + /// function converts the maximum deposit amount to shares. + /// @return The maximum amount of the vault shares. + function maxMint(address receiver) public view override returns (uint256) { + uint256 _maxDeposit = maxDeposit(receiver); + + // slither-disable-next-line incorrect-equality + return + _maxDeposit == type(uint256).max + ? type(uint256).max + : convertToShares(_maxDeposit); + } + + /// @return Returns deposit parameters. + function depositParameters() public view returns (uint256, uint256) { + return (minimumDepositAmount, maximumTotalAssets); + } } diff --git a/core/contracts/Dispatcher.sol b/core/contracts/Dispatcher.sol index 309da2ae8..cbdc0dd0f 100644 --- a/core/contracts/Dispatcher.sol +++ b/core/contracts/Dispatcher.sol @@ -2,37 +2,91 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import "./Router.sol"; +import "./Acre.sol"; /// @title Dispatcher -/// @notice Dispatcher is a contract that routes TBTC from stBTC (Acre) to -/// a given vault and back. Vaults supply yield strategies with TBTC that +/// @notice Dispatcher is a contract that routes tBTC from Acre (stBTC) to +/// yield vaults and back. Vaults supply yield strategies with tBTC that /// generate yield for Bitcoin holders. -contract Dispatcher is Ownable { - error VaultAlreadyAuthorized(); - error VaultUnauthorized(); +contract Dispatcher is Router, Ownable { + using SafeERC20 for IERC20; + /// Struct holds information about a vault. struct VaultInfo { bool authorized; } - /// @notice Authorized Yield Vaults that implement ERC4626 standard. These - /// vaults deposit assets to yield strategies, e.g. Uniswap V3 - /// WBTC/TBTC pool. Vault can be a part of Acre ecosystem or can be - /// implemented externally. As long as it complies with ERC4626 - /// standard and is authorized by the owner it can be plugged into - /// Acre. + /// The main Acre contract holding tBTC deposited by stakers. + Acre public immutable acre; + /// tBTC token contract. + IERC20 public immutable tbtc; + /// Address of the maintainer bot. + address public maintainer; + + /// Authorized Yield Vaults that implement ERC4626 standard. These + /// vaults deposit assets to yield strategies, e.g. Uniswap V3 + /// WBTC/TBTC pool. Vault can be a part of Acre ecosystem or can be + /// implemented externally. As long as it complies with ERC4626 + /// standard and is authorized by the owner it can be plugged into + /// Acre. address[] public vaults; + /// Mapping of vaults to their information. mapping(address => VaultInfo) public vaultsInfo; + /// Emitted when a vault is authorized. + /// @param vault Address of the vault. event VaultAuthorized(address indexed vault); + + /// Emitted when a vault is deauthorized. + /// @param vault Address of the vault. event VaultDeauthorized(address indexed vault); - constructor() Ownable(msg.sender) {} + /// Emitted when tBTC is routed to a vault. + /// @param vault Address of the vault. + /// @param amount Amount of tBTC. + /// @param sharesOut Amount of shares received by Acre. + event DepositAllocated( + address indexed vault, + uint256 amount, + uint256 sharesOut + ); + + /// Emitted when the maintainer address is updated. + /// @param maintainer Address of the new maintainer. + event MaintainerUpdated(address indexed maintainer); + + /// Reverts if the vault is already authorized. + error VaultAlreadyAuthorized(); + + /// Reverts if the vault is not authorized. + error VaultUnauthorized(); + + /// Reverts if the caller is not the maintainer. + error NotMaintainer(); + + /// Reverts if the address is zero. + error ZeroAddress(); + + /// Modifier that reverts if the caller is not the maintainer. + modifier onlyMaintainer() { + if (msg.sender != maintainer) { + revert NotMaintainer(); + } + _; + } + + constructor(Acre _acre, IERC20 _tbtc) Ownable(msg.sender) { + acre = _acre; + tbtc = _tbtc; + } /// @notice Adds a vault to the list of authorized vaults. /// @param vault Address of the vault to add. function authorizeVault(address vault) external onlyOwner { - if (vaultsInfo[vault].authorized) { + if (isVaultAuthorized(vault)) { revert VaultAlreadyAuthorized(); } @@ -45,7 +99,7 @@ contract Dispatcher is Ownable { /// @notice Removes a vault from the list of authorized vaults. /// @param vault Address of the vault to remove. function deauthorizeVault(address vault) external onlyOwner { - if (!vaultsInfo[vault].authorized) { + if (!isVaultAuthorized(vault)) { revert VaultUnauthorized(); } @@ -63,7 +117,58 @@ contract Dispatcher is Ownable { emit VaultDeauthorized(vault); } - function getVaults() external view returns (address[] memory) { + /// @notice Updates the maintainer address. + /// @param newMaintainer Address of the new maintainer. + function updateMaintainer(address newMaintainer) external onlyOwner { + if (newMaintainer == address(0)) { + revert ZeroAddress(); + } + + maintainer = newMaintainer; + + emit MaintainerUpdated(maintainer); + } + + /// TODO: make this function internal once the allocation distribution is + /// implemented + /// @notice Routes tBTC from Acre to a vault. Can be called by the maintainer + /// only. + /// @param vault Address of the vault to route the assets to. + /// @param amount Amount of tBTC to deposit. + /// @param minSharesOut Minimum amount of shares to receive by Acre. + function depositToVault( + address vault, + uint256 amount, + uint256 minSharesOut + ) public onlyMaintainer { + if (!isVaultAuthorized(vault)) { + revert VaultUnauthorized(); + } + + // slither-disable-next-line arbitrary-send-erc20 + tbtc.safeTransferFrom(address(acre), address(this), amount); + tbtc.forceApprove(address(vault), amount); + + uint256 sharesOut = deposit( + IERC4626(vault), + address(acre), + amount, + minSharesOut + ); + // slither-disable-next-line reentrancy-events + emit DepositAllocated(vault, amount, sharesOut); + } + + /// @notice Returns the list of authorized vaults. + function getVaults() public view returns (address[] memory) { return vaults; } + + /// @notice Returns true if the vault is authorized. + /// @param vault Address of the vault to check. + function isVaultAuthorized(address vault) public view returns (bool) { + return vaultsInfo[vault].authorized; + } + + /// TODO: implement redeem() / withdraw() functions } diff --git a/core/contracts/Router.sol b/core/contracts/Router.sol new file mode 100644 index 000000000..6d07a22a2 --- /dev/null +++ b/core/contracts/Router.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +/// @title Router +/// @notice Router is a contract that routes tBTC from stBTC (Acre) to +/// a given vault and back. Vaults supply yield strategies with tBTC that +/// generate yield for Bitcoin holders. +abstract contract Router { + /// Thrown when amount of shares received is below the min set by caller. + /// @param vault Address of the vault. + /// @param sharesOut Amount of shares received by Acre. + /// @param minSharesOut Minimum amount of shares expected to receive. + error MinSharesError( + address vault, + uint256 sharesOut, + uint256 minSharesOut + ); + + /// @notice Routes funds from stBTC (Acre) to a vault. The amount of tBTC to + /// Shares of deposited tBTC are minted to the stBTC contract. + /// @param vault Address of the vault to route the funds to. + /// @param receiver Address of the receiver of the shares. + /// @param amount Amount of tBTC to deposit. + /// @param minSharesOut Minimum amount of shares to receive. + function deposit( + IERC4626 vault, + address receiver, + uint256 amount, + uint256 minSharesOut + ) internal returns (uint256 sharesOut) { + if ((sharesOut = vault.deposit(amount, receiver)) < minSharesOut) { + revert MinSharesError(address(vault), sharesOut, minSharesOut); + } + } +} diff --git a/core/contracts/tbtc/TbtcDepositor.sol b/core/contracts/tbtc/TbtcDepositor.sol index b149ef4bc..19b5a2375 100644 --- a/core/contracts/tbtc/TbtcDepositor.sol +++ b/core/contracts/tbtc/TbtcDepositor.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.21; import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.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"; @@ -15,7 +16,7 @@ import {Acre} from "../Acre.sol"; /// @title tBTC Depositor contract. /// @notice The contract integrates Acre staking with tBTC minting. -/// User who want to stake BTC in Acre should submit a Bitcoin transaction +/// 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, @@ -33,7 +34,7 @@ import {Acre} from "../Acre.sol"; /// After tBTC is minted to the Depositor, on the stake finalization /// tBTC is staked in Acre contract and stBTC shares are emitted to the /// receiver pointed by the staker. -contract TbtcDepositor { +contract TbtcDepositor is Ownable { using BTCUtils for bytes; using SafeERC20 for IERC20; @@ -67,20 +68,56 @@ contract TbtcDepositor { /// token units (18 decimals precision). uint256 public constant SATOSHI_MULTIPLIER = 10 ** 10; - // TODO: Decide if leave or remove? - uint64 public minimumFundingTransactionAmount; + /// @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 identifier. + /// @param caller Address that initialized the stake request. + /// @param receiver The address to which the stBTC shares will be minted. + /// @param referral Data used for referral program. + event StakeInitialized( + uint256 indexed depositKey, + address indexed caller, + address receiver, + uint16 referral + ); + + /// @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 identifier. + /// @param caller Address that finalized the stake request. + event StakeFinalized(uint256 indexed depositKey, address indexed caller); + + /// @notice Emitted when a depositor fee divisor is updated. + /// @param depositorFeeDivisor New value of the depositor fee divisor. + event DepositorFeeDivisorUpdated(uint64 depositorFeeDivisor); /// @dev Receiver address is zero. error ReceiverIsZeroAddress(); + /// @dev Attempted to initiate a stake request that was already initialized. error StakeRequestAlreadyInProgress(); + /// @dev Attempted to finalize a stake request that has not been initialized. error StakeRequestNotInitialized(); + /// @dev Attempted to finalize a stake request that was already finalized. error StakeRequestAlreadyFinalized(); + /// @dev Depositor address stored in the Deposit Request in the tBTC Bridge /// contract doesn't match the current contract address. error UnexpectedDepositor(address bridgeDepositRequestDepositor); + /// @dev Deposit was not completed on the tBTC side and tBTC was not minted /// to the depositor contract. It is thrown when the deposit neither has /// been optimistically minted nor swept. @@ -90,10 +127,16 @@ contract TbtcDepositor { /// @param _bridge tBTC Bridge contract instance. /// @param _tbtcVault tBTC Vault contract instance. /// @param _acre Acre contract instance. - constructor(IBridge _bridge, ITBTCVault _tbtcVault, Acre _acre) { + constructor( + IBridge _bridge, + ITBTCVault _tbtcVault, + Acre _acre + ) Ownable(msg.sender) { bridge = _bridge; tbtcVault = _tbtcVault; acre = _acre; + + depositorFeeDivisor = 0; // Depositor fee is disabled initially. } /// @notice This function allows staking process initialization for a Bitcoin @@ -144,12 +187,16 @@ contract TbtcDepositor { ) .hash256View(); - StakeRequest storage request = stakeRequests[ - calculateDepositKey(fundingTxHash, reveal.fundingOutputIndex) - ]; + uint256 depositKey = calculateDepositKey( + fundingTxHash, + reveal.fundingOutputIndex + ); + StakeRequest storage request = stakeRequests[depositKey]; if (request.requestedAt > 0) revert StakeRequestAlreadyInProgress(); + emit StakeInitialized(depositKey, msg.sender, receiver, referral); + // solhint-disable-next-line not-rely-on-time request.requestedAt = uint64(block.timestamp); @@ -170,14 +217,16 @@ contract TbtcDepositor { /// request, after tBTC minting process completed and tBTC was deposited /// in this Depositor contract. /// @dev It calculates the amount to stake in Acre contract by deducting - /// tBTC network minting fees from the initial funding transaction amount. - /// The amount to stake is calculated depending on the process the tBTC - /// was minted in: + /// tBTC protocol minting fee and the Depositor fee from the initial + /// funding transaction amount. + /// + /// The tBTC protocol minting fee is calculated depending on the process + /// the tBTC was minted in: /// - for swept deposits: /// `amount = depositAmount - depositTreasuryFee - depositTxMaxFee` /// - for optimistically minted deposits: /// ``` - /// amount = depositAmount - depositTreasuryFee - depositTxMaxFee + /// amount = depositAmount - depositTreasuryFee - depositTxMaxFee /// - optimisticMintingFee /// ``` /// These calculation are simplified and can leave some positive @@ -188,9 +237,12 @@ contract TbtcDepositor { /// at the moment of the deposit reveal, there is a chance that the fee /// parameter is updated in the tBTC Vault contract before the optimistic /// minting is finalized. - /// The imbalance should be donated to the Acre staking contract by the - /// Governance. - /// @param depositKey Deposit key computed as + /// The imbalance is left in the tBTC Depositor contract. + /// + /// The Depositor fee is computed based on the `depositorFeeDivisor` + /// parameter. The fee is transferred to the treasury wallet on the + /// stake request finalization. + /// @param depositKey Deposit key computed as /// `keccak256(fundingTxHash | fundingOutputIndex)`. function finalizeStake(uint256 depositKey) external { StakeRequest storage request = stakeRequests[depositKey]; @@ -211,25 +263,30 @@ contract TbtcDepositor { // Check if Depositor revealed to the tBTC Bridge contract matches the // current contract address. + // This is very unlikely scenario, that would require unexpected change or + // bug in tBTC Bridge contract, as the depositor is set automatically + // to the reveal deposit message sender, which will be this contract. + // Anyway we check if the depositor that got the tBTC tokens minted + // is this contract, before we stake them. if (bridgeDepositRequest.depositor != address(this)) revert UnexpectedDepositor(bridgeDepositRequest.depositor); // Extract funding transaction amount sent by the user in Bitcoin transaction. uint256 fundingTxAmount = bridgeDepositRequest.amount; - uint256 amountToStakeSat = (fundingTxAmount - - bridgeDepositRequest.treasuryFee - - request.tbtcDepositTxMaxFee); + // Estimate tBTC protocol fees for minting. + uint256 tbtcMintingFees = bridgeDepositRequest.treasuryFee + + request.tbtcDepositTxMaxFee; // Check if deposit was optimistically minted. if (optimisticMintingRequest.finalizedAt > 0) { // For tBTC minted with optimistic minting process additional fee - // is taken. The fee is calculated on `TBTCVautl.finalizeOptimisticMint` + // is taken. The fee is calculated on `TBTCVault.finalizeOptimisticMint` // call, and not stored in the contract. // There is a possibility the fee has changed since the snapshot of // the `tbtcOptimisticMintingFeeDivisor`, to cover this scenario - // in fee computation we use the bigger of these. - uint256 optimisticMintingFeeDivisor = Math.max( + // we want to assume the bigger fee, so we use the smaller divisor. + uint256 optimisticMintingFeeDivisor = Math.min( request.tbtcOptimisticMintingFeeDivisor, tbtcVault.optimisticMintingFeeDivisor() ); @@ -238,30 +295,58 @@ contract TbtcDepositor { ? (fundingTxAmount / optimisticMintingFeeDivisor) : 0; - amountToStakeSat -= optimisticMintingFee; + tbtcMintingFees += optimisticMintingFee; } else { - // If the deposit wan't optimistically minted check if it was swept. + // If the deposit wasn't optimistically minted check if it was swept. if (bridgeDepositRequest.sweptAt == 0) revert TbtcDepositNotCompleted(); } + // Compute depositor fee. + uint256 depositorFee = depositorFeeDivisor > 0 + ? (fundingTxAmount / depositorFeeDivisor) + : 0; + + // Calculate tBTC amount available to stake after subtracting all the fees. // Convert amount in satoshi to tBTC token precision. - uint256 amountToStakeTbtc = amountToStakeSat * SATOSHI_MULTIPLIER; + uint256 amountToStakeTbtc = (fundingTxAmount - + tbtcMintingFees - + depositorFee) * SATOSHI_MULTIPLIER; - // Fetch receiver and referral stored in extra data in tBTC Bridge Deposit + // Fetch receiver and referral stored in extra data in tBTC Bridge Deposit. // Request. bytes32 extraData = bridgeDepositRequest.extraData; (address receiver, uint16 referral) = decodeExtraData(extraData); + emit StakeFinalized(depositKey, msg.sender); + + // Transfer depositor fee to the treasury wallet. + if (depositorFee > 0) { + IERC20(acre.asset()).safeTransfer(acre.treasury(), depositorFee); + } + // Stake tBTC in Acre. IERC20(acre.asset()).safeIncreaseAllowance( address(acre), amountToStakeTbtc ); + // TODO: Figure out what to do if deposit limit is reached in Acre // TODO: Consider extracting stake function with referrals from Acre to this contract. acre.stake(amountToStakeTbtc, receiver, referral); } + + /// @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); + } + // TODO: Handle minimum deposit amount in tBTC Bridge vs Acre. /// @notice Calculates deposit key the same way as the Bridge contract. @@ -294,7 +379,7 @@ contract TbtcDepositor { return bytes32(abi.encodePacked(receiver, referral)); } - /// @notice Decodes receiver address and referral from extera data, + /// @notice Decodes receiver address and referral from extra data, /// @dev Unpacks the data from bytes32: 20 bytes of receiver address and /// 2 bytes of referral, 10 bytes of trailing zeros. /// @param extraData Encoded extra data. diff --git a/core/contracts/test/BridgeStub.sol b/core/contracts/test/BridgeStub.sol new file mode 100644 index 000000000..2e6213134 --- /dev/null +++ b/core/contracts/test/BridgeStub.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol"; +import {IBridge} from "../external/tbtc/IBridge.sol"; +import {TestERC20} from "./TestERC20.sol"; + +contract BridgeStub is IBridge { + using BTCUtils for bytes; + + TestERC20 public tbtc; + + /// @notice Multiplier to convert satoshi to TBTC token units. + uint256 public constant SATOSHI_MULTIPLIER = 10 ** 10; + + // The values set here are tweaked for test purposes. These are not + // the exact values used in the Bridge contract on mainnet. + // See: https://github.com/keep-network/tbtc-v2/blob/103411a595c33895ff6bff8457383a69eca4963c/solidity/test/bridge/Bridge.Deposit.test.ts#L70-L77 + uint64 public depositDustThreshold = 10000; // 10000 satoshi = 0.0001 BTC + uint64 public depositTreasuryFeeDivisor = 2000; // 1/2000 == 5bps == 0.05% == 0.0005 + uint64 public depositTxMaxFee = 1000; // 1000 satoshi = 0.00001 BTC + uint32 public depositRevealAheadPeriod = 15 days; + + mapping(uint256 => DepositRequest) internal depositsMap; + + constructor(TestERC20 _tbtc) { + tbtc = _tbtc; + } + + function revealDepositWithExtraData( + BitcoinTxInfo calldata fundingTx, + DepositRevealInfo calldata reveal, + bytes32 extraData + ) external { + bytes32 fundingTxHash = abi + .encodePacked( + fundingTx.version, + fundingTx.inputVector, + fundingTx.outputVector, + fundingTx.locktime + ) + .hash256View(); + + DepositRequest storage deposit = depositsMap[ + calculateDepositKey(fundingTxHash, reveal.fundingOutputIndex) + ]; + + require(deposit.revealedAt == 0, "Deposit already revealed"); + + bytes memory fundingOutput = fundingTx + .outputVector + .extractOutputAtIndex(reveal.fundingOutputIndex); + + uint64 fundingOutputAmount = fundingOutput.extractValue(); + + require( + fundingOutputAmount >= depositDustThreshold, + "Deposit amount too small" + ); + + deposit.amount = fundingOutputAmount; + deposit.depositor = msg.sender; + /* solhint-disable-next-line not-rely-on-time */ + deposit.revealedAt = uint32(block.timestamp); + deposit.vault = reveal.vault; + deposit.treasuryFee = depositTreasuryFeeDivisor > 0 + ? fundingOutputAmount / depositTreasuryFeeDivisor + : 0; + deposit.extraData = extraData; + } + + function deposits( + uint256 depositKey + ) external view returns (DepositRequest memory) { + return depositsMap[depositKey]; + } + + function sweep(bytes32 fundingTxHash, uint32 fundingOutputIndex) external { + DepositRequest storage deposit = depositsMap[ + calculateDepositKey(fundingTxHash, fundingOutputIndex) + ]; + + // solhint-disable-next-line not-rely-on-time + deposit.sweptAt = uint32(block.timestamp); + + (, , uint64 depositTxMaxFee, ) = depositParameters(); + // For test purposes as deposit tx fee we take value lower than the max + // possible value as this follows how Bridge may sweep the deposit + // with a fee lower than the max. + // Here we arbitrary choose the deposit tx fee to be at 80% of max deposit fee. + uint64 depositTxFee = (depositTxMaxFee * 8) / 10; + + uint64 amountToMintSat = deposit.amount - + deposit.treasuryFee - + depositTxFee; + + tbtc.mint(deposit.depositor, amountToMintSat * SATOSHI_MULTIPLIER); + } + + function depositParameters() + public + view + returns (uint64, uint64, uint64, uint32) + { + return ( + depositDustThreshold, + depositTreasuryFeeDivisor, + depositTxMaxFee, + depositRevealAheadPeriod + ); + } + + function calculateDepositKey( + bytes32 fundingTxHash, + uint32 fundingOutputIndex + ) private pure returns (uint256) { + return + uint256( + keccak256(abi.encodePacked(fundingTxHash, fundingOutputIndex)) + ); + } + + function setDepositDustThreshold(uint64 _depositDustThreshold) external { + depositDustThreshold = _depositDustThreshold; + } + + function setDepositTreasuryFeeDivisor( + uint64 _depositTreasuryFeeDivisor + ) external { + depositTreasuryFeeDivisor = _depositTreasuryFeeDivisor; + } + + function setDepositTxMaxFee(uint64 _depositTxMaxFee) external { + depositTxMaxFee = _depositTxMaxFee; + } + + function setDepositor(uint256 depositKey, address _depositor) external { + DepositRequest storage deposit = depositsMap[depositKey]; + deposit.depositor = _depositor; + } +} diff --git a/core/contracts/test/TBTCVaultStub.sol b/core/contracts/test/TBTCVaultStub.sol new file mode 100644 index 000000000..6d38ae4bd --- /dev/null +++ b/core/contracts/test/TBTCVaultStub.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; +import {ITBTCVault} from "../tbtc/TbtcDepositor.sol"; +import {IBridge} from "../external/tbtc/IBridge.sol"; +import {TestERC20} from "./TestERC20.sol"; + +contract TBTCVaultStub is ITBTCVault { + TestERC20 public tbtc; + IBridge public bridge; + + /// @notice Multiplier to convert satoshi to TBTC token units. + uint256 public constant SATOSHI_MULTIPLIER = 10 ** 10; + + uint32 public optimisticMintingFeeDivisor = 500; // 1/500 = 0.002 = 0.2% + + mapping(uint256 => OptimisticMintingRequest) public requests; + + constructor(TestERC20 _tbtc, IBridge _bridge) { + tbtc = _tbtc; + bridge = _bridge; + } + + function optimisticMintingRequests( + uint256 depositKey + ) external view returns (OptimisticMintingRequest memory) { + return requests[depositKey]; + } + + function mintTbtc(address account, uint256 valueSat) public { + tbtc.mint(account, valueSat * SATOSHI_MULTIPLIER); + } + + function finalizeOptimisticMinting(uint256 depositKey) external { + OptimisticMintingRequest storage request = requests[depositKey]; + + IBridge.DepositRequest memory deposit = bridge.deposits(depositKey); + + uint256 amountToMint = (deposit.amount - deposit.treasuryFee) * + SATOSHI_MULTIPLIER; + + uint256 optimisticMintFee = optimisticMintingFeeDivisor > 0 + ? (amountToMint / optimisticMintingFeeDivisor) + : 0; + + tbtc.mint(deposit.depositor, amountToMint - optimisticMintFee); + + /* solhint-disable-next-line not-rely-on-time */ + request.finalizedAt = uint64(block.timestamp); + } + + function setOptimisticMintingFeeDivisor( + uint32 _optimisticMintingFeeDivisor + ) public { + optimisticMintingFeeDivisor = _optimisticMintingFeeDivisor; + } +} 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/contracts/test/TestERC4626.sol b/core/contracts/test/TestERC4626.sol new file mode 100644 index 000000000..acf09928e --- /dev/null +++ b/core/contracts/test/TestERC4626.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; + +contract TestERC4626 is ERC4626 { + constructor( + IERC20 asset, + string memory tokenName, + string memory tokenSymbol + ) ERC4626(asset) ERC20(tokenName, tokenSymbol) {} +} diff --git a/core/deploy/00_resolve_tbtc_bridge.ts b/core/deploy/00_resolve_tbtc_bridge.ts new file mode 100644 index 000000000..4b69a2343 --- /dev/null +++ b/core/deploy/00_resolve_tbtc_bridge.ts @@ -0,0 +1,37 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { + HardhatNetworkConfig, + HardhatRuntimeEnvironment, +} from "hardhat/types" +import { isNonZeroAddress } from "../helpers/address" + +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") + + const tbtc = await deployments.get("TBTC") + + await deployments.deploy("Bridge", { + contract: "BridgeStub", + args: [tbtc.address], + from: deployer, + log: true, + waitConfirmations: 1, + }) + } +} + +export default func + +func.tags = ["TBTC", "Bridge"] +func.dependencies = ["TBTCToken"] diff --git a/core/deploy/00_resolve_tbtc.ts b/core/deploy/00_resolve_tbtc_token.ts similarity index 72% rename from core/deploy/00_resolve_tbtc.ts rename to core/deploy/00_resolve_tbtc_token.ts index dc1bfeff5..3eb9ce90c 100644 --- a/core/deploy/00_resolve_tbtc.ts +++ b/core/deploy/00_resolve_tbtc_token.ts @@ -1,5 +1,8 @@ -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" const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { @@ -11,13 +14,17 @@ 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, @@ -27,4 +34,4 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { 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..8088f69e1 --- /dev/null +++ b/core/deploy/00_resolve_tbtc_vault.ts @@ -0,0 +1,41 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { + HardhatNetworkConfig, + HardhatRuntimeEnvironment, +} from "hardhat/types" +import { isNonZeroAddress } from "../helpers/address" + +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: 1, + }) + } +} + +export default func + +func.tags = ["TBTC", "TBTCVault"] +func.dependencies = ["TBTCToken", "Bridge"] diff --git a/core/deploy/00_resolve_testing_erc4626.ts b/core/deploy/00_resolve_testing_erc4626.ts new file mode 100644 index 000000000..d1e82b665 --- /dev/null +++ b/core/deploy/00_resolve_testing_erc4626.ts @@ -0,0 +1,27 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { log } = deployments + const { deployer } = await getNamedAccounts() + const tBTC = await deployments.get("TBTC") + + log("deploying Mock ERC4626 Vault") + + await deployments.deploy("Vault", { + contract: "TestERC4626", + from: deployer, + args: [tBTC.address, "MockVault", "MV"], + log: true, + waitConfirmations: 1, + }) +} + +export default func + +func.tags = ["TestERC4626"] +func.dependencies = ["TBTC"] + +func.skip = async (hre: HardhatRuntimeEnvironment): Promise => + Promise.resolve(hre.network.name === "mainnet") diff --git a/core/deploy/01_deploy_acre.ts b/core/deploy/01_deploy_acre.ts index 531fc6c12..69d331009 100644 --- a/core/deploy/01_deploy_acre.ts +++ b/core/deploy/01_deploy_acre.ts @@ -3,13 +3,13 @@ import type { DeployFunction } from "hardhat-deploy/types" const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, deployments } = hre - const { deployer } = await getNamedAccounts() + const { deployer, treasury } = await getNamedAccounts() const tbtc = await deployments.get("TBTC") await deployments.deploy("Acre", { from: deployer, - args: [tbtc.address], + args: [tbtc.address, treasury], log: true, waitConfirmations: 1, }) diff --git a/core/deploy/02_deploy_acre_router.ts b/core/deploy/02_deploy_dispatcher.ts similarity index 80% rename from core/deploy/02_deploy_acre_router.ts rename to core/deploy/02_deploy_dispatcher.ts index bf99d4d73..3c7a84e5c 100644 --- a/core/deploy/02_deploy_acre_router.ts +++ b/core/deploy/02_deploy_dispatcher.ts @@ -5,9 +5,12 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, deployments } = hre const { deployer } = await getNamedAccounts() + const tbtc = await deployments.get("TBTC") + const acre = await deployments.get("Acre") + await deployments.deploy("Dispatcher", { from: deployer, - args: [], + args: [acre.address, tbtc.address], log: true, waitConfirmations: 1, }) diff --git a/core/deploy/03_deploy_tbtc_depositor.ts b/core/deploy/03_deploy_tbtc_depositor.ts new file mode 100644 index 000000000..9d81cf364 --- /dev/null +++ b/core/deploy/03_deploy_tbtc_depositor.ts @@ -0,0 +1,26 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { HardhatRuntimeEnvironment } from "hardhat/types" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { deployer } = await getNamedAccounts() + + const bridge = await deployments.get("Bridge") + const tbtcVault = await deployments.get("TBTCVault") + const acre = await deployments.get("Acre") + + await deployments.deploy("TbtcDepositor", { + from: deployer, + args: [bridge.address, tbtcVault.address, acre.address], + log: true, + waitConfirmations: 1, + }) + + // TODO: Add Etherscan verification + // TODO: Add Tenderly verification +} + +export default func + +func.tags = ["TbtcDepositor"] +func.dependencies = ["TBTC", "Acre"] diff --git a/core/deploy/11_acre_update_dispatcher.ts b/core/deploy/11_acre_update_dispatcher.ts new file mode 100644 index 000000000..2f645027c --- /dev/null +++ b/core/deploy/11_acre_update_dispatcher.ts @@ -0,0 +1,21 @@ +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 } = await getNamedAccounts() + + const dispatcher = await deployments.get("Dispatcher") + + await deployments.execute( + "Acre", + { from: deployer, log: true, waitConfirmations: 1 }, + "updateDispatcher", + dispatcher.address, + ) +} + +export default func + +func.tags = ["AcreUpdateDispatcher"] +func.dependencies = ["Acre", "Dispatcher"] diff --git a/core/deploy/12_dispatcher_update_maintainer.ts b/core/deploy/12_dispatcher_update_maintainer.ts new file mode 100644 index 000000000..8f616fac0 --- /dev/null +++ b/core/deploy/12_dispatcher_update_maintainer.ts @@ -0,0 +1,19 @@ +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, maintainer } = await getNamedAccounts() + + await deployments.execute( + "Dispatcher", + { from: deployer, log: true, waitConfirmations: 1 }, + "updateMaintainer", + maintainer, + ) +} + +export default func + +func.tags = ["DispatcherUpdateMaintainer"] +func.dependencies = ["Dispatcher"] diff --git a/core/deploy/21_transfer_ownership_acre.ts b/core/deploy/21_transfer_ownership_acre.ts index c62708641..f903996a8 100644 --- a/core/deploy/21_transfer_ownership_acre.ts +++ b/core/deploy/21_transfer_ownership_acre.ts @@ -19,5 +19,4 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func func.tags = ["TransferOwnershipAcre"] -// TODO: Enable once Acre extends Ownable -func.skip = async () => true +func.dependencies = ["Acre"] diff --git a/core/deploy/23_transfer_ownership_tbtc_depositor.ts b/core/deploy/23_transfer_ownership_tbtc_depositor.ts new file mode 100644 index 000000000..d448b30a5 --- /dev/null +++ b/core/deploy/23_transfer_ownership_tbtc_depositor.ts @@ -0,0 +1,22 @@ +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 TbtcDepositor contract to ${governance}`) + + await deployments.execute( + "TbtcDepositor", + { from: deployer, log: true, waitConfirmations: 1 }, + "transferOwnership", + governance, + ) +} + +export default func + +func.tags = ["TransferOwnershipAcre"] +func.dependencies = ["TbtcDepositor"] diff --git a/core/hardhat.config.ts b/core/hardhat.config.ts index 264e9fcbd..df0ebebd1 100644 --- a/core/hardhat.config.ts +++ b/core/hardhat.config.ts @@ -3,6 +3,7 @@ import type { HardhatUserConfig } from "hardhat/config" import "@nomicfoundation/hardhat-toolbox" import "hardhat-contract-sizer" import "hardhat-deploy" +import "solidity-docgen" const config: HardhatUserConfig = { solidity: { @@ -62,13 +63,23 @@ const config: HardhatUserConfig = { namedAccounts: { deployer: { default: 1, - sepolia: 0, - mainnet: "", + sepolia: 0, // TODO: updated to the actual address once available + mainnet: "", // TODO: updated to the actual address once available }, governance: { default: 2, - sepolia: 0, - mainnet: "", + sepolia: 0, // TODO: updated to the actual address once available + mainnet: "", // TODO: updated to the actual address once available + }, + treasury: { + default: 3, + sepolia: 0, // TODO: updated to the actual address once available + mainnet: "", // TODO: updated to the actual address once available + }, + maintainer: { + default: 4, + sepolia: 0, // TODO: updated to the actual address once available + mainnet: "", // TODO: updated to the actual address once available }, }, @@ -86,6 +97,13 @@ const config: HardhatUserConfig = { typechain: { outDir: "typechain", }, + + docgen: { + outputDir: "./gen/docs", + pages: "files", + exclude: ["external/", "test/"], + collapseNewlines: true, + }, } export default config diff --git a/core/package.json b/core/package.json index e6c3e0de3..ce300e6b8 100644 --- a/core/package.json +++ b/core/package.json @@ -14,9 +14,10 @@ "export.json" ], "scripts": { - "clean": "hardhat clean && rm -rf cache/ export/ export.json", + "clean": "hardhat clean && rm -rf cache/ export/ gen/ export.json", "build": "hardhat compile", "deploy": "hardhat deploy --export export.json", + "docs": "hardhat docgen", "format": "npm run lint:js && npm run lint:sol && npm run lint:config", "format:fix": "npm run lint:js:fix && npm run lint:sol:fix && npm run lint:config:fix", "lint:js": "eslint .", @@ -35,7 +36,7 @@ "@nomicfoundation/hardhat-verify": "^2.0.1", "@nomiclabs/hardhat-etherscan": "^3.1.7", "@openzeppelin/hardhat-upgrades": "^2.4.1", - "@thesis-co/eslint-config": "^0.6.1", + "@thesis-co/eslint-config": "github:thesis/eslint-config#7b9bc8c", "@typechain/ethers-v6": "^0.5.1", "@typechain/hardhat": "^9.1.0", "@types/chai": "^4.3.11", @@ -53,6 +54,7 @@ "solhint": "^4.0.0", "solhint-config-thesis": "github:thesis/solhint-config", "solidity-coverage": "^0.8.5", + "solidity-docgen": "0.6.0-beta.36", "ts-node": "^10.9.1", "typechain": "^8.3.2", "typescript": "^5.3.2" diff --git a/core/test/Acre.test.ts b/core/test/Acre.test.ts index 9d7d0c9c6..b4429a856 100644 --- a/core/test/Acre.test.ts +++ b/core/test/Acre.test.ts @@ -3,58 +3,63 @@ import { loadFixture, } from "@nomicfoundation/hardhat-toolbox/network-helpers" import { expect } from "chai" -import { ContractTransactionResponse, ZeroAddress } from "ethers" +import { ContractTransactionResponse, MaxUint256, ZeroAddress } from "ethers" +import { ethers } from "hardhat" import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import type { SnapshotRestorer } from "@nomicfoundation/hardhat-toolbox/network-helpers" -import { deployment } from "./helpers/context" -import { getUnnamedSigner } from "./helpers/signer" +import { + beforeAfterEachSnapshotWrapper, + beforeAfterSnapshotWrapper, + deployment, + getNamedSigner, + getUnnamedSigner, +} from "./helpers" import { to1e18 } from "./utils" -import type { Acre, TestERC20 } from "../typechain" +import type { Acre, TestERC20, Dispatcher } from "../typechain" async function fixture() { - const { tbtc, acre } = await deployment() + const { tbtc, acre, dispatcher } = await deployment() + const { governance } = await getNamedSigner() - const [staker1, staker2] = await getUnnamedSigner() + const [staker1, staker2, thirdParty] = await getUnnamedSigner() const amountToMint = to1e18(100000) - tbtc.mint(staker1, amountToMint) - tbtc.mint(staker2, amountToMint) + await tbtc.mint(staker1, amountToMint) + await tbtc.mint(staker2, amountToMint) - return { acre, tbtc, staker1, staker2 } + return { acre, tbtc, staker1, staker2, dispatcher, governance, thirdParty } } describe("Acre", () => { + const referral: number = 17283 + let acre: Acre let tbtc: TestERC20 + let dispatcher: Dispatcher + + let governance: HardhatEthersSigner let staker1: HardhatEthersSigner let staker2: HardhatEthersSigner + let thirdParty: HardhatEthersSigner before(async () => { - ;({ acre, tbtc, staker1, staker2 } = await loadFixture(fixture)) + ;({ acre, tbtc, staker1, staker2, dispatcher, governance, thirdParty } = + await loadFixture(fixture)) }) describe("stake", () => { - const referral: number = 17283 - let snapshot: SnapshotRestorer - context("when staking as first staker", () => { - beforeEach(async () => { - snapshot = await takeSnapshot() - }) - - afterEach(async () => { - await snapshot.restore() - }) + beforeAfterEachSnapshotWrapper() context("with a referral", () => { - const amountToStake = to1e18(1000) + const amountToStake = to1e18(1) - // In this test case there is only one staker and - // the token vault has not earned anythig yet so received shares are - // equal to staked tokens amount. + // In this test case, there is only one staker and the token vault has + // not earned anything yet so received shares are equal to staked tokens + // amount. const expectedReceivedShares = amountToStake let tx: ContractTransactionResponse @@ -74,8 +79,8 @@ describe("Acre", () => { .stake(amountToStake, receiver.address, referral) }) - it("should emit Deposit event", () => { - expect(tx).to.emit(acre, "Deposit").withArgs( + it("should emit Deposit event", async () => { + await expect(tx).to.emit(acre, "Deposit").withArgs( // Caller. tbtcHolder.address, // Receiver. @@ -87,8 +92,8 @@ describe("Acre", () => { ) }) - it("should emit StakeReferral event", () => { - expect(tx) + it("should emit StakeReferral event", async () => { + await expect(tx) .to.emit(acre, "StakeReferral") .withArgs(referral, amountToStake) }) @@ -112,7 +117,7 @@ describe("Acre", () => { context("without referral", () => { const amountToStake = to1e18(10) - const emptyReferral = ethers.encodeBytes32String("") + const emptyReferral = 0 let tx: ContractTransactionResponse beforeEach(async () => { @@ -147,26 +152,60 @@ describe("Acre", () => { acre .connect(staker1) .stake(amountToStake, staker1.address, referral), - ).to.be.revertedWithCustomError(tbtc, "ERC20InsufficientAllowance") + ) + .to.be.revertedWithCustomError(tbtc, "ERC20InsufficientAllowance") + .withArgs(await acre.getAddress(), approvedAmount, amountToStake) }) }, ) - context("when amount to stake is 1", () => { - const amountToStake = 1 + context("when amount to stake is less than minimum", () => { + let amountToStake: bigint + let minimumDepositAmount: bigint beforeEach(async () => { + minimumDepositAmount = await acre.minimumDepositAmount() + amountToStake = minimumDepositAmount - 1n + await tbtc .connect(staker1) .approve(await acre.getAddress(), amountToStake) }) - it("should not revert", async () => { + it("should revert", async () => { await expect( acre .connect(staker1) .stake(amountToStake, staker1.address, referral), - ).to.not.be.reverted + ) + .to.revertedWithCustomError(acre, "DepositAmountLessThanMin") + .withArgs(amountToStake, minimumDepositAmount) + }) + }) + + context("when amount to stake is equal to the minimum amount", () => { + let amountToStake: bigint + let tx: ContractTransactionResponse + + beforeEach(async () => { + const minimumDepositAmount = await acre.minimumDepositAmount() + amountToStake = minimumDepositAmount + + await tbtc + .connect(staker1) + .approve(await acre.getAddress(), amountToStake) + + tx = await acre + .connect(staker1) + .stake(amountToStake, staker1.address, referral) + }) + + it("should receive shares equal to the staked amount", async () => { + await expect(tx).to.changeTokenBalances( + acre, + [staker1.address], + [amountToStake], + ) }) }) @@ -182,12 +221,14 @@ describe("Acre", () => { it("should revert", async () => { await expect( acre.connect(staker1).stake(amountToStake, ZeroAddress, referral), - ).to.be.revertedWithCustomError(acre, "ERC20InvalidReceiver") + ) + .to.be.revertedWithCustomError(acre, "ERC20InvalidReceiver") + .withArgs(ZeroAddress) }) }) context( - "when a staker approved and staked tokens and wants to stake more but w/o another apporval", + "when a staker approved and staked tokens and wants to stake more but w/o another approval", () => { const amountToStake = to1e18(10) @@ -206,201 +247,788 @@ describe("Acre", () => { acre .connect(staker1) .stake(amountToStake, staker1.address, referral), - ).to.be.revertedWithCustomError(acre, "ERC20InsufficientAllowance") + ) + .to.be.revertedWithCustomError(acre, "ERC20InsufficientAllowance") + .withArgs(await acre.getAddress(), 0, amountToStake) }) }, ) }) - context("when there are two stakers, A and B ", () => { - const staker1AmountToStake = to1e18(75) - const staker2AmountToStake = to1e18(25) + describe("when staking by multiple stakers", () => { + beforeAfterSnapshotWrapper() + + const staker1AmountToStake = to1e18(7) + const staker2AmountToStake = to1e18(3) + const earnedYield = to1e18(5) + let afterStakesSnapshot: SnapshotRestorer let afterSimulatingYieldSnapshot: SnapshotRestorer before(async () => { + // Mint tBTC. + await tbtc.mint(staker1.address, staker1AmountToStake) + await tbtc.mint(staker2.address, staker2AmountToStake) + + // Approve tBTC. await tbtc .connect(staker1) .approve(await acre.getAddress(), staker1AmountToStake) await tbtc .connect(staker2) .approve(await acre.getAddress(), staker2AmountToStake) + }) + + context("when the vault is in initial state", () => { + describe("when two stakers stake", () => { + let stakeTx1: ContractTransactionResponse + let stakeTx2: ContractTransactionResponse + + before(async () => { + stakeTx1 = await acre + .connect(staker1) + .stake(staker1AmountToStake, staker1.address, referral) + + stakeTx2 = await acre + .connect(staker2) + .stake(staker2AmountToStake, staker2.address, referral) - // Mint tokens. - await tbtc.connect(staker1).mint(staker1.address, staker1AmountToStake) - await tbtc.connect(staker2).mint(staker2.address, staker2AmountToStake) + afterStakesSnapshot = await takeSnapshot() + }) + + it("staker A should receive shares equal to a staked amount", async () => { + await expect(stakeTx1).to.changeTokenBalances( + acre, + [staker1.address], + [staker1AmountToStake], + ) + }) + + it("staker B should receive shares equal to a staked amount", async () => { + await expect(stakeTx2).to.changeTokenBalances( + acre, + [staker2.address], + [staker2AmountToStake], + ) + }) + + it("the total assets amount should be equal to all staked tokens", async () => { + expect(await acre.totalAssets()).to.eq( + staker1AmountToStake + staker2AmountToStake, + ) + }) + }) }) - context( - "when the vault is empty and has not yet earned yield from strategies", - () => { + context("when vault has two stakers", () => { + context("when vault earns yield", () => { + let staker1SharesBefore: bigint + let staker2SharesBefore: bigint + + before(async () => { + // Current state: + // Staker A shares = 7 + // Staker B shares = 3 + // Total assets = 7(staker A) + 3(staker B) + 5(yield) + await afterStakesSnapshot.restore() + + staker1SharesBefore = await acre.balanceOf(staker1.address) + staker2SharesBefore = await acre.balanceOf(staker2.address) + + // Simulating yield returned from strategies. The vault now contains + // more tokens than deposited which causes the exchange rate to + // change. + await tbtc.mint(await acre.getAddress(), earnedYield) + }) + after(async () => { - afterStakesSnapshot = await takeSnapshot() + afterSimulatingYieldSnapshot = await takeSnapshot() }) - context("when staker A stakes tokens", () => { - it("should stake tokens correctly", async () => { - await expect( - acre - .connect(staker1) - .stake(staker1AmountToStake, staker1.address, referral), - ).to.be.not.reverted - }) + it("the vault should hold more assets", async () => { + expect(await acre.totalAssets()).to.be.eq( + staker1AmountToStake + staker2AmountToStake + earnedYield, + ) + }) - it("should receive shares equal to a staked amount", async () => { - const shares = await acre.balanceOf(staker1.address) + it("the stakers shares should be the same", async () => { + expect(await acre.balanceOf(staker1.address)).to.be.eq( + staker1SharesBefore, + ) + expect(await acre.balanceOf(staker2.address)).to.be.eq( + staker2SharesBefore, + ) + }) - expect(shares).to.eq(staker1AmountToStake) - }) + it("the staker A should be able to redeem more tokens than before", async () => { + const shares = await acre.balanceOf(staker1.address) + const availableAssetsToRedeem = await acre.previewRedeem(shares) + + // Expected amount w/o rounding: 7 * 15 / 10 = 10.5 + // Expected amount w/ support for rounding: 10499999999999999999 in + // tBTC token precision. + const expectedAssetsToRedeem = 10499999999999999999n + + expect(availableAssetsToRedeem).to.be.eq(expectedAssetsToRedeem) }) - context("when staker B stakes tokens", () => { - it("should stake tokens correctly", async () => { - await expect( - acre - .connect(staker2) - .stake(staker2AmountToStake, staker2.address, referral), - ).to.be.not.reverted - }) + it("the staker B should be able to redeem more tokens than before", async () => { + const shares = await acre.balanceOf(staker2.address) + const availableAssetsToRedeem = await acre.previewRedeem(shares) - it("should receive shares equal to a staked amount", async () => { - const shares = await acre.balanceOf(staker2.address) + // Expected amount w/o rounding: 3 * 15 / 10 = 4.5 + // Expected amount w/ support for rounding: 4499999999999999999 in + // tBTC token precision. + const expectedAssetsToRedeem = 4499999999999999999n - expect(shares).to.eq(staker2AmountToStake) - }) + expect(availableAssetsToRedeem).to.be.eq(expectedAssetsToRedeem) }) - }, - ) + }) - context("when the vault has stakes", () => { - before(async () => { - await afterStakesSnapshot.restore() + context("when staker A stakes more tokens", () => { + context( + "when total tBTC amount after staking would not exceed max amount", + () => { + const newAmountToStake = to1e18(2) + // Current state: + // Total assets = 7(staker A) + 3(staker B) + 5(yield) + // Total shares = 7 + 3 = 10 + // New stake amount = 2 + // Shares to mint = 2 * 10 / 15 = 1.(3) -> 1333333333333333333 in stBTC + // token precision + const expectedSharesToMint = 1333333333333333333n + let sharesBefore: bigint + let availableToRedeemBefore: bigint + + before(async () => { + await afterSimulatingYieldSnapshot.restore() + + sharesBefore = await acre.balanceOf(staker1.address) + availableToRedeemBefore = await acre.previewRedeem(sharesBefore) + + await tbtc.mint(staker1.address, newAmountToStake) + + await tbtc + .connect(staker1) + .approve(await acre.getAddress(), newAmountToStake) + + // State after stake: + // Total assets = 7(staker A) + 3(staker B) + 5(yield) + 2(staker + // A) = 17 + // Total shares = 7 + 3 + 1.(3) = 11.(3) + await acre + .connect(staker1) + .stake(newAmountToStake, staker1.address, referral) + }) + + it("should receive more shares", async () => { + const shares = await acre.balanceOf(staker1.address) + + expect(shares).to.be.eq(sharesBefore + expectedSharesToMint) + }) + + it("should be able to redeem more tokens than before", async () => { + const shares = await acre.balanceOf(staker1.address) + const availableToRedeem = await acre.previewRedeem(shares) + + // Expected amount w/o rounding: 8.(3) * 17 / 11.(3) = 12.5 + // Expected amount w/ support for rounding: 12499999999999999999 in + // tBTC token precision. + const expectedTotalAssetsAvailableToRedeem = + 12499999999999999999n + + expect(availableToRedeem).to.be.greaterThan( + availableToRedeemBefore, + ) + expect(availableToRedeem).to.be.eq( + expectedTotalAssetsAvailableToRedeem, + ) + }) + }, + ) + + context( + "when total tBTC amount after staking would exceed max amount", + () => { + let possibleMaxAmountToStake: bigint + let amountToStake: bigint + + before(async () => { + await afterSimulatingYieldSnapshot.restore() + + // In the current implementation of the `maxDeposit` the + // `address` param is not taken into account - it means it will + // return the same value for any address. + possibleMaxAmountToStake = await acre.maxDeposit( + staker1.address, + ) + amountToStake = possibleMaxAmountToStake + 1n + + await tbtc + .connect(staker1) + .approve(await acre.getAddress(), amountToStake) + }) + + it("should revert", async () => { + await expect( + acre.stake(amountToStake, staker1.address, referral), + ) + .to.be.revertedWithCustomError( + acre, + "ERC4626ExceededMaxDeposit", + ) + .withArgs( + staker1.address, + amountToStake, + possibleMaxAmountToStake, + ) + }) + }, + ) + + context( + "when total tBTC amount after staking would be equal to the max amount", + () => { + let amountToStake: bigint + let tx: ContractTransactionResponse + + before(async () => { + amountToStake = await acre.maxDeposit(staker1.address) + + await tbtc + .connect(staker1) + .approve(await acre.getAddress(), amountToStake) + + tx = await acre.stake(amountToStake, staker1, referral) + }) + + it("should stake tokens correctly", async () => { + await expect(tx).to.emit(acre, "Deposit") + }) + + it("the max deposit amount should be equal 0", async () => { + expect(await acre.maxDeposit(staker1)).to.eq(0) + }) + + it("should not be able to stake more tokens", async () => { + await expect(acre.stake(amountToStake, staker1, referral)) + .to.be.revertedWithCustomError( + acre, + "ERC4626ExceededMaxDeposit", + ) + .withArgs(staker1.address, amountToStake, 0) + }) + }, + ) + }) + }) + }) + }) + + describe("mint", () => { + beforeAfterEachSnapshotWrapper() + + context("when minting as first staker", () => { + const amountToStake = to1e18(1) + let tx: ContractTransactionResponse + let sharesToMint: bigint + + beforeEach(async () => { + sharesToMint = amountToStake + + await tbtc + .connect(staker1) + .approve(await acre.getAddress(), amountToStake) + + tx = await acre.connect(staker1).mint(sharesToMint, staker1.address) + }) + + it("should emit Deposit event", async () => { + await expect(tx).to.emit(acre, "Deposit").withArgs( + // Caller. + staker1.address, + // Receiver. + staker1.address, + // Staked tokens. + amountToStake, + // Received shares. + sharesToMint, + ) + }) + + it("should mint stBTC tokens", async () => { + await expect(tx).to.changeTokenBalances( + acre, + [staker1.address], + [sharesToMint], + ) + }) + + it("should transfer tBTC tokens", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [staker1.address, acre], + [-amountToStake, amountToStake], + ) + }) + }) + + context("when staker wants to mint more shares than max mint limit", () => { + let sharesToMint: bigint + let maxMint: bigint + + beforeEach(async () => { + maxMint = await acre.maxMint(staker1.address) + + sharesToMint = maxMint + 1n + }) + + it("should take into account the max total assets parameter and revert", async () => { + await expect(acre.connect(staker1).mint(sharesToMint, staker1.address)) + .to.be.revertedWithCustomError(acre, "ERC4626ExceededMaxMint") + .withArgs(staker1.address, sharesToMint, maxMint) + }) + }) + + context( + "when staker wants to mint less shares than the min deposit amount", + () => { + let sharesToMint: bigint + let minimumDepositAmount: bigint + + beforeEach(async () => { + minimumDepositAmount = await acre.minimumDepositAmount() + const shares = await acre.convertToShares(minimumDepositAmount) + + sharesToMint = shares - 1n + await tbtc + .connect(staker1) + .approve(await acre.getAddress(), minimumDepositAmount) + }) + + it("should take into account the min deposit amount parameter and revert", async () => { + // In this test case, there is only one staker and the token vault has + // not earned anything yet so received shares are equal to staked + // tokens amount. + const depositAmount = sharesToMint + + await expect( + acre.connect(staker1).mint(sharesToMint, staker1.address), + ) + .to.be.revertedWithCustomError(acre, "DepositAmountLessThanMin") + .withArgs(depositAmount, minimumDepositAmount) + }) + }, + ) + }) + + describe("updateDepositParameters", () => { + beforeAfterEachSnapshotWrapper() + + const validMinimumDepositAmount = to1e18(1) + const validMaximumTotalAssetsAmount = to1e18(30) + + context("when is called by governance", () => { + context("when all parameters are valid", () => { + let tx: ContractTransactionResponse + + beforeEach(async () => { + tx = await acre + .connect(governance) + .updateDepositParameters( + validMinimumDepositAmount, + validMaximumTotalAssetsAmount, + ) + }) + + it("should emit DepositParametersUpdated event", async () => { + await expect(tx) + .to.emit(acre, "DepositParametersUpdated") + .withArgs(validMinimumDepositAmount, validMaximumTotalAssetsAmount) }) - it("the total assets amount should be equal to all staked tokens", async () => { - const totalAssets = await acre.totalAssets() + it("should update parameters correctly", async () => { + const [minimumDepositAmount, maximumTotalAssets] = + await acre.depositParameters() - expect(totalAssets).to.eq(staker1AmountToStake + staker2AmountToStake) + expect(minimumDepositAmount).to.be.eq(validMinimumDepositAmount) + expect(maximumTotalAssets).to.be.eq(validMaximumTotalAssetsAmount) }) }) - context("when vault earns yield", () => { - let staker1SharesBefore: bigint - let staker2SharesBefore: bigint - let vaultYield: bigint + context("when minimum deposit amount is 0", () => { + const newMinimumDepositAmount = 0 - before(async () => { - // Current state: - // Staker A shares = 75 - // Staker B shares = 25 - // Total assets = 75(staker A) + 25(staker B) + 50(yield) - await afterStakesSnapshot.restore() + beforeEach(async () => { + await acre + .connect(governance) + .updateDepositParameters( + newMinimumDepositAmount, + validMaximumTotalAssetsAmount, + ) + }) - staker1SharesBefore = await acre.balanceOf(staker1.address) - staker2SharesBefore = await acre.balanceOf(staker2.address) - vaultYield = to1e18(50) + it("should update the minimum deposit amount correctly", async () => { + const minimumDepositAmount = await acre.minimumDepositAmount() - // Simulating yield returned from strategies. The vault now contains - // more tokens than deposited which causes the exchange rate to - // change. - await tbtc.mint(await acre.getAddress(), vaultYield) + expect(minimumDepositAmount).to.be.eq(newMinimumDepositAmount) }) + }) + + context("when the maximum total assets amount is 0", () => { + const newMaximumTotalAssets = 0 - after(async () => { - afterSimulatingYieldSnapshot = await takeSnapshot() + beforeEach(async () => { + await acre + .connect(governance) + .updateDepositParameters( + validMinimumDepositAmount, + newMaximumTotalAssets, + ) }) - it("the vault should hold more assets", async () => { - expect(await acre.totalAssets()).to.be.eq( - staker1AmountToStake + staker2AmountToStake + vaultYield, - ) + it("should update parameter correctly", async () => { + expect(await acre.maximumTotalAssets()).to.be.eq(0) + }) + }) + }) + + context("when it is called by non-governance", () => { + it("should revert", async () => { + await expect( + acre + .connect(staker1) + .updateDepositParameters( + validMinimumDepositAmount, + validMaximumTotalAssetsAmount, + ), + ) + .to.be.revertedWithCustomError(acre, "OwnableUnauthorizedAccount") + .withArgs(staker1.address) + }) + }) + }) + + describe("maxDeposit", () => { + beforeAfterEachSnapshotWrapper() + + let maximumTotalAssets: bigint + let minimumDepositAmount: bigint + + beforeEach(async () => { + ;[minimumDepositAmount, maximumTotalAssets] = + await acre.depositParameters() + }) + + context( + "when total assets is greater than maximum total assets amount", + () => { + beforeEach(async () => { + const toMint = maximumTotalAssets + 1n + + await tbtc.mint(await acre.getAddress(), toMint) }) - it("the staker's shares should be the same", async () => { - expect(await acre.balanceOf(staker1.address)).to.be.eq( - staker1SharesBefore, - ) - expect(await acre.balanceOf(staker2.address)).to.be.eq( - staker2SharesBefore, - ) + it("should return 0", async () => { + expect(await acre.maxDeposit(staker1.address)).to.be.eq(0) + }) + }, + ) + + context("when the vault is empty", () => { + it("should return maximum total assets amount", async () => { + expect(await acre.maxDeposit(staker1.address)).to.be.eq( + maximumTotalAssets, + ) + }) + }) + + context("when the maximum total amount has not yet been reached", () => { + let expectedValue: bigint + + beforeEach(async () => { + const toMint = to1e18(2) + expectedValue = maximumTotalAssets - toMint + + await tbtc.mint(await acre.getAddress(), toMint) + }) + + it("should return correct value", async () => { + expect(await acre.maxDeposit(staker1.address)).to.be.eq(expectedValue) + }) + }) + + context("when the deposit limit is disabled", () => { + const maximum = MaxUint256 + + beforeEach(async () => { + await acre + .connect(governance) + .updateDepositParameters(minimumDepositAmount, maximum) + }) + + context("when the vault is empty", () => { + it("should return the maximum value", async () => { + expect(await acre.maxDeposit(staker1.address)).to.be.eq(maximum) }) + }) - it("the staker A should be able to redeem more tokens than before", async () => { - const shares = await acre.balanceOf(staker1.address) - const availableAssetsToRedeem = await acre.previewRedeem(shares) + context("when the vault is not empty", () => { + const amountToStake = to1e18(1) + + beforeEach(async () => { + await tbtc + .connect(staker1) + .approve(await acre.getAddress(), amountToStake) - // Expected amount w/o rounding: 75 * 150 / 100 = 112.5 - // Expected amount w/ support for rounding: 112499999999999999999 in - // tBTC token precision. - const expectedAssetsToRedeem = 112499999999999999999n + await acre + .connect(staker1) + .stake(amountToStake, staker1.address, referral) + }) - expect(availableAssetsToRedeem).to.be.eq(expectedAssetsToRedeem) + it("should return the maximum value", async () => { + expect(await acre.maxDeposit(staker1.address)).to.be.eq(maximum) }) + }) + }) + }) - it("the staker B should be able to redeem more tokens than before", async () => { - const shares = await acre.balanceOf(staker2.address) - const availableAssetsToRedeem = await acre.previewRedeem(shares) + describe("updateDispatcher", () => { + let snapshot: SnapshotRestorer - // Expected amount w/o rounding: 25 * 150 / 100 = 37.5 - // Expected amount w/ support for rounding: 37499999999999999999 in - // tBTC token precision. - const expectedAssetsToRedeem = 37499999999999999999n + before(async () => { + snapshot = await takeSnapshot() + }) + + after(async () => { + await snapshot.restore() + }) + + context("when caller is not governance", () => { + it("should revert", async () => { + await expect(acre.connect(thirdParty).updateDispatcher(ZeroAddress)) + .to.be.revertedWithCustomError(acre, "OwnableUnauthorizedAccount") + .withArgs(thirdParty.address) + }) + }) - expect(availableAssetsToRedeem).to.be.eq(expectedAssetsToRedeem) + context("when caller is governance", () => { + context("when a new dispatcher is zero address", () => { + it("should revert", async () => { + await expect( + acre.connect(governance).updateDispatcher(ZeroAddress), + ).to.be.revertedWithCustomError(acre, "ZeroAddress") }) }) - context("when staker A stakes more tokens", () => { - const newAmountToStake = to1e18(20) - // Current state: - // Total assets = 75(staker A) + 25(staker B) + 50(yield) - // Total shares = 75 + 25 = 100 - // 20 * 100 / 150 = 13.(3) -> 13333333333333333333 in stBTC token - /// precision - const expectedSharesToMint = 13333333333333333333n - let sharesBefore: bigint - let availableToRedeemBefore: bigint + context("when a new dispatcher is non-zero address", () => { + let newDispatcher: string + let acreAddress: string + let dispatcherAddress: string + let tx: ContractTransactionResponse before(async () => { - await afterSimulatingYieldSnapshot.restore() + // Dispatcher is set by the deployment scripts. See deployment tests + // where initial parameters are checked. + dispatcherAddress = await dispatcher.getAddress() + newDispatcher = await ethers.Wallet.createRandom().getAddress() + acreAddress = await acre.getAddress() + + tx = await acre.connect(governance).updateDispatcher(newDispatcher) + }) + + it("should update the dispatcher", async () => { + expect(await acre.dispatcher()).to.be.equal(newDispatcher) + }) + + it("should reset approval amount for the old dispatcher", async () => { + const allowance = await tbtc.allowance(acreAddress, dispatcherAddress) + expect(allowance).to.be.equal(0) + }) + + it("should approve max amount for the new dispatcher", async () => { + const allowance = await tbtc.allowance(acreAddress, newDispatcher) + expect(allowance).to.be.equal(MaxUint256) + }) + + it("should emit DispatcherUpdated event", async () => { + await expect(tx) + .to.emit(acre, "DispatcherUpdated") + .withArgs(dispatcherAddress, newDispatcher) + }) + }) + }) + }) + + describe("maxMint", () => { + beforeAfterEachSnapshotWrapper() + + let maximumTotalAssets: bigint + let minimumDepositAmount: bigint + + beforeEach(async () => { + ;[minimumDepositAmount, maximumTotalAssets] = + await acre.depositParameters() + }) + + context( + "when total assets is greater than maximum total assets amount", + () => { + beforeEach(async () => { + const toMint = maximumTotalAssets + 1n + + await tbtc.mint(await acre.getAddress(), toMint) + }) + + it("should return 0", async () => { + expect(await acre.maxMint(staker1.address)).to.be.eq(0) + }) + }, + ) + + context("when the vault is empty", () => { + it("should return maximum total assets amount in shares", async () => { + // When the vault is empty the max shares amount is equal to the maximum + // total assets amount. + expect(await acre.maxMint(staker1.address)).to.be.eq(maximumTotalAssets) + }) + }) + + context("when the maximum total amount has not yet been reached", () => { + let expectedValue: bigint + + beforeEach(async () => { + const toMint = to1e18(2) + const amountToStake = to1e18(3) + + // Staker stakes 3 tBTC. + await tbtc + .connect(staker1) + .approve(await acre.getAddress(), amountToStake) + await acre.connect(staker1).deposit(amountToStake, staker1.address) + + // Vault earns 2 tBTC. + await tbtc.mint(await acre.getAddress(), toMint) + + // The current state is: + // Total assets: 5 + // Total supply: 3 + // Maximum total assets: 30 + // Current max deposit: 25 - 2 - 3 = 20 + // Max shares: 20 * 3 / 5 = 15 -> 12000000000000000001 in stBTC + // precision and rounding support. + expectedValue = 12000000000000000001n + }) + + it("should return correct value", async () => { + expect(await acre.maxMint(staker1.address)).to.be.eq(expectedValue) + }) + }) - sharesBefore = await acre.balanceOf(staker1.address) - availableToRedeemBefore = await acre.previewRedeem(sharesBefore) + context("when the deposit limit is disabled", () => { + const maximum = MaxUint256 - tbtc.mint(staker1.address, newAmountToStake) + beforeEach(async () => { + await acre + .connect(governance) + .updateDepositParameters(minimumDepositAmount, maximum) + }) + context("when the vault is empty", () => { + it("should return the maximum value", async () => { + expect(await acre.maxMint(staker1.address)).to.be.eq(maximum) + }) + }) + + context("when the vault is not empty", () => { + const amountToStake = to1e18(1) + + beforeEach(async () => { await tbtc .connect(staker1) - .approve(await acre.getAddress(), newAmountToStake) + .approve(await acre.getAddress(), amountToStake) + + await acre + .connect(staker1) + .stake(amountToStake, staker1.address, referral) + }) - // State after stake: - // Total assets = 75(staker A) + 25(staker B) + 50(yield) + 20(staker - // A) = 170 - // Total shares = 75 + 25 + 13.(3) = 113.(3) - await acre.stake(newAmountToStake, staker1.address, referral) + it("should return the maximum value", async () => { + expect(await acre.maxMint(staker1.address)).to.be.eq(maximum) }) + }) + }) + }) + + describe("deposit", () => { + beforeAfterEachSnapshotWrapper() - it("should receive more shares", async () => { - const shares = await acre.balanceOf(staker1.address) + const receiver = ethers.Wallet.createRandom() - expect(shares).to.be.eq(sharesBefore + expectedSharesToMint) + let amountToDeposit: bigint + let minimumDepositAmount: bigint + + beforeEach(async () => { + minimumDepositAmount = await acre.minimumDepositAmount() + }) + + context("when the deposit amount is less than minimum", () => { + beforeEach(() => { + amountToDeposit = minimumDepositAmount - 1n + }) + + it("should revert", async () => { + await expect(acre.deposit(amountToDeposit, receiver.address)) + .to.be.revertedWithCustomError(acre, "DepositAmountLessThanMin") + .withArgs(amountToDeposit, minimumDepositAmount) + }) + }) + + context( + "when the deposit amount is equal to the minimum deposit amount", + () => { + let tx: ContractTransactionResponse + let expectedReceivedShares: bigint + + beforeEach(async () => { + amountToDeposit = minimumDepositAmount + expectedReceivedShares = amountToDeposit + + await tbtc.approve(await acre.getAddress(), amountToDeposit) + tx = await acre + .connect(staker1) + .deposit(amountToDeposit, receiver.address) }) - it("should be able to redeem more tokens than before", async () => { - const shares = await acre.balanceOf(staker1.address) - const availableToRedeem = await acre.previewRedeem(shares) + it("should emit Deposit event", async () => { + await expect(tx).to.emit(acre, "Deposit").withArgs( + // Caller. + staker1.address, + // Receiver. + receiver.address, + // Staked tokens. + amountToDeposit, + // Received shares. + expectedReceivedShares, + ) + }) - // Expected amount w/o rounding: 88.(3) * 170 / 113.(3) = 132.5 - // Expected amount w/ support for rounding: 132499999999999999999 in - // tBTC token precision. - const expectedTotalAssetsAvailableToRedeem = 132499999999999999999n + it("should mint stBTC tokens", async () => { + await expect(tx).to.changeTokenBalances( + acre, + [receiver.address], + [expectedReceivedShares], + ) + }) - expect(availableToRedeem).to.be.greaterThan(availableToRedeemBefore) - expect(availableToRedeem).to.be.eq( - expectedTotalAssetsAvailableToRedeem, + it("should transfer tBTC tokens", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [staker1.address, acre], + [-amountToDeposit, amountToDeposit], ) }) - }) - }) + }, + ) }) }) diff --git a/core/test/Deployment.test.ts b/core/test/Deployment.test.ts new file mode 100644 index 000000000..362e2f43c --- /dev/null +++ b/core/test/Deployment.test.ts @@ -0,0 +1,61 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" +import { expect } from "chai" +import { MaxUint256 } from "ethers" + +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" +import { deployment } from "./helpers/context" +import { getNamedSigner } from "./helpers/signer" + +import type { Acre, Dispatcher, TestERC20 } from "../typechain" + +async function fixture() { + const { tbtc, acre, dispatcher } = await deployment() + const { governance, maintainer } = await getNamedSigner() + + return { acre, dispatcher, tbtc, governance, maintainer } +} + +describe("Deployment", () => { + let acre: Acre + let dispatcher: Dispatcher + let tbtc: TestERC20 + let maintainer: HardhatEthersSigner + + before(async () => { + ;({ acre, dispatcher, tbtc, maintainer } = await loadFixture(fixture)) + }) + + describe("Acre", () => { + describe("updateDispatcher", () => { + context("when a dispatcher has been set", () => { + it("should be set to a dispatcher address by the deployment script", async () => { + const actualDispatcher = await acre.dispatcher() + + expect(actualDispatcher).to.be.equal(await dispatcher.getAddress()) + }) + + it("should approve max amount for the dispatcher", async () => { + const actualDispatcher = await acre.dispatcher() + const allowance = await tbtc.allowance( + await acre.getAddress(), + actualDispatcher, + ) + + expect(allowance).to.be.equal(MaxUint256) + }) + }) + }) + }) + + describe("Dispatcher", () => { + describe("updateMaintainer", () => { + context("when a new maintainer has been set", () => { + it("should be set to a new maintainer address", async () => { + const actualMaintainer = await dispatcher.maintainer() + + expect(actualMaintainer).to.be.equal(await maintainer.getAddress()) + }) + }) + }) + }) +}) diff --git a/core/test/Dispatcher.test.ts b/core/test/Dispatcher.test.ts index 1fdecc5d9..eabcc46ac 100644 --- a/core/test/Dispatcher.test.ts +++ b/core/test/Dispatcher.test.ts @@ -1,36 +1,46 @@ import { ethers } from "hardhat" import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { expect } from "chai" +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" + +import { ContractTransactionResponse, ZeroAddress } from "ethers" import { - SnapshotRestorer, - takeSnapshot, - loadFixture, -} from "@nomicfoundation/hardhat-toolbox/network-helpers" -import type { Dispatcher } from "../typechain" -import { deployment } from "./helpers/context" -import { getNamedSigner, getUnnamedSigner } from "./helpers/signer" + beforeAfterEachSnapshotWrapper, + beforeAfterSnapshotWrapper, + deployment, + getNamedSigner, + getUnnamedSigner, +} from "./helpers" + +import type { Dispatcher, TestERC4626, Acre, TestERC20 } from "../typechain" + +import { to1e18 } from "./utils" async function fixture() { - const { dispatcher } = await deployment() - const { governance } = await getNamedSigner() + const { tbtc, acre, dispatcher, vault } = await deployment() + const { governance, maintainer } = await getNamedSigner() const [thirdParty] = await getUnnamedSigner() - return { dispatcher, governance, thirdParty } + return { dispatcher, governance, thirdParty, maintainer, vault, tbtc, acre } } describe("Dispatcher", () => { - let snapshot: SnapshotRestorer - let dispatcher: Dispatcher + let vault: TestERC4626 + let tbtc: TestERC20 + let acre: Acre + let governance: HardhatEthersSigner let thirdParty: HardhatEthersSigner + let maintainer: HardhatEthersSigner let vaultAddress1: string let vaultAddress2: string let vaultAddress3: string let vaultAddress4: string before(async () => { - ;({ dispatcher, governance, thirdParty } = await loadFixture(fixture)) + ;({ dispatcher, governance, thirdParty, maintainer, vault, tbtc, acre } = + await loadFixture(fixture)) vaultAddress1 = await ethers.Wallet.createRandom().getAddress() vaultAddress2 = await ethers.Wallet.createRandom().getAddress() @@ -38,32 +48,36 @@ describe("Dispatcher", () => { vaultAddress4 = await ethers.Wallet.createRandom().getAddress() }) - beforeEach(async () => { - snapshot = await takeSnapshot() - }) - - afterEach(async () => { - await snapshot.restore() - }) - describe("authorizeVault", () => { + beforeAfterSnapshotWrapper() + context("when caller is not a governance account", () => { + beforeAfterSnapshotWrapper() + it("should revert when adding a vault", async () => { await expect( dispatcher.connect(thirdParty).authorizeVault(vaultAddress1), - ).to.be.revertedWithCustomError( - dispatcher, - "OwnableUnauthorizedAccount", ) + .to.be.revertedWithCustomError( + dispatcher, + "OwnableUnauthorizedAccount", + ) + .withArgs(thirdParty.address) }) }) context("when caller is a governance account", () => { - it("should be able to authorize vaults", async () => { - await dispatcher.connect(governance).authorizeVault(vaultAddress1) + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await dispatcher.connect(governance).authorizeVault(vaultAddress1) await dispatcher.connect(governance).authorizeVault(vaultAddress2) await dispatcher.connect(governance).authorizeVault(vaultAddress3) + }) + it("should authorize vaults", async () => { expect(await dispatcher.vaults(0)).to.equal(vaultAddress1) expect(await dispatcher.vaultsInfo(vaultAddress1)).to.be.equal(true) @@ -74,17 +88,14 @@ describe("Dispatcher", () => { expect(await dispatcher.vaultsInfo(vaultAddress3)).to.be.equal(true) }) - it("should not be able to authorize the same vault twice", async () => { - await dispatcher.connect(governance).authorizeVault(vaultAddress1) + it("should not authorize the same vault twice", async () => { await expect( dispatcher.connect(governance).authorizeVault(vaultAddress1), ).to.be.revertedWithCustomError(dispatcher, "VaultAlreadyAuthorized") }) it("should emit an event when adding a vault", async () => { - await expect( - dispatcher.connect(governance).authorizeVault(vaultAddress1), - ) + await expect(tx) .to.emit(dispatcher, "VaultAuthorized") .withArgs(vaultAddress1) }) @@ -92,7 +103,9 @@ describe("Dispatcher", () => { }) describe("deauthorizeVault", () => { - beforeEach(async () => { + beforeAfterSnapshotWrapper() + + before(async () => { await dispatcher.connect(governance).authorizeVault(vaultAddress1) await dispatcher.connect(governance).authorizeVault(vaultAddress2) await dispatcher.connect(governance).authorizeVault(vaultAddress3) @@ -102,15 +115,19 @@ describe("Dispatcher", () => { it("should revert when adding a vault", async () => { await expect( dispatcher.connect(thirdParty).deauthorizeVault(vaultAddress1), - ).to.be.revertedWithCustomError( - dispatcher, - "OwnableUnauthorizedAccount", ) + .to.be.revertedWithCustomError( + dispatcher, + "OwnableUnauthorizedAccount", + ) + .withArgs(thirdParty.address) }) }) context("when caller is a governance account", () => { - it("should be able to authorize vaults", async () => { + beforeAfterEachSnapshotWrapper() + + it("should deauthorize vaults", async () => { await dispatcher.connect(governance).deauthorizeVault(vaultAddress1) // Last vault replaced the first vault in the 'vaults' array @@ -130,7 +147,7 @@ describe("Dispatcher", () => { expect(await dispatcher.vaultsInfo(vaultAddress3)).to.be.equal(false) }) - it("should be able to deauthorize a vault and authorize it again", async () => { + it("should deauthorize a vault and authorize it again", async () => { await dispatcher.connect(governance).deauthorizeVault(vaultAddress1) expect(await dispatcher.vaultsInfo(vaultAddress1)).to.be.equal(false) @@ -138,7 +155,7 @@ describe("Dispatcher", () => { expect(await dispatcher.vaultsInfo(vaultAddress1)).to.be.equal(true) }) - it("should not be able to deauthorize a vault that is not authorized", async () => { + it("should not deauthorize a vault that is not authorized", async () => { await expect( dispatcher.connect(governance).deauthorizeVault(vaultAddress4), ).to.be.revertedWithCustomError(dispatcher, "VaultUnauthorized") @@ -153,4 +170,168 @@ describe("Dispatcher", () => { }) }) }) + + describe("depositToVault", () => { + beforeAfterSnapshotWrapper() + + const assetsToAllocate = to1e18(100) + const minSharesOut = to1e18(100) + + before(async () => { + await dispatcher.connect(governance).authorizeVault(vault.getAddress()) + await tbtc.mint(await acre.getAddress(), to1e18(100000)) + }) + + context("when caller is not maintainer", () => { + beforeAfterSnapshotWrapper() + + it("should revert when depositing to a vault", async () => { + await expect( + dispatcher + .connect(thirdParty) + .depositToVault( + await vault.getAddress(), + assetsToAllocate, + minSharesOut, + ), + ).to.be.revertedWithCustomError(dispatcher, "NotMaintainer") + }) + }) + + context("when caller is maintainer", () => { + context("when vault is not authorized", () => { + beforeAfterSnapshotWrapper() + + it("should revert", async () => { + const randomAddress = await ethers.Wallet.createRandom().getAddress() + await expect( + dispatcher + .connect(maintainer) + .depositToVault(randomAddress, assetsToAllocate, minSharesOut), + ).to.be.revertedWithCustomError(dispatcher, "VaultUnauthorized") + }) + }) + + context("when the vault is authorized", () => { + let vaultAddress: string + + before(async () => { + vaultAddress = await vault.getAddress() + }) + + context("when allocation is successful", () => { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await dispatcher + .connect(maintainer) + .depositToVault(vaultAddress, assetsToAllocate, minSharesOut) + }) + + it("should deposit tBTC to a vault", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [acre, vault], + [-assetsToAllocate, assetsToAllocate], + ) + }) + + it("should mint vault's shares for Acre contract", async () => { + await expect(tx).to.changeTokenBalances( + vault, + [acre], + [minSharesOut], + ) + }) + + it("should emit a DepositAllocated event", async () => { + await expect(tx) + .to.emit(dispatcher, "DepositAllocated") + .withArgs(vaultAddress, assetsToAllocate, minSharesOut) + }) + }) + + context( + "when the expected returned shares are less than the actual returned shares", + () => { + beforeAfterSnapshotWrapper() + + const sharesOut = assetsToAllocate + const minShares = to1e18(101) + + it("should emit a MinSharesError event", async () => { + await expect( + dispatcher + .connect(maintainer) + .depositToVault(vaultAddress, assetsToAllocate, minShares), + ) + .to.be.revertedWithCustomError(dispatcher, "MinSharesError") + .withArgs(vaultAddress, sharesOut, minShares) + }) + }, + ) + }) + }) + }) + + describe("updateMaintainer", () => { + beforeAfterSnapshotWrapper() + + let newMaintainer: string + + before(async () => { + newMaintainer = await ethers.Wallet.createRandom().getAddress() + }) + + context("when caller is not an owner", () => { + beforeAfterSnapshotWrapper() + + it("should revert", async () => { + await expect( + dispatcher.connect(thirdParty).updateMaintainer(newMaintainer), + ) + .to.be.revertedWithCustomError( + dispatcher, + "OwnableUnauthorizedAccount", + ) + .withArgs(thirdParty.address) + }) + }) + + context("when caller is an owner", () => { + context("when maintainer is a zero address", () => { + beforeAfterSnapshotWrapper() + + it("should revert", async () => { + await expect( + dispatcher.connect(governance).updateMaintainer(ZeroAddress), + ).to.be.revertedWithCustomError(dispatcher, "ZeroAddress") + }) + }) + + context("when maintainer is not a zero address", () => { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await dispatcher + .connect(governance) + .updateMaintainer(newMaintainer) + }) + + it("should update the maintainer", async () => { + expect(await dispatcher.maintainer()).to.be.equal(newMaintainer) + }) + + it("should emit an event when updating the maintainer", async () => { + await expect(tx) + .to.emit(dispatcher, "MaintainerUpdated") + .withArgs(newMaintainer) + }) + }) + }) + }) }) diff --git a/core/test/TbtcDepositor.test.ts b/core/test/TbtcDepositor.test.ts new file mode 100644 index 000000000..349d4676a --- /dev/null +++ b/core/test/TbtcDepositor.test.ts @@ -0,0 +1,660 @@ +/* eslint-disable func-names */ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" + +import { expect } from "chai" +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" +import { ContractTransactionResponse, ZeroAddress } from "ethers" +import type { + Acre, + BridgeStub, + TBTCVaultStub, + TbtcDepositor, + TestERC20, +} from "../typechain" +import { deployment, getNamedSigner, getUnnamedSigner } from "./helpers" +import { beforeAfterSnapshotWrapper } from "./helpers/snapshot" +import { tbtcDepositData } from "./data/tbtc" +import { lastBlockTime } from "./helpers/time" +import { to1ePrecision } from "./utils" + +async function fixture() { + const { tbtcDepositor, tbtcBridge, tbtcVault, acre, tbtc } = + await deployment() + + return { tbtcDepositor, tbtcBridge, tbtcVault, acre, tbtc } +} + +describe("TbtcDepositor", () => { + 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 = 80 // 1/80 = 0.0125 = 1.25% + + let tbtcDepositor: TbtcDepositor + let tbtcBridge: BridgeStub + let tbtcVault: TBTCVaultStub + let acre: Acre + let tbtc: TestERC20 + + let governance: HardhatEthersSigner + let thirdParty: HardhatEthersSigner + + before(async () => { + ;({ tbtcDepositor, tbtcBridge, tbtcVault, acre, tbtc } = + await loadFixture(fixture)) + ;({ governance } = await getNamedSigner()) + ;[thirdParty] = await getUnnamedSigner() + + await acre.connect(governance).updateDepositParameters( + 10000000000000, // 0.00001 + await acre.maximumTotalAssets(), + ) + + await tbtcBridge + .connect(governance) + .setDepositTreasuryFeeDivisor(defaultDepositTreasuryFeeDivisor) + await tbtcBridge + .connect(governance) + .setDepositTxMaxFee(defaultDepositTxMaxFee) + await tbtcVault + .connect(governance) + .setOptimisticMintingFeeDivisor(defaultOptimisticFeeDivisor) + await tbtcDepositor + .connect(governance) + .updateDepositorFeeDivisor(defaultDepositorFeeDivisor) + }) + + describe("initializeStake", () => { + describe("when receiver is zero address", () => { + it("should revert", async () => { + await expect( + tbtcDepositor.initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + ZeroAddress, + 0, + ), + ).to.be.revertedWithCustomError(tbtcDepositor, "ReceiverIsZeroAddress") + }) + }) + + describe("when receiver is non zero address", () => { + describe("when stake request is not in progress", () => { + describe("when referral is non-zero", () => { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await tbtcDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.receiver, + tbtcDepositData.referral, + ) + }) + + it("should emit StakeInitialized event", async () => { + await expect(tx) + .to.emit(tbtcDepositor, "StakeInitialized") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + tbtcDepositData.receiver, + tbtcDepositData.referral, + ) + }) + + it("should store request data", async () => { + const storedStakeRequest = await tbtcDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect( + storedStakeRequest.requestedAt, + "invalid requestedAt", + ).to.be.equal(await lastBlockTime()) + expect( + storedStakeRequest.finalizedAt, + "invalid finalizedAt", + ).to.be.equal(0) + expect( + storedStakeRequest.tbtcDepositTxMaxFee, + "invalid tbtcDepositTxMaxFee", + ).to.be.equal(1000) + expect( + storedStakeRequest.tbtcOptimisticMintingFeeDivisor, + "invalid tbtcOptimisticMintingFeeDivisor", + ).to.be.equal(500) + }) + + it("should reveal the deposit to the bridge contract with extra data", async () => { + const storedRevealedDeposit = await tbtcBridge.deposits( + tbtcDepositData.depositKey, + ) + + expect( + storedRevealedDeposit.extraData, + "invalid extraData", + ).to.be.equal(tbtcDepositData.extraData) + }) + }) + + describe("when referral is zero", () => { + beforeAfterSnapshotWrapper() + + it("should succeed", async () => { + await expect( + tbtcDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.receiver, + 0, + ), + ).to.be.not.reverted + }) + }) + }) + + describe("when stake request is already in progress", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await tbtcDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.receiver, + tbtcDepositData.referral, + ) + }) + + it("should revert", async () => { + await expect( + tbtcDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.receiver, + tbtcDepositData.referral, + ), + ).to.be.revertedWithCustomError( + tbtcDepositor, + "StakeRequestAlreadyInProgress", + ) + }) + }) + }) + }) + + describe("finalizeStake", () => { + beforeAfterSnapshotWrapper() + + // Funding transaction amount: 10000 satoshi + // tBTC Deposit Treasury Fee: 0.05% = 10000 * 0.05% = 5 satoshi + // tBTC Deposit Transaction Max Fee: 1000 satoshi + // tBTC Optimistic Minting Fee: 0.2% = 10000 * 0.2% = 20 satoshi + // Depositor Fee: 1.25% = 10000 * 1.25% = 125 satoshi + + describe("when stake request has not been initialized", () => { + it("should revert", async () => { + await expect( + tbtcDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ).to.be.revertedWithCustomError( + tbtcDepositor, + "StakeRequestNotInitialized", + ) + }) + }) + + describe("when stake request has been initialized", () => { + function initializeStake() { + return tbtcDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.receiver, + tbtcDepositData.referral, + ) + } + + describe("when stake request has not been finalized", () => { + function testFinalizeStake( + expectedAssetsAmount: bigint, + expectedReceivedSharesAmount = expectedAssetsAmount, + ) { + let tx: ContractTransactionResponse + + before(async () => { + tx = await tbtcDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey) + }) + + it("should emit StakeFinalized event", async () => { + await expect(tx) + .to.emit(tbtcDepositor, "StakeFinalized") + .withArgs(tbtcDepositData.depositKey, thirdParty.address) + }) + + it("should emit StakeReferral event", async () => { + await expect(tx) + .to.emit(acre, "StakeReferral") + .withArgs(tbtcDepositData.referral, expectedAssetsAmount) + }) + + it("should emit Deposit event", async () => { + await expect(tx) + .to.emit(acre, "Deposit") + .withArgs( + await tbtcDepositor.getAddress(), + tbtcDepositData.receiver, + expectedAssetsAmount, + expectedReceivedSharesAmount, + ) + }) + + it("should stake in Acre contract", async () => { + await expect( + tx, + "invalid minted stBTC amount", + ).to.changeTokenBalances( + acre, + [tbtcDepositData.receiver], + [expectedReceivedSharesAmount], + ) + + await expect( + tx, + "invalid staked tBTC amount", + ).to.changeTokenBalances(tbtc, [acre], [expectedAssetsAmount]) + }) + } + + describe("when revealed depositor doesn't match tbtc depositor contract", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + + await tbtcBridge.setDepositor( + tbtcDepositData.depositKey, + thirdParty.address, + ) + }) + + it("should revert", async () => { + await expect( + tbtcDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ).to.be.revertedWithCustomError( + tbtcDepositor, + "UnexpectedDepositor", + ) + }) + }) + + describe("when revealed depositor matches tbtc depositor contract", () => { + beforeAfterSnapshotWrapper() + + describe("when minting request was finalized by optimistic minting", () => { + describe("when optimistic minting fee divisor is zero", () => { + beforeAfterSnapshotWrapper() + + const expectedAssetsAmount = to1ePrecision(8870, 10) // 8870 satoshi + + before(async () => { + await tbtcVault.setOptimisticMintingFeeDivisor(0) + + await initializeStake() + + // Simulate deposit request finalization via optimistic minting. + await tbtcVault.finalizeOptimisticMinting( + tbtcDepositData.depositKey, + ) + }) + + testFinalizeStake(expectedAssetsAmount) + }) + + describe("when optimistic minting fee divisor is not zero", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await tbtcVault.setOptimisticMintingFeeDivisor( + defaultOptimisticFeeDivisor, + ) + }) + + describe("when current optimistic minting fee is greater than it was on stake initialization", () => { + beforeAfterSnapshotWrapper() + + const expectedAssetsAmount = to1ePrecision(8830, 10) // 8830 satoshi + + before(async () => { + await initializeStake() + + await tbtcVault.setOptimisticMintingFeeDivisor( + defaultOptimisticFeeDivisor / 2, + ) // 1/250 = 0.004 = 0.4% + + // Simulate deposit request finalization via optimistic minting. + await tbtcVault.finalizeOptimisticMinting( + tbtcDepositData.depositKey, + ) + }) + + testFinalizeStake(expectedAssetsAmount) + }) + + describe("when current optimistic minting fee is lower than it was on stake initialization", () => { + beforeAfterSnapshotWrapper() + + // Since the current Optimistic Fee (10 satoshi) is lower than + // the one calculated on request initialization (20 satoshi) the + // higher value is deducted from the funding transaction amount. + const expectedAssetsAmount = to1ePrecision(8850, 10) // 8850 satoshi + + before(async () => { + await initializeStake() + + await tbtcVault.setOptimisticMintingFeeDivisor( + defaultOptimisticFeeDivisor * 2, + ) // 1/1000 = 0.001 = 0.1% + + // Simulate deposit request finalization via optimistic minting. + await tbtcVault.finalizeOptimisticMinting( + tbtcDepositData.depositKey, + ) + }) + + testFinalizeStake(expectedAssetsAmount) + }) + }) + }) + + describe("when minting request was not finalized by optimistic minting", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + describe("when minting request has not been swept", () => { + beforeAfterSnapshotWrapper() + + it("should revert", async () => { + await expect( + tbtcDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ).to.be.revertedWithCustomError( + tbtcDepositor, + "TbtcDepositNotCompleted", + ) + }) + }) + + describe("when minting request was swept", () => { + describe("when depositor fee divisor is zero", () => { + beforeAfterSnapshotWrapper() + + const expectedAssetsAmount = to1ePrecision(8995, 10) // 8995 satoshi + + before(async () => { + await tbtcDepositor + .connect(governance) + .updateDepositorFeeDivisor(0) + + // Simulate deposit request finalization via sweeping. + await tbtcBridge.sweep( + tbtcDepositData.fundingTxHash, + tbtcDepositData.reveal.fundingOutputIndex, + ) + }) + + testFinalizeStake(expectedAssetsAmount) + }) + + describe("when depositor fee divisor is not zero", () => { + beforeAfterSnapshotWrapper() + + const expectedAssetsAmount = to1ePrecision(8870, 10) // 8870 satoshi + + before(async () => { + await tbtcDepositor + .connect(governance) + .updateDepositorFeeDivisor(defaultDepositorFeeDivisor) + + // Simulate deposit request finalization via sweeping. + await tbtcBridge.sweep( + tbtcDepositData.fundingTxHash, + tbtcDepositData.reveal.fundingOutputIndex, + ) + }) + + testFinalizeStake(expectedAssetsAmount) + }) + }) + }) + }) + }) + + describe("when stake request has been finalized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + + // Simulate deposit request finalization via sweeping. + await tbtcBridge.sweep( + tbtcDepositData.fundingTxHash, + tbtcDepositData.reveal.fundingOutputIndex, + ) + + // Finalize stake request. + await tbtcDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + tbtcDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ).to.be.revertedWithCustomError( + tbtcDepositor, + "StakeRequestAlreadyFinalized", + ) + }) + }) + }) + }) + + describe("updateDepositorFeeDivisor", () => { + beforeAfterSnapshotWrapper() + + describe("when caller is not governance", () => { + it("should revert", async () => { + await expect( + tbtcDepositor.connect(thirdParty).updateDepositorFeeDivisor(1234), + ) + .to.be.revertedWithCustomError( + tbtcDepositor, + "OwnableUnauthorizedAccount", + ) + .withArgs(thirdParty.address) + }) + }) + + describe("when caller is governance", () => { + const testUpdateDepositorFeeDivisor = (newValue: number) => + function () { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await tbtcDepositor + .connect(governance) + .updateDepositorFeeDivisor(newValue) + }) + + it("should emit DepositorFeeDivisorUpdated event", async () => { + await expect(tx) + .to.emit(tbtcDepositor, "DepositorFeeDivisorUpdated") + .withArgs(newValue) + }) + + it("should update value correctly", async () => { + expect(await tbtcDepositor.depositorFeeDivisor()).to.be.eq(newValue) + }) + } + + describe( + "when new value is non-zero", + testUpdateDepositorFeeDivisor(47281), + ) + + describe("when new value is zero", testUpdateDepositorFeeDivisor(0)) + }) + }) + + describe("calculateDepositKey", () => { + it("should calculate the deposit key", async () => { + // Test data from transaction: https://etherscan.io/tx/0x7816e66d2b1a7858c2e8c49099bf009e52d07e081d5b562ac9ff6d2b072387c9 + expect( + await tbtcDepositor.calculateDepositKey( + "0xa08d41ee8e044b25d365fd54d27d79da6db9e9e2f8be166b82a510d0d31b9406", + 114, + ), + ).to.be.equal( + "0x4e89fe01b92ff0ebf0bdeb70891fcb6c286d750b191971999091c8a1e5b3f11d", + ) + }) + }) + + const extraDataValidTestData = new Map< + string, + { + receiver: string + referral: number + extraData: string + } + >([ + [ + "receiver has leading zeros", + { + receiver: "0x000055d85E80A49B5930C4a77975d44f012D86C1", + referral: 6851, // hex: 0x1ac3 + extraData: + "0x000055d85e80a49b5930c4a77975d44f012d86c11ac300000000000000000000", + }, + ], + [ + "receiver has trailing zeros", + { + receiver: "0x2d2F8BC7923F7F806Dc9bb2e17F950b42CfE0000", + referral: 6851, // hex: 0x1ac3 + extraData: + "0x2d2f8bc7923f7f806dc9bb2e17f950b42cfe00001ac300000000000000000000", + }, + ], + [ + "referral is zero", + { + receiver: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + referral: 0, + extraData: + "0xeb098d6cde6a202981316b24b19e64d82721e89e000000000000000000000000", + }, + ], + [ + "referral has leading zeros", + { + receiver: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + referral: 31, // hex: 0x001f + extraData: + "0xeb098d6cde6a202981316b24b19e64d82721e89e001f00000000000000000000", + }, + ], + [ + "referral has trailing zeros", + { + receiver: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + referral: 19712, // hex: 0x4d00 + extraData: + "0xeb098d6cde6a202981316b24b19e64d82721e89e4d0000000000000000000000", + }, + ], + [ + "referral is maximum value", + { + receiver: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + referral: 65535, // max uint16 + extraData: + "0xeb098d6cde6a202981316b24b19e64d82721e89effff00000000000000000000", + }, + ], + ]) + + describe("encodeExtraData", () => { + extraDataValidTestData.forEach( + ({ receiver, referral, extraData: expectedExtraData }, testName) => { + it(testName, async () => { + expect( + await tbtcDepositor.encodeExtraData(receiver, referral), + ).to.be.equal(expectedExtraData) + }) + }, + ) + }) + + describe("decodeExtraData", () => { + extraDataValidTestData.forEach( + ( + { receiver: expectedReceiver, referral: expectedReferral, extraData }, + testName, + ) => { + it(testName, async () => { + const [actualReceiver, actualReferral] = + await tbtcDepositor.decodeExtraData(extraData) + + expect(actualReceiver, "invalid receiver").to.be.equal( + expectedReceiver, + ) + 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 expectedReceiver = "0xeb098d6cDE6A202981316b24B19e64D82721e89E" + const expectedReferral = 6851 // hex: 0x1ac3 + + const [actualReceiver, actualReferral] = + await tbtcDepositor.decodeExtraData(extraData) + + expect(actualReceiver, "invalid receiver").to.be.equal(expectedReceiver) + expect(actualReferral, "invalid referral").to.be.equal(expectedReferral) + }) + }) +}) diff --git a/core/test/data/tbtc.ts b/core/test/data/tbtc.ts new file mode 100644 index 000000000..a5838a198 --- /dev/null +++ b/core/test/data/tbtc.ts @@ -0,0 +1,46 @@ +/* eslint-disable import/prefer-default-export */ + +// 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 + receiver: "0xa9B38eA6435c8941d6eDa6a46b68E3e211719699", + // 2-bytes of extraData + referral: "0x5bd1", + extraData: + "0xa9b38ea6435c8941d6eda6a46b68e3e2117196995bd100000000000000000000", + // Deposit key is keccak256(fundingTxHash | fundingOutputIndex). + depositKey: + "0x8dde6118338ae2a046eb77a4acceb0521699275f9cc8e9b50057b29d9de1e844", +} diff --git a/core/test/helpers/context.ts b/core/test/helpers/context.ts index 34875e43d..02d2cf8fa 100644 --- a/core/test/helpers/context.ts +++ b/core/test/helpers/context.ts @@ -1,16 +1,31 @@ import { deployments } from "hardhat" - import { getDeployedContract } from "./contract" -import type { Acre, Dispatcher, TestERC20 } from "../../typechain" +import type { + Acre, + Dispatcher, + TestERC20, + TbtcDepositor, + BridgeStub, + TestERC4626, + TBTCVaultStub, +} from "../../typechain" // eslint-disable-next-line import/prefer-default-export export async function deployment() { await deployments.fixture() - const tbtc: TestERC20 = await getDeployedContract("TBTC") const acre: Acre = await getDeployedContract("Acre") + const tbtcDepositor: TbtcDepositor = + await getDeployedContract("TbtcDepositor") + + const tbtc: TestERC20 = await getDeployedContract("TBTC") + const tbtcBridge: BridgeStub = await getDeployedContract("Bridge") + const tbtcVault: TBTCVaultStub = await getDeployedContract("TBTCVault") + const dispatcher: Dispatcher = await getDeployedContract("Dispatcher") - return { tbtc, acre, dispatcher } + const vault: TestERC4626 = await getDeployedContract("Vault") + + return { tbtc, acre, tbtcDepositor, tbtcBridge, tbtcVault, dispatcher, vault } } diff --git a/core/test/helpers/contract.ts b/core/test/helpers/contract.ts index 88a83812a..6ba7b36ae 100644 --- a/core/test/helpers/contract.ts +++ b/core/test/helpers/contract.ts @@ -1,5 +1,4 @@ -import { ethers } from "ethers" -import { deployments } from "hardhat" +import { deployments, ethers } from "hardhat" import type { BaseContract } from "ethers" import { getUnnamedSigner } from "./signer" diff --git a/core/test/helpers/index.ts b/core/test/helpers/index.ts index 27ddcb0b9..e4df2196a 100644 --- a/core/test/helpers/index.ts +++ b/core/test/helpers/index.ts @@ -1,3 +1,4 @@ export * from "./context" export * from "./contract" export * from "./signer" +export * from "./snapshot" diff --git a/core/test/helpers/snapshot.ts b/core/test/helpers/snapshot.ts new file mode 100644 index 000000000..804c0455d --- /dev/null +++ b/core/test/helpers/snapshot.ts @@ -0,0 +1,36 @@ +import { + SnapshotRestorer, + takeSnapshot, +} from "@nomicfoundation/hardhat-toolbox/network-helpers" + +/** + * Adds a before/after hook pair to snapshot the EVM state before and after tests + * in a test suite. + */ +export function beforeAfterSnapshotWrapper(): void { + let snapshot: SnapshotRestorer + + before(async () => { + snapshot = await takeSnapshot() + }) + + after(async () => { + await snapshot.restore() + }) +} + +/** + * Adds a beforeEach/afterEach hook pair to snapshot the EVM state before and + * after each of tests in a test suite. + */ +export function beforeAfterEachSnapshotWrapper(): void { + let snapshot: SnapshotRestorer + + beforeEach(async () => { + snapshot = await takeSnapshot() + }) + + afterEach(async () => { + await snapshot.restore() + }) +} diff --git a/core/test/helpers/time.ts b/core/test/helpers/time.ts new file mode 100644 index 000000000..933a47c3b --- /dev/null +++ b/core/test/helpers/time.ts @@ -0,0 +1,10 @@ +/* eslint-disable import/prefer-default-export */ +import { ethers } from "hardhat" + +/** + * Returns timestamp of the latest block. + * @return {number} Latest block timestamp. + */ +export async function lastBlockTime(): Promise { + return (await ethers.provider.getBlock("latest"))!.timestamp +} diff --git a/dapp/.eslintrc b/dapp/.eslintrc index 4317c87a7..35437e4ed 100644 --- a/dapp/.eslintrc +++ b/dapp/.eslintrc @@ -15,6 +15,73 @@ 2, { "allowRequiredDefaults": true } ], - "react/require-default-props": [0] + "react/require-default-props": [0], + }, + // FIXME: + // This is temporary solution after changes of the eslint-config version: @thesis-co/eslint-config: "github:thesis/eslint-config#7b9bc8c" + // Overrides rules should be fixed file by file. + "overrides": [ + { + "files": [ + "src/components/Header/ConnectWallet.tsx", + "src/components/Modals/Support/MissingAccount.tsx", + "src/components/Modals/Staking/SignMessage.tsx", + "src/hooks/useDepositBTCTransaction.ts", + "src/components/shared/Form/FormTokenBalanceInput.tsx" + ], + "rules": { + "@typescript-eslint/no-misused-promises": "off" + } + }, + { + "files": [ + "src/hooks/useSignMessage.ts" + ], + "rules": { + "@typescript-eslint/no-floating-promises": "off" + } + }, + { + "files": [ + "src/theme/*" + ], + "rules": { + "@typescript-eslint/unbound-method": "off" + } + }, + { + "files": [ + "src/theme/Alert.ts" + ], + "rules": { + "@typescript-eslint/no-unsafe-member-access": "off" + } + }, + { + "files": [ + "src/components/shared/Form/FormTokenBalanceInput.tsx" + ], + "rules": { + "@typescript-eslint/no-unsafe-assignment": "off" + } + }, + { + "files": [ + "src/components/shared/TokenAmountForm/index.tsx" + ], + "rules": { + "@typescript-eslint/require-await": "off" + } + } + ], + "settings": { + "import/resolver": { + "alias": { + "map": [ + ["#", "./src"] + ], + "extensions": [".js", ".jsx",".ts", ".tsx"] + } + } } } diff --git a/dapp/README.md b/dapp/README.md index f6033bf13..7fb03a1ef 100644 --- a/dapp/README.md +++ b/dapp/README.md @@ -23,4 +23,4 @@ Once the build is running, you can import the manifest on desktop: Click on Browse next to **Add a local app** and select the manifest file. The app is now visible in the menu. -If you have any problems, take a look [here](https://developers.ledger.com/docs/non-dapp/tutorial/3-import/#desktop). +If you have any problems, take a look [here](https://developers.ledger.com/APIs/wallet-api/examples/use-live-app/import). diff --git a/dapp/manifest-ledger-live-app.json b/dapp/manifest-ledger-live-app.json index 29f63c113..265928a59 100644 --- a/dapp/manifest-ledger-live-app.json +++ b/dapp/manifest-ledger-live-app.json @@ -18,6 +18,6 @@ "en": "Bitcoin Liquid Staking" } }, - "permissions": ["account.request"], + "permissions": ["account.request", "message.sign"], "domains": ["http://*"] } diff --git a/dapp/package.json b/dapp/package.json index a3dfaea79..34cc514a8 100644 --- a/dapp/package.json +++ b/dapp/package.json @@ -18,22 +18,27 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@ledgerhq/wallet-api-client": "^1.2.1", - "@ledgerhq/wallet-api-client-react": "^1.1.2", + "@ledgerhq/wallet-api-client": "^1.5.0", + "@ledgerhq/wallet-api-client-react": "^1.3.0", + "formik": "^2.4.5", "framer-motion": "^10.16.5", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-number-format": "^5.3.1" }, "devDependencies": { - "@thesis-co/eslint-config": "^0.6.1", + "@thesis-co/eslint-config": "github:thesis/eslint-config#7b9bc8c", "@types/react": "^18.2.38", "@types/react-dom": "^18.2.17", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", "@vitejs/plugin-react": "^4.2.0", "eslint": "^8.54.0", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-plugin-import": "^2.29.1", "prettier": "^3.1.0", "typescript": "^5.3.2", - "vite": "^5.0.2" + "vite": "^5.0.2", + "vite-plugin-node-polyfills": "^0.19.0" } } diff --git a/dapp/src/components/DocsDrawer/index.tsx b/dapp/src/components/DocsDrawer/index.tsx index 5082af57b..060c6e731 100644 --- a/dapp/src/components/DocsDrawer/index.tsx +++ b/dapp/src/components/DocsDrawer/index.tsx @@ -5,8 +5,8 @@ import { DrawerContent, DrawerOverlay, } from "@chakra-ui/react" -import { useDocsDrawer } from "../../hooks" -import { TextMd } from "../shared/Typography" +import { useDocsDrawer } from "#/hooks" +import { TextMd } from "#/components/shared/Typography" export default function DocsDrawer() { const { isOpen, onClose } = useDocsDrawer() diff --git a/dapp/src/components/GlobalStyles/index.tsx b/dapp/src/components/GlobalStyles/index.tsx index 80b36e6e8..352fe7af0 100644 --- a/dapp/src/components/GlobalStyles/index.tsx +++ b/dapp/src/components/GlobalStyles/index.tsx @@ -1,11 +1,11 @@ import React from "react" import { Global } from "@emotion/react" -import SegmentRegular from "../../fonts/Segment-Regular.otf" -import SegmentMedium from "../../fonts/Segment-Medium.otf" -import SegmentSemiBold from "../../fonts/Segment-SemiBold.otf" -import SegmentBold from "../../fonts/Segment-Bold.otf" -import SegmentBlack from "../../fonts/Segment-Black.otf" +import SegmentRegular from "#/fonts/Segment-Regular.otf" +import SegmentMedium from "#/fonts/Segment-Medium.otf" +import SegmentSemiBold from "#/fonts/Segment-SemiBold.otf" +import SegmentBold from "#/fonts/Segment-Bold.otf" +import SegmentBlack from "#/fonts/Segment-Black.otf" export default function GlobalStyles() { return ( diff --git a/dapp/src/components/Header/ConnectWallet.tsx b/dapp/src/components/Header/ConnectWallet.tsx index 3bb5af7d9..5319a04ca 100644 --- a/dapp/src/components/Header/ConnectWallet.tsx +++ b/dapp/src/components/Header/ConnectWallet.tsx @@ -1,15 +1,15 @@ import React from "react" import { Button, HStack, Icon } from "@chakra-ui/react" import { Account } from "@ledgerhq/wallet-api-client" -import { Bitcoin, Ethereum } from "../../static/icons" import { useRequestBitcoinAccount, useRequestEthereumAccount, useWalletContext, -} from "../../hooks" -import { truncateAddress } from "../../utils" -import { CurrencyBalance } from "../shared/CurrencyBalance" -import { TextMd } from "../shared/Typography" +} from "#/hooks" +import { CurrencyBalance } from "#/components/shared/CurrencyBalance" +import { TextMd } from "#/components/shared/Typography" +import { Bitcoin, Ethereum } from "#/static/icons" +import { truncateAddress } from "#/utils" export type ConnectButtonsProps = { leftIcon: typeof Icon @@ -46,7 +46,7 @@ export default function ConnectWallet() { Balance diff --git a/dapp/src/components/Header/index.tsx b/dapp/src/components/Header/index.tsx index 544fbdee9..480cb502e 100644 --- a/dapp/src/components/Header/index.tsx +++ b/dapp/src/components/Header/index.tsx @@ -1,7 +1,7 @@ import React from "react" import { Flex, HStack, Icon } from "@chakra-ui/react" +import { AcreLogo } from "#/static/icons" import ConnectWallet from "./ConnectWallet" -import { AcreLogo } from "../../static/icons" export default function Header() { return ( diff --git a/dapp/src/components/Modals/ActionForm/index.tsx b/dapp/src/components/Modals/ActionForm/index.tsx new file mode 100644 index 000000000..eabb156bb --- /dev/null +++ b/dapp/src/components/Modals/ActionForm/index.tsx @@ -0,0 +1,41 @@ +import React from "react" +import { + ModalBody, + Tabs, + TabList, + Tab, + TabPanels, + TabPanel, +} from "@chakra-ui/react" +import { useModalFlowContext } from "#/hooks" +import StakeForm from "../Staking/StakeForm" + +const TABS = ["stake", "unstake"] as const + +type Action = (typeof TABS)[number] + +function ActionForm({ action }: { action: Action }) { + const { goNext } = useModalFlowContext() + + return ( + + + + {TABS.map((tab) => ( + + {tab} + + ))} + + + + + + {/* TODO: Add form for unstake */} + + + + ) +} + +export default ActionForm diff --git a/dapp/src/components/Modals/Staking/DepositBTC.tsx b/dapp/src/components/Modals/Staking/DepositBTC.tsx new file mode 100644 index 000000000..924a0ee24 --- /dev/null +++ b/dapp/src/components/Modals/Staking/DepositBTC.tsx @@ -0,0 +1,20 @@ +import React from "react" +import { useDepositBTCTransaction, useModalFlowContext } from "#/hooks" +import Alert from "#/components/shared/Alert" +import { TextMd } from "#/components/shared/Typography" +import StakingSteps from "./components/StakingSteps" + +export default function DepositBTC() { + const { goNext } = useModalFlowContext() + const { depositBTC } = useDepositBTCTransaction(goNext) + + return ( + + + + Make a Bitcoin transaction to deposit and stake your BTC. + + + + ) +} diff --git a/dapp/src/components/Modals/Staking/Overview.tsx b/dapp/src/components/Modals/Staking/Overview.tsx deleted file mode 100644 index 2ad8a1e94..000000000 --- a/dapp/src/components/Modals/Staking/Overview.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react" -import { Button, ModalBody, ModalFooter } from "@chakra-ui/react" -import { ModalStep } from "../../../contexts" -import { TextMd } from "../../shared/Typography" - -export default function Overview({ goNext }: ModalStep) { - return ( - <> - - Staking overview - - - - - - ) -} diff --git a/dapp/src/components/Modals/Staking/Overview/index.tsx b/dapp/src/components/Modals/Staking/Overview/index.tsx new file mode 100644 index 000000000..af2485db6 --- /dev/null +++ b/dapp/src/components/Modals/Staking/Overview/index.tsx @@ -0,0 +1,36 @@ +import React from "react" +import { + Button, + ModalBody, + ModalFooter, + ModalHeader, + StepNumber, +} from "@chakra-ui/react" +import StepperBase from "#/components/shared/StepperBase" +import { useModalFlowContext } from "#/hooks" +import { STEPS } from "./steps" + +export default function Overview() { + const { goNext } = useModalFlowContext() + + return ( + <> + Staking steps overview + + } + steps={STEPS} + /> + + + + + + ) +} diff --git a/dapp/src/components/Modals/Staking/Overview/steps.tsx b/dapp/src/components/Modals/Staking/Overview/steps.tsx new file mode 100644 index 000000000..7a6c3ce7a --- /dev/null +++ b/dapp/src/components/Modals/Staking/Overview/steps.tsx @@ -0,0 +1,25 @@ +import React from "react" +import { StepBase } from "#/components/shared/StepperBase" +import { Description, Title } from "../components/StakingSteps" + +export const STEPS: StepBase[] = [ + { + id: "sign-message", + title: Sign message, + description: ( + + You will sign a gas-free Ethereum message to indicate the address where + you'd like to get your stBTC liquid staking token. + + ), + }, + { + id: "deposit-btc", + title: Deposit BTC, + description: ( + + You will make a Bitcoin transaction to deposit and stake your BTC. + + ), + }, +] diff --git a/dapp/src/components/Modals/Staking/SignMessage.tsx b/dapp/src/components/Modals/Staking/SignMessage.tsx new file mode 100644 index 000000000..e3cda7886 --- /dev/null +++ b/dapp/src/components/Modals/Staking/SignMessage.tsx @@ -0,0 +1,25 @@ +import React from "react" +import { Highlight } from "@chakra-ui/react" +import { useModalFlowContext, useSignMessage } from "#/hooks" +import Alert from "#/components/shared/Alert" +import { TextMd } from "#/components/shared/Typography" +import StakingSteps from "./components/StakingSteps" + +export default function SignMessage() { + const { goNext } = useModalFlowContext() + const { signMessage } = useSignMessage(goNext) + + return ( + + {/* TODO: Add the correct action after click */} + {}}> + + + You will receive stBTC liquid staking token at this Ethereum address + once the staking transaction is completed. + + + + + ) +} diff --git a/dapp/src/components/Modals/Staking/StakeForm.tsx b/dapp/src/components/Modals/Staking/StakeForm.tsx deleted file mode 100644 index 8d308ecff..000000000 --- a/dapp/src/components/Modals/Staking/StakeForm.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react" -import { Button, ModalBody } from "@chakra-ui/react" -import { ModalStep } from "../../../contexts" -import { TextMd } from "../../shared/Typography" - -export default function StakeModal({ goNext }: ModalStep) { - return ( - - Stake modal - - - ) -} diff --git a/dapp/src/components/Modals/Staking/StakeForm/Details.tsx b/dapp/src/components/Modals/Staking/StakeForm/Details.tsx new file mode 100644 index 000000000..698e6da54 --- /dev/null +++ b/dapp/src/components/Modals/Staking/StakeForm/Details.tsx @@ -0,0 +1,48 @@ +import React from "react" +import { List } from "@chakra-ui/react" +import TransactionDetailsAmountItem from "#/components/shared/TransactionDetails/AmountItem" +import { useTokenAmountFormValue } from "#/components/shared/TokenAmountForm/TokenAmountFormBase" +import { useTransactionDetails } from "#/hooks" +import { CurrencyType } from "#/types" + +function Details({ currency }: { currency: CurrencyType }) { + const value = useTokenAmountFormValue() + const details = useTransactionDetails(value ?? 0n) + + return ( + + + + + + ) +} + +export default Details diff --git a/dapp/src/components/Modals/Staking/StakeForm/index.tsx b/dapp/src/components/Modals/Staking/StakeForm/index.tsx new file mode 100644 index 000000000..e5d5a0754 --- /dev/null +++ b/dapp/src/components/Modals/Staking/StakeForm/index.tsx @@ -0,0 +1,40 @@ +import React, { useCallback } from "react" +import { Button } from "@chakra-ui/react" +import { BITCOIN_MIN_AMOUNT } from "#/constants" +import { ModalStep } from "#/contexts" +import TokenAmountForm from "#/components/shared/TokenAmountForm" +import { TokenAmountFormValues } from "#/components/shared/TokenAmountForm/TokenAmountFormBase" +import { useWalletContext, useTransactionContext } from "#/hooks" +import Details from "./Details" + +function StakeForm({ goNext }: ModalStep) { + const { btcAccount } = useWalletContext() + const { setTokenAmount } = useTransactionContext() + + const handleSubmitForm = useCallback( + (values: TokenAmountFormValues) => { + if (!values.amount) return + + setTokenAmount({ amount: values.amount, currency: "bitcoin" }) + goNext() + }, + [goNext, setTokenAmount], + ) + + return ( + +
+ + + ) +} + +export default StakeForm diff --git a/dapp/src/components/Modals/Staking/components/StakingSteps.tsx b/dapp/src/components/Modals/Staking/components/StakingSteps.tsx new file mode 100644 index 000000000..f092ad079 --- /dev/null +++ b/dapp/src/components/Modals/Staking/components/StakingSteps.tsx @@ -0,0 +1,77 @@ +import React from "react" +import { + Button, + HStack, + ModalBody, + ModalFooter, + ModalHeader, +} from "@chakra-ui/react" +import { TextLg, TextMd } from "#/components/shared/Typography" +import StepperBase, { StepBase } from "#/components/shared/StepperBase" +import Spinner from "#/components/shared/Spinner" + +export function Title({ children }: { children: React.ReactNode }) { + return {children} +} + +export function Description({ children }: { children: React.ReactNode }) { + return {children} +} + +const STEPS: StepBase[] = [ + { + id: "sign-message", + title: Sign message, + description: ( + + + Sign the message in your ETH wallet. + + ), + }, + { + id: "deposit-btc", + title: Deposit BTC, + description: ( + + + Waiting for your deposit... + + ), + }, +] + +export default function StakingSteps({ + buttonText, + activeStep, + onClick, + children, +}: { + buttonText: string + activeStep: number + onClick: () => void + children: React.ReactNode +}) { + return ( + <> + {`Step ${activeStep + 1} / ${STEPS.length}`} + + + {children} + + + + + + ) +} diff --git a/dapp/src/components/Modals/Staking/index.tsx b/dapp/src/components/Modals/Staking/index.tsx index d5b117df3..e74dd57ea 100644 --- a/dapp/src/components/Modals/Staking/index.tsx +++ b/dapp/src/components/Modals/Staking/index.tsx @@ -1,17 +1,23 @@ import React from "react" -import { useModalFlowContext } from "../../../hooks" -import StakeForm from "./StakeForm" +import { useModalFlowContext } from "#/hooks" +import ModalBase from "#/components/shared/ModalBase" import Overview from "./Overview" -import ModalBase from "../../shared/ModalBase" +import ActionForm from "../ActionForm" +import SignMessage from "./SignMessage" +import DepositBTC from "./DepositBTC" -function StakingSteps() { - const { activeStep, goNext } = useModalFlowContext() +function ActiveStakingStep() { + const { activeStep } = useModalFlowContext() switch (activeStep) { case 1: - return + return case 2: - return + return + case 3: + return + case 4: + return default: return null } @@ -25,8 +31,8 @@ export default function StakingModal({ onClose: () => void }) { return ( - - + + ) } diff --git a/dapp/src/components/Modals/Support/MissingAccount.tsx b/dapp/src/components/Modals/Support/MissingAccount.tsx index 7b6216276..89ca3effd 100644 --- a/dapp/src/components/Modals/Support/MissingAccount.tsx +++ b/dapp/src/components/Modals/Support/MissingAccount.tsx @@ -7,41 +7,41 @@ import { ModalFooter, ModalHeader, } from "@chakra-ui/react" -import { CurrencyType, RequestAccountParams } from "../../../types" -import { TextMd } from "../../shared/Typography" -import AlertWrapper from "../../shared/Alert" -import { CURRENCIES_BY_TYPE } from "../../../constants" +import { TextMd } from "#/components/shared/Typography" +import Alert from "#/components/shared/Alert" +import { getCurrencyByType } from "#/utils" +import { CurrencyType, RequestAccountParams } from "#/types" type MissingAccountProps = { - currencyType: CurrencyType + currency: CurrencyType icon: typeof Icon requestAccount: (...params: RequestAccountParams) => Promise } export default function MissingAccount({ - currencyType, + currency, icon, requestAccount, }: MissingAccountProps) { - const currency = CURRENCIES_BY_TYPE[currencyType] + const { name, symbol } = getCurrencyByType(currency) return ( <> - {currency.name} account not installed + {name} account not installed - {currency.name} account is required to make transactions for - depositing and staking your {currency.symbol}. + {name} account is required to make transactions for depositing and + staking your {symbol}. - + You will be sent to the Ledger Accounts section to perform this action. - + + + + + {!hasError && !helperText && ( + + + + )} + + ) +} diff --git a/dapp/src/components/shared/TransactionDetails/AmountItem.tsx b/dapp/src/components/shared/TransactionDetails/AmountItem.tsx new file mode 100644 index 000000000..cf0c29045 --- /dev/null +++ b/dapp/src/components/shared/TransactionDetails/AmountItem.tsx @@ -0,0 +1,36 @@ +import React, { ComponentProps } from "react" +import { Flex } from "@chakra-ui/react" +import TransactionDetailsItem, { TransactionDetailsItemProps } from "." +import { CurrencyBalanceWithConversion } from "../CurrencyBalanceWithConversion" + +type TransactionDetailsAmountItemProps = ComponentProps< + typeof CurrencyBalanceWithConversion +> & + Pick + +function TransactionDetailsAmountItem({ + label, + from, + to, +}: TransactionDetailsAmountItemProps) { + return ( + + + + + + ) +} + +export default TransactionDetailsAmountItem diff --git a/dapp/src/components/shared/TransactionDetails/index.tsx b/dapp/src/components/shared/TransactionDetails/index.tsx new file mode 100644 index 000000000..bdaf6552c --- /dev/null +++ b/dapp/src/components/shared/TransactionDetails/index.tsx @@ -0,0 +1,32 @@ +import React from "react" +import { ListItem, ListItemProps } from "@chakra-ui/react" +import { TextMd } from "../Typography" + +export type TransactionDetailsItemProps = { + label: string + value?: string + children?: React.ReactNode +} & ListItemProps + +function TransactionDetailsItem({ + label, + value, + children, + ...listItemProps +}: TransactionDetailsItemProps) { + return ( + + + {label} + + {value ? {value} : children} + + ) +} + +export default TransactionDetailsItem diff --git a/dapp/src/constants/currency.ts b/dapp/src/constants/currency.ts index c35c7c9c6..6f747bb1a 100644 --- a/dapp/src/constants/currency.ts +++ b/dapp/src/constants/currency.ts @@ -1,4 +1,4 @@ -import { Currency, CurrencyType } from "../types" +import { Currency, CurrencyType } from "#/types" export const BITCOIN: Currency = { name: "Bitcoin", @@ -27,5 +27,5 @@ export const CURRENCY_ID_ETHEREUM = export const CURRENCIES_BY_TYPE: Record = { bitcoin: BITCOIN, ethereum: ETHEREUM, - usd: ETHEREUM, + usd: USD, } diff --git a/dapp/src/constants/index.ts b/dapp/src/constants/index.ts index 68cb50031..a5cb59713 100644 --- a/dapp/src/constants/index.ts +++ b/dapp/src/constants/index.ts @@ -1 +1,2 @@ export * from "./currency" +export * from "./staking" diff --git a/dapp/src/constants/staking.ts b/dapp/src/constants/staking.ts new file mode 100644 index 000000000..421c57bb5 --- /dev/null +++ b/dapp/src/constants/staking.ts @@ -0,0 +1 @@ +export const BITCOIN_MIN_AMOUNT = "1000000" // 0.01 BTC diff --git a/dapp/src/contexts/TransactionContext.tsx b/dapp/src/contexts/TransactionContext.tsx new file mode 100644 index 000000000..3064bd530 --- /dev/null +++ b/dapp/src/contexts/TransactionContext.tsx @@ -0,0 +1,37 @@ +import React, { createContext, useMemo, useState } from "react" +import { TokenAmount } from "#/types" + +type TransactionContextValue = { + tokenAmount?: TokenAmount + setTokenAmount: React.Dispatch> +} + +export const TransactionContext = createContext({ + tokenAmount: undefined, + setTokenAmount: () => {}, +}) + +export function TransactionContextProvider({ + children, +}: { + children: React.ReactNode +}): React.ReactElement { + const [tokenAmount, setTokenAmount] = useState( + undefined, + ) + + const contextValue: TransactionContextValue = + useMemo( + () => ({ + tokenAmount, + setTokenAmount, + }), + [tokenAmount], + ) + + return ( + + {children} + + ) +} diff --git a/dapp/src/contexts/index.tsx b/dapp/src/contexts/index.tsx index 6787e1cc6..a849afb3f 100644 --- a/dapp/src/contexts/index.tsx +++ b/dapp/src/contexts/index.tsx @@ -3,3 +3,4 @@ export * from "./LedgerWalletAPIProvider" export * from "./DocsDrawerContext" export * from "./SidebarContext" export * from "./ModalFlowContext" +export * from "./TransactionContext" diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index 076da9a53..d05d9cf04 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -5,3 +5,7 @@ export * from "./useWalletContext" export * from "./useSidebar" export * from "./useDocsDrawer" export * from "./useModalFlowContext" +export * from "./useTransactionContext" +export * from "./useTransactionDetails" +export * from "./useSignMessage" +export * from "./useDepositBTCTransaction" diff --git a/dapp/src/hooks/useDepositBTCTransaction.ts b/dapp/src/hooks/useDepositBTCTransaction.ts new file mode 100644 index 000000000..0bef0c4be --- /dev/null +++ b/dapp/src/hooks/useDepositBTCTransaction.ts @@ -0,0 +1,13 @@ +import { useCallback } from "react" +import { OnSuccessCallback } from "#/types" + +export function useDepositBTCTransaction(onSuccess?: OnSuccessCallback) { + // TODO: sending transactions using the SDK + const depositBTC = useCallback(() => { + if (onSuccess) { + setTimeout(onSuccess, 1000) + } + }, [onSuccess]) + + return { depositBTC } +} diff --git a/dapp/src/hooks/useDocsDrawer.ts b/dapp/src/hooks/useDocsDrawer.ts index 4ce8bba3e..3536dba2a 100644 --- a/dapp/src/hooks/useDocsDrawer.ts +++ b/dapp/src/hooks/useDocsDrawer.ts @@ -1,5 +1,5 @@ import { useContext } from "react" -import { DocsDrawerContext } from "../contexts" +import { DocsDrawerContext } from "#/contexts" export function useDocsDrawer() { const context = useContext(DocsDrawerContext) diff --git a/dapp/src/hooks/useModalFlowContext.ts b/dapp/src/hooks/useModalFlowContext.ts index 48882c561..fda6eb681 100644 --- a/dapp/src/hooks/useModalFlowContext.ts +++ b/dapp/src/hooks/useModalFlowContext.ts @@ -1,5 +1,5 @@ import { useContext } from "react" -import { ModalFlowContext } from "../contexts" +import { ModalFlowContext } from "#/contexts" export function useModalFlowContext() { const context = useContext(ModalFlowContext) diff --git a/dapp/src/hooks/useRequestBitcoinAccount.ts b/dapp/src/hooks/useRequestBitcoinAccount.ts index 036db196d..051467dcf 100644 --- a/dapp/src/hooks/useRequestBitcoinAccount.ts +++ b/dapp/src/hooks/useRequestBitcoinAccount.ts @@ -1,8 +1,8 @@ import { useRequestAccount } from "@ledgerhq/wallet-api-client-react" import { useCallback, useContext, useEffect } from "react" -import { CURRENCY_ID_BITCOIN } from "../constants" -import { UseRequestAccountReturn } from "../types" -import { WalletContext } from "../contexts" +import { WalletContext } from "#/contexts" +import { UseRequestAccountReturn } from "#/types" +import { CURRENCY_ID_BITCOIN } from "#/constants" export function useRequestBitcoinAccount(): UseRequestAccountReturn { const { setBtcAccount } = useContext(WalletContext) diff --git a/dapp/src/hooks/useRequestEthereumAccount.ts b/dapp/src/hooks/useRequestEthereumAccount.ts index 5c3cad1f1..fcf18c6ff 100644 --- a/dapp/src/hooks/useRequestEthereumAccount.ts +++ b/dapp/src/hooks/useRequestEthereumAccount.ts @@ -1,8 +1,8 @@ import { useRequestAccount } from "@ledgerhq/wallet-api-client-react" import { useCallback, useContext, useEffect } from "react" -import { CURRENCY_ID_ETHEREUM } from "../constants" -import { UseRequestAccountReturn } from "../types" -import { WalletContext } from "../contexts" +import { WalletContext } from "#/contexts" +import { UseRequestAccountReturn } from "#/types" +import { CURRENCY_ID_ETHEREUM } from "#/constants" export function useRequestEthereumAccount(): UseRequestAccountReturn { const { setEthAccount } = useContext(WalletContext) diff --git a/dapp/src/hooks/useSidebar.ts b/dapp/src/hooks/useSidebar.ts index 986143a89..944364076 100644 --- a/dapp/src/hooks/useSidebar.ts +++ b/dapp/src/hooks/useSidebar.ts @@ -1,5 +1,5 @@ import { useContext } from "react" -import { SidebarContext } from "../contexts" +import { SidebarContext } from "#/contexts" export function useSidebar() { const context = useContext(SidebarContext) diff --git a/dapp/src/hooks/useSignMessage.ts b/dapp/src/hooks/useSignMessage.ts new file mode 100644 index 000000000..cd38d1001 --- /dev/null +++ b/dapp/src/hooks/useSignMessage.ts @@ -0,0 +1,26 @@ +import { useSignMessage as useSignMessageLedgerLive } from "@ledgerhq/wallet-api-client-react" +import { useCallback, useEffect } from "react" +import { OnSuccessCallback } from "#/types" +import { useWalletContext } from "./useWalletContext" + +const SIGN_MESSAGE = "Test message" + +export function useSignMessage(onSuccess?: OnSuccessCallback) { + const { ethAccount } = useWalletContext() + const { signMessage, signature } = useSignMessageLedgerLive() + + useEffect(() => { + if (signature && onSuccess) { + onSuccess() + } + }, [onSuccess, signature]) + + // TODO: signing message using the SDK + const handleSignMessage = useCallback(async () => { + if (!ethAccount?.id) return + + await signMessage(ethAccount.id, Buffer.from(SIGN_MESSAGE, "utf-8")) + }, [ethAccount, signMessage]) + + return { signMessage: handleSignMessage } +} diff --git a/dapp/src/hooks/useTransactionContext.ts b/dapp/src/hooks/useTransactionContext.ts new file mode 100644 index 000000000..41a8a8359 --- /dev/null +++ b/dapp/src/hooks/useTransactionContext.ts @@ -0,0 +1,14 @@ +import { useContext } from "react" +import { TransactionContext } from "#/contexts" + +export function useTransactionContext() { + const context = useContext(TransactionContext) + + if (!context) { + throw new Error( + "TransactionContext used outside of TransactionContext component", + ) + } + + return context +} diff --git a/dapp/src/hooks/useTransactionDetails.ts b/dapp/src/hooks/useTransactionDetails.ts new file mode 100644 index 000000000..f8f25f96b --- /dev/null +++ b/dapp/src/hooks/useTransactionDetails.ts @@ -0,0 +1,30 @@ +import { useEffect, useState } from "react" + +type UseTransactionDetailsResult = { + btcAmount: string + protocolFee: string + estimatedAmount: string +} + +export function useTransactionDetails(amount: bigint | undefined) { + const [details, setDetails] = useState< + UseTransactionDetailsResult | undefined + >(undefined) + + useEffect(() => { + if (!amount) { + setDetails(undefined) + } else { + const protocolFee = amount / 10000n + const estimatedAmount = amount - protocolFee + + setDetails({ + btcAmount: amount.toString(), + protocolFee: protocolFee.toString(), + estimatedAmount: estimatedAmount.toString(), + }) + } + }, [amount]) + + return details +} diff --git a/dapp/src/hooks/useWalletContext.ts b/dapp/src/hooks/useWalletContext.ts index 0da19204b..afca84aff 100644 --- a/dapp/src/hooks/useWalletContext.ts +++ b/dapp/src/hooks/useWalletContext.ts @@ -1,5 +1,5 @@ import { useContext } from "react" -import { WalletContext } from "../contexts" +import { WalletContext } from "#/contexts" export function useWalletContext() { const context = useContext(WalletContext) diff --git a/dapp/src/theme/Alert.ts b/dapp/src/theme/Alert.ts index 28bbc7e3c..d03592cc0 100644 --- a/dapp/src/theme/Alert.ts +++ b/dapp/src/theme/Alert.ts @@ -6,8 +6,10 @@ import { defineStyle, } from "@chakra-ui/react" +const KEYS = [...parts.keys, "rightIconContainer"] as const + const { defineMultiStyleConfig, definePartsStyle } = - createMultiStyleConfigHelpers(parts.keys) + createMultiStyleConfigHelpers(KEYS) const baseStyleDialog = defineStyle({ py: 5, @@ -24,9 +26,23 @@ const baseStyleIcon = defineStyle({ mr: 4, }) +const baseStyleRightIconContainer = defineStyle({ + position: "absolute", + right: 0, + top: 0, + p: 5, + h: "100%", + borderLeft: "2px solid white", + color: "brand.400", + display: "flex", + alignItems: "center", + w: 14, +}) + const baseStyle = definePartsStyle({ container: baseStyleDialog, icon: baseStyleIcon, + rightIconContainer: baseStyleRightIconContainer, }) const statusInfo = definePartsStyle({ diff --git a/dapp/src/theme/Form.ts b/dapp/src/theme/Form.ts new file mode 100644 index 000000000..42a4a7165 --- /dev/null +++ b/dapp/src/theme/Form.ts @@ -0,0 +1,21 @@ +import { formAnatomy as parts } from "@chakra-ui/anatomy" +import { createMultiStyleConfigHelpers, defineStyle } from "@chakra-ui/react" + +const { defineMultiStyleConfig, definePartsStyle } = + createMultiStyleConfigHelpers(parts.keys) + +const baseStyleHelperText = defineStyle({ + display: "flex", + alignItems: "center", + gap: 1, + fontWeight: "medium", + color: "grey.500", +}) + +const baseStyle = definePartsStyle({ + helperText: baseStyleHelperText, +}) + +const Form = defineMultiStyleConfig({ baseStyle }) + +export default Form diff --git a/dapp/src/theme/FormError.ts b/dapp/src/theme/FormError.ts new file mode 100644 index 000000000..f9b8dd9b3 --- /dev/null +++ b/dapp/src/theme/FormError.ts @@ -0,0 +1,19 @@ +import { defineStyle, createMultiStyleConfigHelpers } from "@chakra-ui/react" + +import { formErrorAnatomy as parts } from "@chakra-ui/anatomy" + +const { defineMultiStyleConfig, definePartsStyle } = + createMultiStyleConfigHelpers(parts.keys) + +const baseStyleText = defineStyle({ + fontWeight: "medium", + color: "red.400", +}) + +const baseStyle = definePartsStyle({ + text: baseStyleText, +}) + +const FormError = defineMultiStyleConfig({ baseStyle }) + +export default FormError diff --git a/dapp/src/theme/FormLabel.ts b/dapp/src/theme/FormLabel.ts new file mode 100644 index 000000000..08e211903 --- /dev/null +++ b/dapp/src/theme/FormLabel.ts @@ -0,0 +1,25 @@ +import { defineStyle, defineStyleConfig } from "@chakra-ui/react" + +const baseStyle = defineStyle({ + fontWeight: "semibold", + color: "grey.700", +}) + +const sizeMd = defineStyle({ + fontSize: "sm", + lineHeight: "sm", +}) + +const sizeLg = defineStyle({ + fontSize: "md", + lineHeight: "md", +}) + +const sizes = { + md: sizeMd, + lg: sizeLg, +} + +const FormLabel = defineStyleConfig({ baseStyle, sizes }) + +export default FormLabel diff --git a/dapp/src/theme/Input.ts b/dapp/src/theme/Input.ts new file mode 100644 index 000000000..cccff7c7c --- /dev/null +++ b/dapp/src/theme/Input.ts @@ -0,0 +1,44 @@ +import { inputAnatomy as parts } from "@chakra-ui/anatomy" +import { createMultiStyleConfigHelpers, defineStyle } from "@chakra-ui/react" + +const { definePartsStyle, defineMultiStyleConfig } = + createMultiStyleConfigHelpers(parts.keys) + +const variantBalanceField = defineStyle({ + border: "1px solid", + borderColor: "gold.300", + color: "grey.700", + fontWeight: "bold", + bg: "opacity.white.5", + paddingRight: 20, + // TODO: Set the color correctly without using the chakra variable. + caretColor: "var(--chakra-colors-brand-400)", + + _placeholder: { + color: "grey.300", + fontWeight: "medium", + }, + + _invalid: { + color: "red.400", + }, +}) + +const variantBalanceElement = defineStyle({ + h: "100%", + width: 14, + mr: 2, +}) + +const variantBalance = definePartsStyle({ + field: variantBalanceField, + element: variantBalanceElement, +}) + +const variants = { + balance: variantBalance, +} + +const Input = defineMultiStyleConfig({ variants }) + +export default Input diff --git a/dapp/src/theme/Spinner.ts b/dapp/src/theme/Spinner.ts new file mode 100644 index 000000000..74b0f3ff8 --- /dev/null +++ b/dapp/src/theme/Spinner.ts @@ -0,0 +1,11 @@ +import { defineStyle, defineStyleConfig } from "@chakra-ui/react" + +const baseStyle = defineStyle({ + color: "brand.400", + borderWidth: 3, + borderTopColor: "gold.400", + borderBottomColor: "gold.400", + borderLeftColor: "gold.400", +}) + +export const spinnerTheme = defineStyleConfig({ baseStyle }) diff --git a/dapp/src/theme/Tabs.ts b/dapp/src/theme/Tabs.ts new file mode 100644 index 000000000..4f48860fa --- /dev/null +++ b/dapp/src/theme/Tabs.ts @@ -0,0 +1,51 @@ +import { tabsAnatomy as parts } from "@chakra-ui/anatomy" +import { createMultiStyleConfigHelpers, defineStyle } from "@chakra-ui/react" + +const { definePartsStyle, defineMultiStyleConfig } = + createMultiStyleConfigHelpers(parts.keys) + +const baseStyle = definePartsStyle({ + tab: { + fontWeight: "bold", + color: "grey.400", + }, +}) + +const variantUnderlineTab = defineStyle({ + pb: 4, + borderBottom: "2px solid", + borderColor: "transparent", + background: "transparent", + textTransform: "capitalize", + + _selected: { + color: "grey.700", + borderColor: "brand.400", + }, + _hover: { + color: "grey.700", + }, +}) + +const variantUnderlineTabList = defineStyle({ + gap: 5, + pb: 6, +}) + +const variantUnderlineTabPanel = defineStyle({ + px: 0, +}) + +const variantUnderline = definePartsStyle({ + tab: variantUnderlineTab, + tablist: variantUnderlineTabList, + tabpanel: variantUnderlineTabPanel, +}) + +const variants = { + underline: variantUnderline, +} + +const Tabs = defineMultiStyleConfig({ baseStyle, variants }) + +export default Tabs diff --git a/dapp/src/theme/TokenBalanceInput.ts b/dapp/src/theme/TokenBalanceInput.ts new file mode 100644 index 000000000..8c662a769 --- /dev/null +++ b/dapp/src/theme/TokenBalanceInput.ts @@ -0,0 +1,31 @@ +import { createMultiStyleConfigHelpers, defineStyle } from "@chakra-ui/react" + +const PARTS = ["labelContainer", "balanceContainer", "balance"] + +const { defineMultiStyleConfig, definePartsStyle } = + createMultiStyleConfigHelpers(PARTS) + +const baseStyleLabelContainer = defineStyle({ + display: "flex", + justifyContent: "space-between", +}) + +const baseStyleBalanceContainer = defineStyle({ + display: "flex", + gap: 1, +}) + +const baseStyleBalance = defineStyle({ + fontWeight: "medium", + color: "grey.500", +}) + +const baseStyle = definePartsStyle({ + labelContainer: baseStyleLabelContainer, + balanceContainer: baseStyleBalanceContainer, + balance: baseStyleBalance, +}) + +const TokenBalanceInput = defineMultiStyleConfig({ baseStyle }) + +export default TokenBalanceInput diff --git a/dapp/src/theme/index.ts b/dapp/src/theme/index.ts index e5de072c0..421fabe96 100644 --- a/dapp/src/theme/index.ts +++ b/dapp/src/theme/index.ts @@ -10,8 +10,15 @@ import Tooltip from "./Tooltip" import Heading from "./Heading" import Sidebar from "./Sidebar" import CurrencyBalance from "./CurrencyBalance" +import TokenBalanceInput from "./TokenBalanceInput" +import Input from "./Input" import Stepper from "./Stepper" import Alert from "./Alert" +import Form from "./Form" +import FormLabel from "./FormLabel" +import FormError from "./FormError" +import Tabs from "./Tabs" +import { spinnerTheme as Spinner } from "./Spinner" const defaultTheme = { colors, @@ -38,8 +45,15 @@ const defaultTheme = { CurrencyBalance, Card, Tooltip, + Input, + TokenBalanceInput, Stepper, Alert, + Form, + FormLabel, + FormError, + Tabs, + Spinner, }, } diff --git a/dapp/src/types/callback.ts b/dapp/src/types/callback.ts new file mode 100644 index 000000000..526705388 --- /dev/null +++ b/dapp/src/types/callback.ts @@ -0,0 +1 @@ +export type OnSuccessCallback = () => void | Promise diff --git a/dapp/src/types/index.ts b/dapp/src/types/index.ts index 1e77e81e7..ea2933681 100644 --- a/dapp/src/types/index.ts +++ b/dapp/src/types/index.ts @@ -1,2 +1,4 @@ export * from "./ledger-live-app" export * from "./currency" +export * from "./staking" +export * from "./callback" diff --git a/dapp/src/types/staking.ts b/dapp/src/types/staking.ts new file mode 100644 index 000000000..56b4740be --- /dev/null +++ b/dapp/src/types/staking.ts @@ -0,0 +1,6 @@ +import { CurrencyType } from "./currency" + +export type TokenAmount = { + amount: bigint + currency: CurrencyType +} diff --git a/dapp/src/utils/currency.ts b/dapp/src/utils/currency.ts new file mode 100644 index 000000000..4d8b02a78 --- /dev/null +++ b/dapp/src/utils/currency.ts @@ -0,0 +1,5 @@ +import { Currency, CurrencyType } from "#/types" +import { CURRENCIES_BY_TYPE } from "#/constants" + +export const getCurrencyByType = (currency: CurrencyType): Currency => + CURRENCIES_BY_TYPE[currency] diff --git a/dapp/src/utils/forms.ts b/dapp/src/utils/forms.ts new file mode 100644 index 000000000..97872a8f4 --- /dev/null +++ b/dapp/src/utils/forms.ts @@ -0,0 +1,39 @@ +import { CurrencyType } from "#/types" +import { getCurrencyByType } from "./currency" +import { formatTokenAmount } from "./numbers" + +const ERRORS = { + REQUIRED: "Required.", + EXCEEDED_VALUE: "The amount exceeds your current balance.", + INSUFFICIENT_VALUE: (minValue: string) => + `The minimum amount must be at least ${minValue} BTC.`, +} + +export function getErrorsObj(errors: { [key in keyof T]: string }) { + return (Object.keys(errors) as Array).every((name) => !errors[name]) + ? {} + : errors +} + +export function validateTokenAmount( + value: bigint | undefined, + maxValue: string, + minValue: string, + currency: CurrencyType, +): string | undefined { + if (value === undefined) return ERRORS.REQUIRED + + const { decimals } = getCurrencyByType(currency) + + const maxValueInBI = BigInt(maxValue) + const minValueInBI = BigInt(minValue) + + const isMaximumValueExceeded = value > maxValueInBI + const isMinimumValueFulfilled = value >= minValueInBI + + if (isMaximumValueExceeded) return ERRORS.EXCEEDED_VALUE + if (!isMinimumValueFulfilled) + return ERRORS.INSUFFICIENT_VALUE(formatTokenAmount(minValue, decimals)) + + return undefined +} diff --git a/dapp/src/utils/index.ts b/dapp/src/utils/index.ts index 613e0f071..02eaa61b8 100644 --- a/dapp/src/utils/index.ts +++ b/dapp/src/utils/index.ts @@ -1,2 +1,4 @@ export * from "./numbers" export * from "./address" +export * from "./forms" +export * from "./currency" diff --git a/dapp/src/utils/numbers.ts b/dapp/src/utils/numbers.ts index d2de47cca..faf472c31 100644 --- a/dapp/src/utils/numbers.ts +++ b/dapp/src/utils/numbers.ts @@ -1,12 +1,23 @@ -export const toLocaleString = (value: number): string => - value.toLocaleString("default", { maximumFractionDigits: 2 }) +export const numberToLocaleString = ( + value: string | number, + desiredDecimals = 2, +): string => { + const number = typeof value === "number" ? value : parseFloat(value) + + if (number === 0) return `0.${"0".repeat(desiredDecimals)}` + + return number.toLocaleString("default", { + maximumFractionDigits: desiredDecimals, + }) +} /** * Convert a fixed point bigint with precision `fixedPointDecimals` to a * floating point number truncated to `desiredDecimals`. * * This function is based on the solution used by the Taho extension. - * More info: https://github.com/tahowallet/extension/blob/main/background/lib/fixed-point.ts#L216-L239 + * Source: + * https://github.com/tahowallet/extension/blob/main/background/lib/fixed-point.ts#L216-L239 */ export function bigIntToUserAmount( fixedPoint: bigint, @@ -58,10 +69,130 @@ export const formatTokenAmount = ( return `<0.${"0".repeat(desiredDecimals - 1)}1` } - return toLocaleString(formattedAmount) + return numberToLocaleString(formattedAmount) } export const formatSatoshiAmount = ( amount: number | string, desiredDecimals = 2, ) => formatTokenAmount(amount, 8, desiredDecimals) + +/** + * Converts a fixed point number with a bigint amount and a decimals field + * indicating the orders of magnitude in `amount` behind the decimal point into + * a string in US decimal format (no thousands separators, . for the decimal + * separator). + * + * Used in cases where precision is critical. + * + * This function is based on the solution used by the Taho extension. + * Source: + * https://github.com/tahowallet/extension/blob/main/background/lib/fixed-point.ts#L172-L214 + */ +export function fixedPointNumberToString( + amount: bigint, + decimals: number, + trimTrailingZeros = true, +): string { + const undecimaledAmount = amount.toString() + const preDecimalLength = undecimaledAmount.length - decimals + + const preDecimalCharacters = + preDecimalLength > 0 + ? undecimaledAmount.substring(0, preDecimalLength) + : "0" + const postDecimalCharacters = + "0".repeat(Math.max(-preDecimalLength, 0)) + + undecimaledAmount.substring(preDecimalLength) + + const trimmedPostDecimalCharacters = trimTrailingZeros + ? postDecimalCharacters.replace(/0*$/, "") + : postDecimalCharacters + + const decimalString = + trimmedPostDecimalCharacters.length > 0 + ? `.${trimmedPostDecimalCharacters}` + : "" + + return `${preDecimalCharacters}${decimalString}` +} + +/** + * Convert a fixed point bigint with precision `fixedPointDecimals` to another + * fixed point bigint with precision `targetDecimals`. + * + * It is highly recommended that the precision of the fixed point bigint is + * tracked alongside the number, e.g. as with the FixedPointNumber type. To this + * end, prefer `convertFixedPointNumber` unless you are already carrying + * precision information separately. + * + * Source: + * https://github.com/tahowallet/extension/blob/main/background/lib/fixed-point.ts#L25-L44 + */ +function convertFixedPoint( + fixedPoint: bigint, + fixedPointDecimals: number, + targetDecimals: number, +): bigint { + if (fixedPointDecimals >= targetDecimals) { + return fixedPoint / 10n ** BigInt(fixedPointDecimals - targetDecimals) + } + + return fixedPoint * 10n ** BigInt(targetDecimals - fixedPointDecimals) +} + +/** + * Parses a simple floating point string in US decimal format (potentially + * using commas as thousands separators, and using a single period as a decimal + * separator) to a FixedPointNumber. The decimals in the returned + * FixedPointNumber will match the number of digits after the decimal in the + * floating point string. + * + * Source: + * https://github.com/tahowallet/extension/blob/main/background/lib/fixed-point.ts#L134-L170 + */ +function parseToFixedPointNumber( + floatingPointString: string, +): { amount: bigint; decimals: number } | undefined { + if (!floatingPointString.match(/^[^0-9]*([0-9,]+)(?:\.([0-9]*))?$/)) { + return undefined + } + + const [whole, decimals, ...extra] = floatingPointString.split(".") + + // Only one `.` supported. + if (extra.length > 0) { + return undefined + } + + const noThousandsSeparatorWhole = whole.replace(",", "") + const setDecimals = decimals ?? "" + + try { + return { + amount: BigInt([noThousandsSeparatorWhole, setDecimals].join("")), + decimals: setDecimals.length, + } + } catch (error) { + return undefined + } +} + +/** + * Convert a floating point number to bigint.`. + * It is necessary to parse floating point number to fixed point first. + * Then convert a fixed point bigint with precision `parsedAmount.decimals` to another + * fixed point bigint with precision `decimals`. + */ +export function userAmountToBigInt( + amount: string, + decimals = 18, +): bigint | undefined { + const parsedAmount = parseToFixedPointNumber(amount) + + if (typeof parsedAmount === "undefined") { + return undefined + } + + return convertFixedPoint(parsedAmount.amount, parsedAmount.decimals, decimals) +} diff --git a/dapp/tsconfig.json b/dapp/tsconfig.json index 2e31274ea..03b8b2fe1 100644 --- a/dapp/tsconfig.json +++ b/dapp/tsconfig.json @@ -11,8 +11,11 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "strict": true + "strict": true, + "baseUrl": ".", + "paths": { + "#/*": ["./src/*"] + } }, - "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/dapp/vite.config.ts b/dapp/vite.config.ts index ea1889ae7..b60415bd0 100644 --- a/dapp/vite.config.ts +++ b/dapp/vite.config.ts @@ -1,6 +1,14 @@ import { defineConfig } from "vite" import react from "@vitejs/plugin-react" +import { nodePolyfills } from "vite-plugin-node-polyfills" + +import { resolve } from "path" export default defineConfig({ - plugins: [react()], + resolve: { + alias: { + "#": resolve(__dirname, "./src"), + }, + }, + plugins: [nodePolyfills(), react()], }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 543b9e64f..d28714496 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,8 +49,8 @@ importers: specifier: ^2.4.1 version: 2.4.1(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-verify@2.0.1)(ethers@6.8.1)(hardhat@2.19.1) '@thesis-co/eslint-config': - specifier: ^0.6.1 - version: 0.6.1(eslint@8.54.0)(prettier@3.1.0)(typescript@5.3.2) + specifier: github:thesis/eslint-config#7b9bc8c + version: github.com/thesis/eslint-config/7b9bc8c(eslint@8.54.0)(prettier@3.1.0)(typescript@5.3.2) '@typechain/ethers-v6': specifier: ^0.5.1 version: 0.5.1(ethers@6.8.1)(typechain@8.3.2)(typescript@5.3.2) @@ -102,6 +102,9 @@ importers: solidity-coverage: specifier: ^0.8.5 version: 0.8.5(hardhat@2.19.1) + solidity-docgen: + specifier: 0.6.0-beta.36 + version: 0.6.0-beta.36(hardhat@2.19.1) ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@20.9.4)(typescript@5.3.2) @@ -124,11 +127,14 @@ importers: specifier: ^11.11.0 version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.38)(react@18.2.0) '@ledgerhq/wallet-api-client': - specifier: ^1.2.1 - version: 1.2.1 + specifier: ^1.5.0 + version: 1.5.0 '@ledgerhq/wallet-api-client-react': - specifier: ^1.1.2 - version: 1.1.2(react@18.2.0) + specifier: ^1.3.0 + version: 1.3.0(react@18.2.0) + formik: + specifier: ^2.4.5 + version: 2.4.5(react@18.2.0) framer-motion: specifier: ^10.16.5 version: 10.16.5(react-dom@18.2.0)(react@18.2.0) @@ -138,10 +144,13 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-number-format: + specifier: ^5.3.1 + version: 5.3.1(react-dom@18.2.0)(react@18.2.0) devDependencies: '@thesis-co/eslint-config': - specifier: ^0.6.1 - version: 0.6.1(eslint@8.54.0)(prettier@3.1.0)(typescript@5.3.2) + specifier: github:thesis/eslint-config#7b9bc8c + version: github.com/thesis/eslint-config/7b9bc8c(eslint@8.54.0)(prettier@3.1.0)(typescript@5.3.2) '@types/react': specifier: ^18.2.38 version: 18.2.38 @@ -160,6 +169,12 @@ importers: eslint: specifier: ^8.54.0 version: 8.54.0 + eslint-import-resolver-alias: + specifier: ^1.1.2 + version: 1.1.2(eslint-plugin-import@2.29.1) + eslint-plugin-import: + specifier: ^2.29.1 + version: 2.29.1(@typescript-eslint/parser@6.12.0)(eslint@8.54.0) prettier: specifier: ^3.1.0 version: 3.1.0 @@ -169,6 +184,9 @@ importers: vite: specifier: ^5.0.2 version: 5.0.2 + vite-plugin-node-polyfills: + specifier: ^0.19.0 + version: 0.19.0(vite@5.0.2) sdk: dependencies: @@ -186,8 +204,8 @@ importers: specifier: ^7.23.7 version: 7.23.7(@babel/core@7.23.3) '@thesis-co/eslint-config': - specifier: ^0.6.1 - version: 0.6.1(eslint@8.54.0)(prettier@3.1.0)(typescript@5.3.2) + specifier: github:thesis/eslint-config#7b9bc8c + version: github.com/thesis/eslint-config/7b9bc8c(eslint@8.54.0)(prettier@3.1.0)(typescript@5.3.2) '@types/jest': specifier: ^29.5.11 version: 29.5.11 @@ -235,8 +253,8 @@ importers: version: 6.1.0(react@18.2.0) devDependencies: '@thesis-co/eslint-config': - specifier: ^0.6.1 - version: 0.6.1(eslint@8.54.0)(prettier@3.1.0)(typescript@5.3.2) + specifier: github:thesis/eslint-config#7b9bc8c + version: github.com/thesis/eslint-config/7b9bc8c(eslint@8.54.0)(prettier@3.1.0)(typescript@5.3.2) '@types/node': specifier: ^20.9.4 version: 20.9.4 @@ -4371,7 +4389,7 @@ packages: '@bitcoinerlab/secp256k1': 1.0.5 '@keep-network/ecdsa': 2.0.0(@keep-network/keep-core@1.8.1-dev.0) '@keep-network/tbtc-v2': 1.5.1(@keep-network/keep-core@1.8.1-dev.0) - '@ledgerhq/wallet-api-client': 1.2.1 + '@ledgerhq/wallet-api-client': 1.5.0 bignumber.js: 9.1.2 bitcoinjs-lib: 6.1.5 bufio: 1.2.1 @@ -4490,25 +4508,25 @@ packages: resolution: {integrity: sha512-HHK9y4GGe4X7CXbRUCh7z8Mp+WggpJn1dmUjmuk1rNugESF6o8nAOnXA+BxwtRRNV3CgNJR3Wxdos4J9qV0Zsg==} dev: false - /@ledgerhq/wallet-api-client-react@1.1.2(react@18.2.0): - resolution: {integrity: sha512-ZBnp8HBHwtuDE/jqYuJmqx20Dx9dqqcZaOW4YuaY32GRwqEJJslTtcypCCgq2kArl0Y0q0irOYEd/0I7ULxdLQ==} + /@ledgerhq/wallet-api-client-react@1.3.0(react@18.2.0): + resolution: {integrity: sha512-UYNKQ1Yp/ZieqY4SGKgkoxKXJ3t0Zj/PPnZDoOrG/YbAFd4r3bL4XvTMa5T+bIdjbqITTo7VRiA9mhk5ootLrA==} peerDependencies: react: ^16.8.0 || ^17 || ^18 dependencies: - '@ledgerhq/wallet-api-client': 1.2.1 + '@ledgerhq/wallet-api-client': 1.5.0 react: 18.2.0 dev: false - /@ledgerhq/wallet-api-client@1.2.1: - resolution: {integrity: sha512-uTBTZCpbLTM5y5Cd7ioQB0lcq0b3cbrU2bGzCiKuY1IEd0NUyFhr2dKliRrcLoMPDRtQRmRnSxeX0BFKinoo8Q==} + /@ledgerhq/wallet-api-client@1.5.0: + resolution: {integrity: sha512-I07RlTmHw0uia5xhHa8Z3I7yVlYkxUWPfKWruh0vM6o0hDzPddGp+oDJZlriJIIrj2eHfaUCO1bEgycewPe0jA==} dependencies: '@ledgerhq/hw-transport': 6.29.0 - '@ledgerhq/wallet-api-core': 1.3.1 + '@ledgerhq/wallet-api-core': 1.6.0 bignumber.js: 9.1.2 dev: false - /@ledgerhq/wallet-api-core@1.3.1: - resolution: {integrity: sha512-yOeb1tfdwF6NdxVEIVr8SVz5iOyh6asWa0bbuCyMpiLrfuVS/Wkr6OeDMBYSxWxXxRFmQDJ9XQxdtSS+MGNk1Q==} + /@ledgerhq/wallet-api-core@1.6.0: + resolution: {integrity: sha512-nVPN3yu5+5pbhfFYh0iKmij5U7KVKZfpDkusXbj4yKvueKY8ZXU1wgvw1rDhgxlqu+s7JTA50MX56doyhm46Qg==} dependencies: '@ledgerhq/errors': 6.15.0 bignumber.js: 9.1.2 @@ -5789,6 +5807,34 @@ packages: - supports-color dev: false + /@rollup/plugin-inject@5.0.5: + resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0 + estree-walker: 2.0.2 + magic-string: 0.30.5 + dev: true + + /@rollup/pluginutils@5.1.0: + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + /@rollup/rollup-android-arm-eabi@4.5.1: resolution: {integrity: sha512-YaN43wTyEBaMqLDYeze+gQ4ZrW5RbTEGtT5o1GVDkhpdNcsLTnLRcLccvwy3E9wiDKWg9RIhuoy3JQKDRBfaZA==} cpu: [arm] @@ -6145,35 +6191,6 @@ packages: dependencies: defer-to-connect: 2.0.1 - /@thesis-co/eslint-config@0.6.1(eslint@8.54.0)(prettier@3.1.0)(typescript@5.3.2): - resolution: {integrity: sha512-0vJCCI4UwUdniDCQeTFlMBT+bSp5pGkrtHrZrG2vmyLZwSVdJNtInjkBc/Jd0sGfMtPo3pqQRwA40Zo80lPi+Q==} - engines: {node: '>=14.0.0'} - peerDependencies: - eslint: '>=6.8.0' - dependencies: - '@thesis-co/prettier-config': github.com/thesis/prettier-config/daeaac564056a7885e4366ce12bfde6fd823fc90(prettier@3.1.0) - '@typescript-eslint/eslint-plugin': 6.12.0(@typescript-eslint/parser@6.12.0)(eslint@8.54.0)(typescript@5.3.2) - '@typescript-eslint/parser': 6.12.0(eslint@8.54.0)(typescript@5.3.2) - eslint: 8.54.0 - eslint-config-airbnb: 19.0.4(eslint-plugin-import@2.29.0)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2)(eslint@8.54.0) - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.0)(eslint@8.54.0) - eslint-config-airbnb-typescript: 17.1.0(@typescript-eslint/eslint-plugin@6.12.0)(@typescript-eslint/parser@6.12.0)(eslint-plugin-import@2.29.0)(eslint@8.54.0) - eslint-config-prettier: 9.0.0(eslint@8.54.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.12.0)(eslint@8.54.0) - eslint-plugin-jsx-a11y: 6.8.0(eslint@8.54.0) - eslint-plugin-no-only-tests: 3.1.0 - eslint-plugin-prettier: 5.0.1(eslint-config-prettier@9.0.0)(eslint@8.54.0)(prettier@3.1.0) - eslint-plugin-react: 7.33.2(eslint@8.54.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.54.0) - transitivePeerDependencies: - - '@types/eslint' - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - prettier - - supports-color - - typescript - dev: true - /@threshold-network/solidity-contracts@1.2.1(@keep-network/keep-core@1.8.1-dev.0): resolution: {integrity: sha512-v19gnQzdU52DLB6ZpkCW4YHTvApmwA1EG/YRxh+FRrzeDOC6rv+EqUhLgKpMtSpgBZT1ryqqLXm1QmjkW6CIGw==} requiresBuild: true @@ -6397,6 +6414,13 @@ packages: '@types/node': 20.9.4 dev: true + /@types/hoist-non-react-statics@3.3.5: + resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} + dependencies: + '@types/react': 18.2.38 + hoist-non-react-statics: 3.3.2 + dev: false + /@types/http-cache-semantics@4.0.4: resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -7386,7 +7410,6 @@ packages: inherits: 2.0.4 minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 - dev: false /asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} @@ -7399,6 +7422,16 @@ packages: engines: {node: '>=0.8'} dev: false + /assert@2.1.0: + resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} + dependencies: + call-bind: 1.0.5 + is-nan: 1.3.2 + object-is: 1.1.5 + object.assign: 4.1.4 + util: 0.12.5 + dev: true + /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} @@ -8006,6 +8039,12 @@ packages: run-parallel-limit: 1.1.0 dev: true + /browser-resolve@2.0.0: + resolution: {integrity: sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==} + dependencies: + resolve: 1.22.8 + dev: true + /browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} @@ -8025,7 +8064,6 @@ packages: browserify-aes: 1.2.0 browserify-des: 1.0.2 evp_bytestokey: 1.0.3 - dev: false /browserify-des@1.0.2: resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} @@ -8034,14 +8072,12 @@ packages: des.js: 1.1.0 inherits: 2.0.4 safe-buffer: 5.2.1 - dev: false /browserify-rsa@4.1.0: resolution: {integrity: sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==} dependencies: bn.js: 5.2.1 randombytes: 2.1.0 - dev: false /browserify-sign@4.2.2: resolution: {integrity: sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==} @@ -8056,7 +8092,12 @@ packages: parse-asn1: 5.1.6 readable-stream: 3.6.2 safe-buffer: 5.2.1 - dev: false + + /browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + dependencies: + pako: 1.0.11 + dev: true /browserslist@4.22.1: resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} @@ -8182,6 +8223,10 @@ packages: engines: {node: '>=14.0.0'} dev: false + /builtin-status-codes@3.0.0: + resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} + dev: true + /builtins@5.0.1: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} dependencies: @@ -8774,6 +8819,10 @@ packages: /confusing-browser-globals@1.0.11: resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + /console-browserify@1.2.0: + resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} + dev: true + /constant-case@3.0.4: resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} dependencies: @@ -8781,6 +8830,10 @@ packages: tslib: 2.6.2 upper-case: 2.0.2 + /constants-browserify@1.0.0: + resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} + dev: true + /content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -8903,7 +8956,6 @@ packages: dependencies: bn.js: 4.12.0 elliptic: 6.5.4 - dev: false /create-gatsby@3.12.3: resolution: {integrity: sha512-N0K/Z/MD5LMRJcBy669WpSgrn+31zBV72Lv0RHolX0fXa77Yx58HsEiLWz83j/dtciGMQfEOEHFRetUqZhOggA==} @@ -9003,7 +9055,6 @@ packages: public-encrypt: 4.0.3 randombytes: 2.1.0 randomfill: 1.0.4 - dev: false /crypto-js@3.3.0: resolution: {integrity: sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==} @@ -9347,6 +9398,11 @@ packages: /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + /deepmerge@2.2.1: + resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==} + engines: {node: '>=0.10.0'} + dev: false + /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -9433,7 +9489,6 @@ packages: dependencies: inherits: 2.0.4 minimalistic-assert: 1.0.1 - dev: false /destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} @@ -9531,7 +9586,6 @@ packages: bn.js: 4.12.0 miller-rabin: 4.0.1 randombytes: 2.1.0 - dev: false /difflib@0.2.4: resolution: {integrity: sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==} @@ -9573,6 +9627,11 @@ packages: resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} dev: false + /domain-browser@4.23.0: + resolution: {integrity: sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA==} + engines: {node: '>=10'} + dev: true + /domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} @@ -10017,7 +10076,7 @@ packages: eslint: 8.54.0 dev: true - /eslint-config-react-app@6.0.0(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(babel-eslint@10.1.0)(eslint-plugin-flowtype@5.10.0)(eslint-plugin-import@2.29.0)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2)(eslint@7.32.0)(typescript@5.3.2): + /eslint-config-react-app@6.0.0(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(babel-eslint@10.1.0)(eslint-plugin-flowtype@5.10.0)(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2)(eslint@7.32.0)(typescript@5.3.2): resolution: {integrity: sha512-bpoAAC+YRfzq0dsTk+6v9aHm/uqnDwayNAXleMypGl6CpxI9oXXscVHo4fk3eJPIn+rsbtNetB4r/ZIidFIE8A==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: @@ -10047,12 +10106,21 @@ packages: confusing-browser-globals: 1.0.11 eslint: 7.32.0 eslint-plugin-flowtype: 5.10.0(eslint@8.54.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.12.0)(eslint@8.54.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.12.0)(eslint@8.54.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.54.0) eslint-plugin-react: 7.33.2(eslint@8.54.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.54.0) typescript: 5.3.2 + /eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1): + resolution: {integrity: sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==} + engines: {node: '>= 4'} + peerDependencies: + eslint-plugin-import: '>=1.4.0' + dependencies: + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.12.0)(eslint@8.54.0) + dev: true + /eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} dependencies: @@ -10133,6 +10201,41 @@ packages: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color + dev: true + + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.12.0)(eslint@8.54.0): + resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 6.12.0(eslint@8.54.0)(typescript@5.3.2) + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.54.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.12.0)(eslint-import-resolver-node@0.3.9)(eslint@8.54.0) + hasown: 2.0.0 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color /eslint-plugin-jsx-a11y@6.8.0(eslint@8.54.0): resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==} @@ -10410,6 +10513,10 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -11167,6 +11274,22 @@ packages: combined-stream: 1.0.8 mime-types: 2.1.35 + /formik@2.4.5(react@18.2.0): + resolution: {integrity: sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ==} + peerDependencies: + react: '>=16.8.0' + dependencies: + '@types/hoist-non-react-statics': 3.3.5 + deepmerge: 2.2.1 + hoist-non-react-statics: 3.3.2 + lodash: 4.17.21 + lodash-es: 4.17.21 + react: 18.2.0 + react-fast-compare: 2.0.4 + tiny-warning: 1.0.3 + tslib: 2.6.2 + dev: false + /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -11689,9 +11812,9 @@ packages: enhanced-resolve: 5.15.0 error-stack-parser: 2.1.4 eslint: 7.32.0 - eslint-config-react-app: 6.0.0(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(babel-eslint@10.1.0)(eslint-plugin-flowtype@5.10.0)(eslint-plugin-import@2.29.0)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2)(eslint@7.32.0)(typescript@5.3.2) + eslint-config-react-app: 6.0.0(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(babel-eslint@10.1.0)(eslint-plugin-flowtype@5.10.0)(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2)(eslint@7.32.0)(typescript@5.3.2) eslint-plugin-flowtype: 5.10.0(eslint@8.54.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.12.0)(eslint@8.54.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.12.0)(eslint@8.54.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.54.0) eslint-plugin-react: 7.33.2(eslint@8.54.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.54.0) @@ -12545,6 +12668,10 @@ packages: quick-lru: 5.1.1 resolve-alpn: 1.2.1 + /https-browserify@1.0.0: + resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} + dev: true + /https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -12721,6 +12848,14 @@ packages: is-relative: 1.0.0 is-windows: 1.0.2 + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: true + /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: @@ -12874,6 +13009,14 @@ packages: /is-map@2.0.2: resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + /is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + dev: true + /is-natural-number@4.0.1: resolution: {integrity: sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==} dev: false @@ -13062,6 +13205,11 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + /isomorphic-timers-promises@1.0.1: + resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==} + engines: {node: '>=10'} + dev: true + /isomorphic-unfetch@3.1.0: resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==} dependencies: @@ -13921,6 +14069,10 @@ packages: /lock@1.1.0: resolution: {integrity: sha512-NZQIJJL5Rb9lMJ0Yl1JoVr9GSdo4HTPsUEWsSFzB8dE8DSoiLCVavWZPi7Rnlv/o73u6I24S/XYc/NmG4l8EKA==} + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + /lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} dev: true @@ -14056,6 +14208,13 @@ packages: resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} dev: true + /magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /make-dir@1.3.0: resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} engines: {node: '>=4'} @@ -14195,7 +14354,6 @@ packages: dependencies: bn.js: 4.12.0 brorand: 1.1.0 - dev: false /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} @@ -14613,6 +14771,39 @@ packages: /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + /node-stdlib-browser@1.2.0: + resolution: {integrity: sha512-VSjFxUhRhkyed8AtLwSCkMrJRfQ3e2lGtG3sP6FEgaLKBBbxM/dLfjRe1+iLhjvyLFW3tBQ8+c0pcOtXGbAZJg==} + engines: {node: '>=10'} + dependencies: + assert: 2.1.0 + browser-resolve: 2.0.0 + browserify-zlib: 0.2.0 + buffer: 5.7.1 + console-browserify: 1.2.0 + constants-browserify: 1.0.0 + create-require: 1.1.1 + crypto-browserify: 3.12.0 + domain-browser: 4.23.0 + events: 3.3.0 + https-browserify: 1.0.0 + isomorphic-timers-promises: 1.0.1 + os-browserify: 0.3.0 + path-browserify: 1.0.1 + pkg-dir: 5.0.0 + process: 0.11.10 + punycode: 1.4.1 + querystring-es3: 0.2.1 + readable-stream: 3.6.2 + stream-browserify: 3.0.0 + stream-http: 3.2.0 + string_decoder: 1.3.0 + timers-browserify: 2.0.12 + tty-browserify: 0.0.1 + url: 0.11.3 + util: 0.12.5 + vm-browserify: 1.1.2 + dev: true + /nofilter@1.0.4: resolution: {integrity: sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==} engines: {node: '>=8'} @@ -14726,6 +14917,14 @@ packages: /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + dev: true + /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -14918,6 +15117,10 @@ packages: resolution: {integrity: sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ==} dev: true + /os-browserify@0.3.0: + resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} + dev: true + /os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} @@ -15030,6 +15233,10 @@ packages: registry-url: 6.0.1 semver: 7.5.4 + /pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: true + /param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: @@ -15050,7 +15257,6 @@ packages: evp_bytestokey: 1.0.3 pbkdf2: 3.1.2 safe-buffer: 5.2.1 - dev: false /parse-cache-control@1.0.1: resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==} @@ -15103,6 +15309,10 @@ packages: ansi-escapes: 4.3.2 cross-spawn: 7.0.3 + /path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + dev: true + /path-case@3.0.4: resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} dependencies: @@ -15227,6 +15437,13 @@ packages: dependencies: find-up: 4.1.0 + /pkg-dir@5.0.0: + resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} + engines: {node: '>=10'} + dependencies: + find-up: 5.0.0 + dev: true + /pkg-up@3.1.0: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} @@ -15666,7 +15883,6 @@ packages: /process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - dev: false /progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} @@ -15737,7 +15953,6 @@ packages: parse-asn1: 5.1.6 randombytes: 2.1.0 safe-buffer: 5.2.1 - dev: false /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} @@ -15745,6 +15960,10 @@ packages: end-of-stream: 1.4.4 once: 1.4.0 + /punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + dev: true + /punycode@2.1.0: resolution: {integrity: sha512-Yxz2kRwT90aPiWEMHVYnEf4+rhwF1tBmmZ4KepCP+Wkium9JxtWnUm1nqGwpiAHr/tnTSeHqr3wb++jgSkXjhA==} engines: {node: '>=6'} @@ -15794,6 +16013,11 @@ packages: split-on-first: 1.1.0 strict-uri-encode: 2.0.0 + /querystring-es3@0.2.1: + resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==} + engines: {node: '>=0.4.x'} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -15815,7 +16039,6 @@ packages: dependencies: randombytes: 2.1.0 safe-buffer: 5.2.1 - dev: false /range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} @@ -15920,6 +16143,10 @@ packages: /react-error-overlay@6.0.11: resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==} + /react-fast-compare@2.0.4: + resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} + dev: false + /react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false @@ -15962,6 +16189,17 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true + /react-number-format@5.3.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-qpYcQLauIeEhCZUZY9jXZnnroOtdy3jYaS1zQ3M1Sr6r/KMOBEIGNIb7eKT19g2N1wbYgFgvDzs19hw5TrB8XQ==} + peerDependencies: + react: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.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'} @@ -16922,6 +17160,16 @@ packages: - supports-color dev: true + /solidity-docgen@0.6.0-beta.36(hardhat@2.19.1): + resolution: {integrity: sha512-f/I5G2iJgU1h0XrrjRD0hHMr7C10u276vYvm//rw1TzFcYQ4xTOyAoi9oNAHRU0JU4mY9eTuxdVc2zahdMuhaQ==} + peerDependencies: + hardhat: ^2.8.0 + dependencies: + handlebars: 4.7.8 + hardhat: 2.19.1(ts-node@10.9.1)(typescript@5.3.2) + solidity-ast: 0.4.53 + dev: true + /source-list-map@2.0.1: resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} @@ -17028,6 +17276,22 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + /stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /stream-http@3.2.0: + resolution: {integrity: sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==} + dependencies: + builtin-status-codes: 3.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + xtend: 4.0.2 + dev: true + /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -17535,6 +17799,13 @@ packages: engines: {node: '>=0.10.0'} dev: false + /timers-browserify@2.0.12: + resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} + engines: {node: '>=0.6.0'} + dependencies: + setimmediate: 1.0.5 + dev: true + /timers-ext@0.1.7: resolution: {integrity: sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==} dependencies: @@ -17557,6 +17828,10 @@ packages: nan: 2.18.0 dev: false + /tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + dev: false + /title-case@3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} dependencies: @@ -17761,6 +18036,15 @@ packages: json5: 1.0.2 minimist: 1.2.8 strip-bom: 3.0.0 + dev: true + + /tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -17786,6 +18070,10 @@ packages: tslib: 1.14.1 typescript: 5.3.2 + /tty-browserify@0.0.1: + resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} + dev: true + /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} requiresBuild: true @@ -18120,6 +18408,13 @@ packages: engines: {node: '>= 4'} dev: false + /url@0.11.3: + resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==} + dependencies: + punycode: 1.4.1 + qs: 6.11.2 + dev: true + /use-callback-ref@1.3.0(@types/react@18.2.38)(react@18.2.0): resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==} engines: {node: '>=10'} @@ -18165,6 +18460,16 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.12 + which-typed-array: 1.1.13 + dev: true + /utila@0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} @@ -18248,6 +18553,18 @@ packages: extsprintf: 1.3.0 dev: false + /vite-plugin-node-polyfills@0.19.0(vite@5.0.2): + resolution: {integrity: sha512-AhdVxAmVnd1doUlIRGUGV6ZRPfB9BvIwDF10oCOmL742IsvsFIAV4tSMxSfu5e0Px0QeJLgWVOSbtHIvblzqMw==} + peerDependencies: + vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + dependencies: + '@rollup/plugin-inject': 5.0.5 + node-stdlib-browser: 1.2.0 + vite: 5.0.2 + transitivePeerDependencies: + - rollup + dev: true + /vite@5.0.2: resolution: {integrity: sha512-6CCq1CAJCNM1ya2ZZA7+jS2KgnhbzvxakmlIjN24cF/PXhRMzpM/z8QgsVJA/Dm5fWUWnVEsmtBoMhmerPxT0g==} engines: {node: ^18.0.0 || >=20.0.0} @@ -18283,6 +18600,10 @@ packages: fsevents: 2.3.3 dev: true + /vm-browserify@1.1.2: + resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + dev: true + /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: @@ -19431,6 +19752,38 @@ packages: - supports-color dev: false + github.com/thesis/eslint-config/7b9bc8c(eslint@8.54.0)(prettier@3.1.0)(typescript@5.3.2): + resolution: {tarball: https://codeload.github.com/thesis/eslint-config/tar.gz/7b9bc8c} + id: github.com/thesis/eslint-config/7b9bc8c + name: '@thesis-co/eslint-config' + version: 0.8.0-pre + engines: {node: '>=14.0.0'} + peerDependencies: + eslint: '>=6.8.0' + dependencies: + '@thesis-co/prettier-config': github.com/thesis/prettier-config/daeaac564056a7885e4366ce12bfde6fd823fc90(prettier@3.1.0) + '@typescript-eslint/eslint-plugin': 6.12.0(@typescript-eslint/parser@6.12.0)(eslint@8.54.0)(typescript@5.3.2) + '@typescript-eslint/parser': 6.12.0(eslint@8.54.0)(typescript@5.3.2) + eslint: 8.54.0 + eslint-config-airbnb: 19.0.4(eslint-plugin-import@2.29.0)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2)(eslint@8.54.0) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.0)(eslint@8.54.0) + eslint-config-airbnb-typescript: 17.1.0(@typescript-eslint/eslint-plugin@6.12.0)(@typescript-eslint/parser@6.12.0)(eslint-plugin-import@2.29.0)(eslint@8.54.0) + eslint-config-prettier: 9.0.0(eslint@8.54.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.12.0)(eslint@8.54.0) + eslint-plugin-jsx-a11y: 6.8.0(eslint@8.54.0) + eslint-plugin-no-only-tests: 3.1.0 + eslint-plugin-prettier: 5.0.1(eslint-config-prettier@9.0.0)(eslint@8.54.0)(prettier@3.1.0) + eslint-plugin-react: 7.33.2(eslint@8.54.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.54.0) + transitivePeerDependencies: + - '@types/eslint' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - prettier + - supports-color + - typescript + dev: true + github.com/thesis/prettier-config/daeaac564056a7885e4366ce12bfde6fd823fc90(prettier@3.1.0): resolution: {tarball: https://codeload.github.com/thesis/prettier-config/tar.gz/daeaac564056a7885e4366ce12bfde6fd823fc90} id: github.com/thesis/prettier-config/daeaac564056a7885e4366ce12bfde6fd823fc90 diff --git a/sdk/package.json b/sdk/package.json index 11c055bba..0aa7e26fd 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@babel/preset-env": "^7.23.7", - "@thesis-co/eslint-config": "^0.6.1", + "@thesis-co/eslint-config": "github:thesis/eslint-config#7b9bc8c", "@types/jest": "^29.5.11", "@types/node": "^20.9.4", "eslint": "^8.54.0", diff --git a/website/package.json b/website/package.json index 23f6fdb53..3d0dcc6fd 100644 --- a/website/package.json +++ b/website/package.json @@ -28,7 +28,7 @@ "react-helmet": "^6.1.0" }, "devDependencies": { - "@thesis-co/eslint-config": "^0.6.1", + "@thesis-co/eslint-config": "github:thesis/eslint-config#7b9bc8c", "@types/node": "^20.9.4", "@types/react": "^18.2.38", "@types/react-dom": "^18.2.17",