diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index ec9afb3e3..0e95d8530 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,3 +1,6 @@ # Auto-fix linting d2a058fe6cfbab6f82d0d977d1b2d8bd9f494df1 0976ac1b09b7257fe74389bafe94697059a07aed + +# Move core/ to solidity/ +9b45a73a99b879e6fd362553f60de79405abad98 diff --git a/.github/workflows/reusable-sdk-build.yaml b/.github/workflows/reusable-sdk-build.yaml index fe0a3d5ab..9c38cee5b 100644 --- a/.github/workflows/reusable-sdk-build.yaml +++ b/.github/workflows/reusable-sdk-build.yaml @@ -3,14 +3,14 @@ on: workflow_call: jobs: - core-build: - uses: ./.github/workflows/reusable-core-build.yaml + solidity-build: + uses: ./.github/workflows/reusable-solidity-build.yaml sdk-build: defaults: run: working-directory: ./sdk - needs: [core-build] + needs: [solidity-build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -24,11 +24,11 @@ jobs: node-version-file: "sdk/.nvmrc" cache: "pnpm" - - name: Download Core Build Artifacts + - name: Download Solidity Build Artifacts uses: actions/download-artifact@v4 with: - name: core-build - path: core/ + name: solidity-build + path: solidity/ - name: Install Dependencies run: pnpm install --prefer-offline --frozen-lockfile diff --git a/.github/workflows/reusable-core-build.yaml b/.github/workflows/reusable-solidity-build.yaml similarity index 69% rename from .github/workflows/reusable-core-build.yaml rename to .github/workflows/reusable-solidity-build.yaml index 18309d41d..08d4b5327 100644 --- a/.github/workflows/reusable-core-build.yaml +++ b/.github/workflows/reusable-solidity-build.yaml @@ -1,13 +1,13 @@ -name: Build the core package +name: Build the Solidity package on: workflow_call: jobs: - build-core: + build-solidity: runs-on: ubuntu-latest defaults: run: - working-directory: ./core + working-directory: ./solidity steps: - uses: actions/checkout@v4 @@ -17,7 +17,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version-file: "core/.nvmrc" + node-version-file: "solidity/.nvmrc" cache: "pnpm" - name: Install Dependencies @@ -29,9 +29,9 @@ jobs: - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: - name: core-build + name: solidity-build path: | - core/build/ - core/cache/ - core/typechain/ + solidity/build/ + solidity/cache/ + solidity/typechain/ if-no-files-found: error diff --git a/.github/workflows/sdk.yaml b/.github/workflows/sdk.yaml index 5898da5ee..f83606fdd 100644 --- a/.github/workflows/sdk.yaml +++ b/.github/workflows/sdk.yaml @@ -31,11 +31,11 @@ jobs: node-version-file: "sdk/.nvmrc" cache: "pnpm" - - name: Download Core Build Artifacts + - name: Download Solidity Build Artifacts uses: actions/download-artifact@v4 with: - name: core-build - path: core/ + name: solidity-build + path: solidity/ - name: Install Dependencies run: pnpm install --prefer-offline --frozen-lockfile @@ -58,11 +58,11 @@ jobs: node-version-file: "sdk/.nvmrc" cache: "pnpm" - - name: Download Core Build Artifacts + - name: Download Solidity Build Artifacts uses: actions/download-artifact@v4 with: - name: core-build - path: core/ + name: solidity-build + path: solidity/ - name: Install Dependencies run: pnpm install --prefer-offline --frozen-lockfile diff --git a/.github/workflows/core.yaml b/.github/workflows/solidity.yaml similarity index 79% rename from .github/workflows/core.yaml rename to .github/workflows/solidity.yaml index 8dbe76ae6..7586edb1a 100644 --- a/.github/workflows/core.yaml +++ b/.github/workflows/solidity.yaml @@ -1,11 +1,11 @@ -name: Core +name: Solidity on: push: branches: - main paths: - - "core/**" + - "solidity/**" pull_request: workflow_dispatch: inputs: @@ -19,14 +19,14 @@ on: defaults: run: - working-directory: ./core + working-directory: ./solidity jobs: - core-build: - uses: ./.github/workflows/reusable-core-build.yaml + solidity-build: + uses: ./.github/workflows/reusable-solidity-build.yaml - core-format: - needs: [core-build] + solidity-format: + needs: [solidity-build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -37,7 +37,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version-file: "core/.nvmrc" + node-version-file: "solidity/.nvmrc" cache: "pnpm" - name: Install Dependencies @@ -46,14 +46,14 @@ jobs: - name: Download Build Artifacts uses: actions/download-artifact@v4 with: - name: core-build - path: core/ + name: solidity-build + path: solidity/ - name: Format run: pnpm run format - core-slither: - needs: [core-build] + solidity-slither: + needs: [solidity-build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -64,7 +64,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version-file: "core/.nvmrc" + node-version-file: "solidity/.nvmrc" cache: "pnpm" - name: Install Dependencies @@ -82,14 +82,14 @@ jobs: - name: Download Build Artifacts uses: actions/download-artifact@v4 with: - name: core-build - path: core/ + name: solidity-build + path: solidity/ - name: Run Slither run: slither --hardhat-ignore-compile . - core-test: - needs: [core-build] + solidity-test: + needs: [solidity-build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -100,7 +100,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version-file: "core/.nvmrc" + node-version-file: "solidity/.nvmrc" cache: "pnpm" - name: Install Dependencies @@ -109,14 +109,14 @@ jobs: - name: Download Build Artifacts uses: actions/download-artifact@v4 with: - name: core-build - path: core/ + name: solidity-build + path: solidity/ - name: Test run: pnpm run test --no-compile - core-deploy-dry-run: - needs: [core-build] + solidity-deploy-dry-run: + needs: [solidity-build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -127,7 +127,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version-file: "core/.nvmrc" + node-version-file: "solidity/.nvmrc" cache: "pnpm" - name: Install Dependencies @@ -136,14 +136,14 @@ jobs: - name: Download Build Artifacts uses: actions/download-artifact@v4 with: - name: core-build - path: core/ + name: solidity-build + path: solidity/ - name: Deploy run: pnpm run deploy --no-compile - core-deploy-testnet: - needs: [core-deploy-dry-run] + solidity-deploy-testnet: + needs: [solidity-deploy-dry-run] if: github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: @@ -155,7 +155,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version-file: "core/.nvmrc" + node-version-file: "solidity/.nvmrc" cache: "pnpm" - name: Install dependencies @@ -164,8 +164,8 @@ jobs: - name: Download build artifacts uses: actions/download-artifact@v4 with: - name: core-build - path: core/ + name: solidity-build + path: solidity/ - name: Remove existing deployment artifacts for the selected network run: rm -rf deployments/${{ github.event.inputs.environment }} @@ -186,5 +186,5 @@ jobs: with: name: deployed-contracts-${{ github.event.inputs.environment }} path: | - core/deployments/${{ github.event.inputs.environment }} + solidity/deployments/${{ github.event.inputs.environment }} if-no-files-found: error diff --git a/.github/workflows/subgraph.yaml b/.github/workflows/subgraph.yaml index 60586fad4..0460c7a30 100644 --- a/.github/workflows/subgraph.yaml +++ b/.github/workflows/subgraph.yaml @@ -121,4 +121,3 @@ jobs: - name: Tests run: pnpm run test - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fe78facce..f068599f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,28 +5,28 @@ repos: - id: root-lint name: "lint root" entry: /usr/bin/env bash -c "npm run format" - exclude: (^core/|^dapp/|^website/|^subgraph/) + exclude: (^solidity/|^dapp/|^website/|^subgraph/) language: script description: "Checks code according to the package's linter configuration" - # Core - - id: core-lint-sol - name: "lint core sol" - entry: /usr/bin/env bash -c "npm --prefix ./core/ run lint:sol" - files: ^core/ + # Solidity + - id: solidity-lint-sol + name: "lint solidity sol" + entry: /usr/bin/env bash -c "npm --prefix ./solidity/ run lint:sol" + files: ^solidity/ types: [solidity] language: script description: "Checks solidity code according to the package's linter configuration" - - id: core-lint-js - name: "lint core ts/js" - entry: /usr/bin/env bash -c "npm --prefix ./core/ run lint:js" - files: ^core/ + - id: solidity-lint-js + name: "lint solidity ts/js" + entry: /usr/bin/env bash -c "npm --prefix ./solidity/ run lint:js" + files: ^solidity/ types_or: [ts, javascript] language: script description: "Checks TS/JS code according to the package's linter configuration" - - id: core-lint-config - name: "lint core json/yaml" - entry: /usr/bin/env bash -c "npm --prefix ./core/ run lint:config" - files: ^core/ + - id: solidity-lint-config + name: "lint solidity json/yaml" + entry: /usr/bin/env bash -c "npm --prefix ./solidity/ run lint:config" + files: ^solidity/ types_or: [json, yaml] language: script description: "Checks JSON/YAML code according to the package's linter configuration" @@ -90,4 +90,3 @@ repos: types_or: [json, yaml] language: script description: "Checks JSON/YAML code according to the package's linter configuration" - diff --git a/.prettierignore b/.prettierignore index 4511cab70..931aae2cc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,5 @@ # Packages that have own prettier configuration. -core/ +solidity/ dapp/ website/ sdk/ diff --git a/README.md b/README.md index b159a1fd5..3dbae51c2 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ Bitcoin Liquid Staking -[![Core](https://github.com/thesis/acre/actions/workflows/core.yaml/badge.svg?branch=main&event=push)](https://github.com/thesis/acre/actions/workflows/core.yaml) - ## Development ### pnpm @@ -51,7 +49,7 @@ commands: pre-commit run --all-files # Execute hooks for specific files (e.g. stBTC.sol): -pre-commit run --files ./core/contracts/stBTC.sol +pre-commit run --files ./solidity/contracts/stBTC.sol ``` ### Syncpack diff --git a/core/.solhintignore b/core/.solhintignore deleted file mode 100644 index c2658d7d1..000000000 --- a/core/.solhintignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ diff --git a/core/README.md b/core/README.md deleted file mode 100644 index b90952ee8..000000000 --- a/core/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Acre - -Acre is a “liquid staking” solution that allows people to earn yield on their Bitcoin via yield farming on Ethereum. diff --git a/core/contracts/Dispatcher.sol b/core/contracts/Dispatcher.sol deleted file mode 100644 index ec1696239..000000000 --- a/core/contracts/Dispatcher.sol +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -import "@openzeppelin/contracts/access/Ownable2Step.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import "./Router.sol"; -import "./stBTC.sol"; - -/// @title Dispatcher -/// @notice Dispatcher is a contract that routes tBTC from stBTC to -/// yield vaults and back. Vaults supply yield strategies with tBTC that -/// generate yield for Bitcoin holders. -contract Dispatcher is Router, Ownable2Step { - using SafeERC20 for IERC20; - - /// Struct holds information about a vault. - struct VaultInfo { - bool authorized; - } - - /// The main stBTC contract holding tBTC deposited by stakers. - stBTC public immutable stbtc; - /// 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); - - /// Emitted when tBTC is routed to a vault. - /// @param vault Address of the vault. - /// @param amount Amount of tBTC. - /// @param sharesOut Amount of received shares. - 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(stBTC _stbtc, IERC20 _tbtc) Ownable(msg.sender) { - stbtc = _stbtc; - 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 (isVaultAuthorized(vault)) { - revert VaultAlreadyAuthorized(); - } - - vaults.push(vault); - vaultsInfo[vault].authorized = true; - - emit VaultAuthorized(vault); - } - - /// @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 (!isVaultAuthorized(vault)) { - revert VaultUnauthorized(); - } - - vaultsInfo[vault].authorized = false; - - for (uint256 i = 0; i < vaults.length; i++) { - if (vaults[i] == vault) { - vaults[i] = vaults[vaults.length - 1]; - // slither-disable-next-line costly-loop - vaults.pop(); - break; - } - } - - emit VaultDeauthorized(vault); - } - - /// @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 stBTC 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. - 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(stbtc), address(this), amount); - tbtc.forceApprove(address(vault), amount); - - uint256 sharesOut = deposit( - IERC4626(vault), - address(stbtc), - 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 deleted file mode 100644 index f726264de..000000000 --- a/core/contracts/Router.sol +++ /dev/null @@ -1,37 +0,0 @@ -// 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 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 received shares. - /// @param minSharesOut Minimum amount of shares expected to receive. - error MinSharesError( - address vault, - uint256 sharesOut, - uint256 minSharesOut - ); - - /// @notice Routes funds from stBTC 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/test/TestERC4626.sol b/core/contracts/test/TestERC4626.sol deleted file mode 100644 index acf09928e..000000000 --- a/core/contracts/test/TestERC4626.sol +++ /dev/null @@ -1,12 +0,0 @@ -// 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_testing_erc4626.ts b/core/deploy/00_resolve_testing_erc4626.ts deleted file mode 100644 index c49346b06..000000000 --- a/core/deploy/00_resolve_testing_erc4626.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { HardhatRuntimeEnvironment } from "hardhat/types" -import type { DeployFunction } from "hardhat-deploy/types" -import { waitConfirmationsNumber } from "../helpers/deployment" - -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: waitConfirmationsNumber(hre), - }) -} - -export default func - -func.tags = ["TestERC4626"] -func.dependencies = ["TBTC"] - -func.skip = async (hre: HardhatRuntimeEnvironment): Promise => - Promise.resolve( - hre.network.name === "mainnet" || hre.network.name === "sepolia", - ) diff --git a/core/deploy/02_deploy_dispatcher.ts b/core/deploy/02_deploy_dispatcher.ts deleted file mode 100644 index 4473531e9..000000000 --- a/core/deploy/02_deploy_dispatcher.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { HardhatRuntimeEnvironment } from "hardhat/types" -import type { DeployFunction } from "hardhat-deploy/types" -import { waitConfirmationsNumber } from "../helpers/deployment" - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { getNamedAccounts, deployments, helpers } = hre - const { deployer } = await getNamedAccounts() - - const tbtc = await deployments.get("TBTC") - const stbtc = await deployments.get("stBTC") - - const dispatcher = await deployments.deploy("Dispatcher", { - from: deployer, - args: [stbtc.address, tbtc.address], - log: true, - waitConfirmations: waitConfirmationsNumber(hre), - }) - - if (hre.network.tags.etherscan) { - await helpers.etherscan.verify(dispatcher) - } - - // TODO: Add Tenderly verification -} - -export default func - -func.tags = ["Dispatcher"] -func.dependencies = ["stBTC"] diff --git a/core/test/Dispatcher.test.ts b/core/test/Dispatcher.test.ts deleted file mode 100644 index 4e1e8a987..000000000 --- a/core/test/Dispatcher.test.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { ethers, helpers } 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 { - beforeAfterEachSnapshotWrapper, - beforeAfterSnapshotWrapper, - deployment, -} from "./helpers" - -import { - Dispatcher, - TestERC4626, - StBTC as stBTC, - TestERC20, -} from "../typechain" - -import { to1e18 } from "./utils" - -const { getNamedSigners, getUnnamedSigners } = helpers.signers - -async function fixture() { - const { tbtc, stbtc, dispatcher, vault } = await deployment() - const { governance, maintainer } = await getNamedSigners() - const [thirdParty] = await getUnnamedSigners() - - return { dispatcher, governance, thirdParty, maintainer, vault, tbtc, stbtc } -} - -describe("Dispatcher", () => { - let dispatcher: Dispatcher - let vault: TestERC4626 - let tbtc: TestERC20 - let stbtc: stBTC - - 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, maintainer, vault, tbtc, stbtc } = - await loadFixture(fixture)) - - vaultAddress1 = await ethers.Wallet.createRandom().getAddress() - vaultAddress2 = await ethers.Wallet.createRandom().getAddress() - vaultAddress3 = await ethers.Wallet.createRandom().getAddress() - vaultAddress4 = await ethers.Wallet.createRandom().getAddress() - }) - - 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", - ) - .withArgs(thirdParty.address) - }) - }) - - context("when caller is a governance account", () => { - 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) - - expect(await dispatcher.vaults(1)).to.equal(vaultAddress2) - expect(await dispatcher.vaultsInfo(vaultAddress2)).to.be.equal(true) - - expect(await dispatcher.vaults(2)).to.equal(vaultAddress3) - expect(await dispatcher.vaultsInfo(vaultAddress3)).to.be.equal(true) - }) - - 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(tx) - .to.emit(dispatcher, "VaultAuthorized") - .withArgs(vaultAddress1) - }) - }) - }) - - describe("deauthorizeVault", () => { - beforeAfterSnapshotWrapper() - - before(async () => { - await dispatcher.connect(governance).authorizeVault(vaultAddress1) - await dispatcher.connect(governance).authorizeVault(vaultAddress2) - await dispatcher.connect(governance).authorizeVault(vaultAddress3) - }) - - context("when caller is not a governance account", () => { - it("should revert when adding a vault", async () => { - await expect( - dispatcher.connect(thirdParty).deauthorizeVault(vaultAddress1), - ) - .to.be.revertedWithCustomError( - dispatcher, - "OwnableUnauthorizedAccount", - ) - .withArgs(thirdParty.address) - }) - }) - - context("when caller is a governance account", () => { - beforeAfterEachSnapshotWrapper() - - it("should deauthorize vaults", async () => { - await dispatcher.connect(governance).deauthorizeVault(vaultAddress1) - - // Last vault replaced the first vault in the 'vaults' array - expect(await dispatcher.vaults(0)).to.equal(vaultAddress3) - expect(await dispatcher.vaultsInfo(vaultAddress1)).to.be.equal(false) - expect((await dispatcher.getVaults()).length).to.equal(2) - - await dispatcher.connect(governance).deauthorizeVault(vaultAddress2) - - // Last vault (vaultAddress2) was removed from the 'vaults' array - expect(await dispatcher.vaults(0)).to.equal(vaultAddress3) - expect((await dispatcher.getVaults()).length).to.equal(1) - expect(await dispatcher.vaultsInfo(vaultAddress2)).to.be.equal(false) - - await dispatcher.connect(governance).deauthorizeVault(vaultAddress3) - expect((await dispatcher.getVaults()).length).to.equal(0) - expect(await dispatcher.vaultsInfo(vaultAddress3)).to.be.equal(false) - }) - - 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) - - await dispatcher.connect(governance).authorizeVault(vaultAddress1) - expect(await dispatcher.vaultsInfo(vaultAddress1)).to.be.equal(true) - }) - - it("should not deauthorize a vault that is not authorized", async () => { - await expect( - dispatcher.connect(governance).deauthorizeVault(vaultAddress4), - ).to.be.revertedWithCustomError(dispatcher, "VaultUnauthorized") - }) - - it("should emit an event when removing a vault", async () => { - await expect( - dispatcher.connect(governance).deauthorizeVault(vaultAddress1), - ) - .to.emit(dispatcher, "VaultDeauthorized") - .withArgs(vaultAddress1) - }) - }) - }) - - describe("depositToVault", () => { - beforeAfterSnapshotWrapper() - - const assetsToAllocate = to1e18(100) - const minSharesOut = to1e18(100) - - before(async () => { - await dispatcher.connect(governance).authorizeVault(vault.getAddress()) - await tbtc.mint(await stbtc.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, - [stbtc, vault], - [-assetsToAllocate, assetsToAllocate], - ) - }) - - it("should mint vault's shares for stBTC contract", async () => { - await expect(tx).to.changeTokenBalances( - vault, - [stbtc], - [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/dapp/src/acre-react/hooks/useStakeFlow.ts b/dapp/src/acre-react/hooks/useStakeFlow.ts index 169a28894..b13c3b8a5 100644 --- a/dapp/src/acre-react/hooks/useStakeFlow.ts +++ b/dapp/src/acre-react/hooks/useStakeFlow.ts @@ -67,7 +67,8 @@ export function useStakeFlow(): UseStakeFlowReturn { const stake = useCallback(async () => { if (!stakeFlow) throw new Error("Initialize stake first") - + // The current waiting time for repeat transactions is very long. + // TODO: Find the right value and pass it as additional options. await stakeFlow.stake() }, [stakeFlow]) diff --git a/dapp/src/assets/icons/Bitcoin.tsx b/dapp/src/assets/icons/BitcoinIcon.tsx similarity index 96% rename from dapp/src/assets/icons/Bitcoin.tsx rename to dapp/src/assets/icons/BitcoinIcon.tsx index c25918520..b98f2a8d3 100644 --- a/dapp/src/assets/icons/Bitcoin.tsx +++ b/dapp/src/assets/icons/BitcoinIcon.tsx @@ -1,8 +1,8 @@ import React from "react" import { createIcon } from "@chakra-ui/react" -export const Bitcoin = createIcon({ - displayName: "Bitcoin", +export const BitcoinIcon = createIcon({ + displayName: "BitcoinIcon", viewBox: "0 0 28 28", path: [ diff --git a/dapp/src/assets/icons/ConnectETHAccount.tsx b/dapp/src/assets/icons/CableWithPlugIcon.tsx similarity index 57% rename from dapp/src/assets/icons/ConnectETHAccount.tsx rename to dapp/src/assets/icons/CableWithPlugIcon.tsx index dfa2abb87..b460a843c 100644 --- a/dapp/src/assets/icons/ConnectETHAccount.tsx +++ b/dapp/src/assets/icons/CableWithPlugIcon.tsx @@ -1,8 +1,8 @@ import React from "react" import { createIcon } from "@chakra-ui/react" -export const ConnectETHAccount = createIcon({ - displayName: "ConnectETHAccount", +export const CableWithPlugIcon = createIcon({ + displayName: "CableWithPlugIcon", viewBox: "0 0 127 122", defaultProps: { fill: "none", @@ -13,35 +13,17 @@ export const ConnectETHAccount = createIcon({ stroke="url(#paint0_linear_3172_24686)" strokeWidth="2" />, - - - - - , , , , - - + + diff --git a/dapp/src/assets/icons/ConnectBTCAccount.tsx b/dapp/src/assets/icons/ConnectBTCAccount.tsx deleted file mode 100644 index 6ce8ef649..000000000 --- a/dapp/src/assets/icons/ConnectBTCAccount.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from "react" -import { createIcon } from "@chakra-ui/react" - -export const ConnectBTCAccount = createIcon({ - displayName: "ConnectBTCAccount", - viewBox: "0 0 127 122", - defaultProps: { - fill: "none", - }, - path: [ - , - - - - - , - , - , - , - , - - - - - - - - - , - ], -}) diff --git a/dapp/src/assets/icons/Ethereum.tsx b/dapp/src/assets/icons/Ethereum.tsx deleted file mode 100644 index 27cf854c6..000000000 --- a/dapp/src/assets/icons/Ethereum.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react" -import { createIcon } from "@chakra-ui/react" - -export const EthereumIcon = createIcon({ - displayName: "EthereumIcon", - viewBox: "0 0 24 25", - path: [ - - - - , - - - - - , - ], -}) diff --git a/dapp/src/assets/icons/EthereumIcon.tsx b/dapp/src/assets/icons/EthereumIcon.tsx new file mode 100644 index 000000000..633e86193 --- /dev/null +++ b/dapp/src/assets/icons/EthereumIcon.tsx @@ -0,0 +1,31 @@ +import React from "react" +import { createIcon } from "@chakra-ui/react" + +export const EthereumIcon = createIcon({ + displayName: "EthereumIcon", + viewBox: "0 0 56 57", + path: [ + + + + + , + + + + + , + ], +}) diff --git a/dapp/src/assets/icons/index.ts b/dapp/src/assets/icons/index.ts index f6931b182..d786957f5 100644 --- a/dapp/src/assets/icons/index.ts +++ b/dapp/src/assets/icons/index.ts @@ -1,12 +1,8 @@ export * from "./Info" -export * from "./Bitcoin" -export * from "./Ethereum" export * from "./ArrowUpRight" export * from "./ArrowLeft" export * from "./ArrowRight" export * from "./AcreLogo" -export * from "./ConnectBTCAccount" -export * from "./ConnectETHAccount" export * from "./stBTC" export * from "./BTC" export * from "./ShieldPlus" @@ -18,3 +14,6 @@ export * from "./SortDESC" export * from "./Pause" export * from "./ChevronRight" export * from "./LoadingSpinnerSuccessIcon" +export * from "./BitcoinIcon" +export * from "./EthereumIcon" +export * from "./CableWithPlugIcon" diff --git a/dapp/src/components/Header/ConnectWallet.tsx b/dapp/src/components/Header/ConnectWallet.tsx index 1a76f97c3..0398874df 100644 --- a/dapp/src/components/Header/ConnectWallet.tsx +++ b/dapp/src/components/Header/ConnectWallet.tsx @@ -1,10 +1,10 @@ import React from "react" import { Button, HStack, Icon, Tooltip } from "@chakra-ui/react" -import { Account } from "@ledgerhq/wallet-api-client" import { useWallet } from "#/hooks" import { CurrencyBalance } from "#/components/shared/CurrencyBalance" import { TextMd } from "#/components/shared/Typography" -import { Bitcoin, EthereumIcon } from "#/assets/icons" +import { BitcoinIcon, EthereumIcon } from "#/assets/icons" +import { Account } from "@ledgerhq/wallet-api-client" import { CURRENCY_ID_BITCOIN } from "#/constants" import { isSupportedBTCAddressType, @@ -61,7 +61,7 @@ export default function ConnectWallet() { + + + Your funds are secure. + + + + ) +} diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/StakingErrorModal/ServerErrorModal.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/StakingErrorModal/ServerErrorModal.tsx new file mode 100644 index 000000000..ff4cc6b5e --- /dev/null +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/StakingErrorModal/ServerErrorModal.tsx @@ -0,0 +1,97 @@ +import React from "react" +import { + Button, + Flex, + HStack, + Icon, + Link, + ModalBody, + ModalCloseButton, + ModalFooter, + ModalHeader, + Tooltip, +} from "@chakra-ui/react" +import { CableWithPlugIcon, Info } from "#/assets/icons" +import { TextMd } from "#/components/shared/Typography" +import { EXTERNAL_HREF } from "#/constants" +import IconWrapper from "#/components/shared/IconWrapper" +import { MODAL_BASE_SIZE } from "#/components/shared/ModalBase" +import { + IconBrandDiscordFilled, + IconReload, + IconServerBolt, +} from "@tabler/icons-react" + +export default function ServerErrorModal({ + isLoading, + retry, +}: { + isLoading: boolean + retry: () => void +}) { + return ( + <> + + + We're currently facing system issues. + + + + + + + Your deposit didn't go through but no worries, your funds are + safe. + + + + + + + System status + {/* TODO: ADD a tooltip */} + + + + + Partial Outage + + + + + ) +} diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/StakingErrorModal/index.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/StakingErrorModal/index.tsx new file mode 100644 index 000000000..6ae4e1276 --- /dev/null +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/StakingErrorModal/index.tsx @@ -0,0 +1,50 @@ +import React, { useCallback, useState } from "react" +import { + useExecuteFunction, + useModalFlowContext, + useStakeFlowContext, +} from "#/hooks" +import { PROCESS_STATUSES } from "#/types" +import { logPromiseFailure } from "#/utils" +import ServerErrorModal from "./ServerErrorModal" +import RetryModal from "./RetryModal" +import LoadingModal from "../../LoadingModal" + +export default function StakingErrorModal() { + const { setStatus } = useModalFlowContext() + const { stake } = useStakeFlowContext() + + const [isLoading, setIsLoading] = useState(false) + const [isServerError, setIsServerError] = useState(false) + + const onStakeBTCSuccess = useCallback( + () => setStatus(PROCESS_STATUSES.SUCCEEDED), + [setStatus], + ) + + const onStakeBTCError = useCallback(() => setIsServerError(true), []) + + const handleStake = useExecuteFunction( + stake, + onStakeBTCSuccess, + onStakeBTCError, + ) + + const handleRetry = useCallback(async () => { + setIsLoading(true) + await handleStake() + setIsLoading(false) + }, [handleStake]) + + const handleRetryWrapper = useCallback( + () => logPromiseFailure(handleRetry()), + [handleRetry], + ) + + if (isServerError) + return + + if (isLoading) return + + return +} diff --git a/dapp/src/components/TransactionModal/ErrorModal.tsx b/dapp/src/components/TransactionModal/ErrorModal.tsx new file mode 100644 index 000000000..92e343c31 --- /dev/null +++ b/dapp/src/components/TransactionModal/ErrorModal.tsx @@ -0,0 +1,9 @@ +import React from "react" +import { ActionFlowType } from "#/types" +import StakingErrorModal from "./ActiveStakingStep/StakingErrorModal" + +export default function ErrorModal({ type }: { type: ActionFlowType }) { + if (type === "stake") return + // TODO: Handle the case of unstake action + return null +} diff --git a/dapp/src/components/TransactionModal/ModalContentWrapper/LoadingModal.tsx b/dapp/src/components/TransactionModal/LoadingModal.tsx similarity index 100% rename from dapp/src/components/TransactionModal/ModalContentWrapper/LoadingModal.tsx rename to dapp/src/components/TransactionModal/LoadingModal.tsx diff --git a/dapp/src/components/TransactionModal/ModalContentWrapper/MissingAccountModal.tsx b/dapp/src/components/TransactionModal/MissingAccountModal.tsx similarity index 84% rename from dapp/src/components/TransactionModal/ModalContentWrapper/MissingAccountModal.tsx rename to dapp/src/components/TransactionModal/MissingAccountModal.tsx index e2c358152..d93f7287e 100644 --- a/dapp/src/components/TransactionModal/ModalContentWrapper/MissingAccountModal.tsx +++ b/dapp/src/components/TransactionModal/MissingAccountModal.tsx @@ -12,6 +12,8 @@ import { TextMd } from "#/components/shared/Typography" import { logPromiseFailure, getCurrencyByType } from "#/utils" import { CurrencyType, RequestAccountParams } from "#/types" import { CardAlert } from "#/components/shared/alerts" +import IconWrapper from "#/components/shared/IconWrapper" +import { CableWithPlugIcon } from "#/assets/icons" type MissingAccountModalProps = { currency: CurrencyType @@ -34,8 +36,10 @@ export default function MissingAccountModal({ <> {name} account not installed - - + + + + {name} account is required to make transactions for depositing and staking your {symbol}. diff --git a/dapp/src/components/TransactionModal/ModalContentWrapper/index.tsx b/dapp/src/components/TransactionModal/ModalContentWrapper.tsx similarity index 88% rename from dapp/src/components/TransactionModal/ModalContentWrapper/index.tsx rename to dapp/src/components/TransactionModal/ModalContentWrapper.tsx index 3e06e68c7..33c94bdbe 100644 --- a/dapp/src/components/TransactionModal/ModalContentWrapper/index.tsx +++ b/dapp/src/components/TransactionModal/ModalContentWrapper.tsx @@ -6,14 +6,15 @@ import { useTransactionContext, useWalletContext, } from "#/hooks" -import { ConnectBTCAccount, ConnectETHAccount } from "#/assets/icons" +import { BitcoinIcon, EthereumIcon } from "#/assets/icons" import { ActionFlowType, PROCESS_STATUSES } from "#/types" import { isSupportedBTCAddressType } from "#/utils" import ActionFormModal from "./ActionFormModal" +import ErrorModal from "./ErrorModal" +import LoadingModal from "./LoadingModal" import MissingAccountModal from "./MissingAccountModal" import ResumeModal from "./ResumeModal" import SuccessModal from "./SuccessModal" -import LoadingModal from "./LoadingModal" export default function ModalContentWrapper({ defaultType, @@ -32,7 +33,7 @@ export default function ModalContentWrapper({ return ( ) @@ -41,7 +42,7 @@ export default function ModalContentWrapper({ return ( ) @@ -56,5 +57,7 @@ export default function ModalContentWrapper({ if (status === PROCESS_STATUSES.SUCCEEDED) return + if (status === PROCESS_STATUSES.FAILED) return + return children } diff --git a/dapp/src/components/TransactionModal/ModalContentWrapper/ResumeModal.tsx b/dapp/src/components/TransactionModal/ResumeModal.tsx similarity index 100% rename from dapp/src/components/TransactionModal/ModalContentWrapper/ResumeModal.tsx rename to dapp/src/components/TransactionModal/ResumeModal.tsx diff --git a/dapp/src/components/TransactionModal/ModalContentWrapper/SuccessModal.tsx b/dapp/src/components/TransactionModal/SuccessModal.tsx similarity index 100% rename from dapp/src/components/TransactionModal/ModalContentWrapper/SuccessModal.tsx rename to dapp/src/components/TransactionModal/SuccessModal.tsx diff --git a/dapp/src/components/shared/IconWrapper.tsx b/dapp/src/components/shared/IconWrapper.tsx new file mode 100644 index 000000000..fc35fcb42 --- /dev/null +++ b/dapp/src/components/shared/IconWrapper.tsx @@ -0,0 +1,20 @@ +import React from "react" +import { Box, HStack, Icon, IconProps } from "@chakra-ui/react" + +type IconWrapperProps = { + icon: typeof Icon + children: React.ReactNode +} & IconProps + +export default function IconWrapper({ + icon, + children, + ...props +}: IconWrapperProps) { + return ( + + + {children} + + ) +} diff --git a/dapp/src/components/shared/ModalBase/index.tsx b/dapp/src/components/shared/ModalBase/index.tsx index 2c812ce0a..416b22850 100644 --- a/dapp/src/components/shared/ModalBase/index.tsx +++ b/dapp/src/components/shared/ModalBase/index.tsx @@ -1,9 +1,11 @@ import React from "react" import { Modal, ModalContent, ModalOverlay, ModalProps } from "@chakra-ui/react" +export const MODAL_BASE_SIZE = "lg" + export default function ModalBase({ children, ...restProps }: ModalProps) { return ( - + {children} diff --git a/dapp/src/constants/externalHref.ts b/dapp/src/constants/externalHref.ts new file mode 100644 index 000000000..cdf77bfcf --- /dev/null +++ b/dapp/src/constants/externalHref.ts @@ -0,0 +1,4 @@ +export const EXTERNAL_HREF = { + // TODO: Add a correct link + DISCORD: "https://discord.com/", +} diff --git a/dapp/src/constants/index.ts b/dapp/src/constants/index.ts index 920e2c38b..653ef4777 100644 --- a/dapp/src/constants/index.ts +++ b/dapp/src/constants/index.ts @@ -2,3 +2,4 @@ export * from "./currency" export * from "./staking" export * from "./chains" export * from "./time" +export * from "./externalHref" diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index d245edf5a..f8dd6aba2 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -20,6 +20,7 @@ export * from "./useDepositTelemetry" export * from "./useFetchBTCPriceUSD" export * from "./useWallet" export * from "./useTimeout" +export * from "./useCountdown" export * from "./useActivities" export * from "./useFetchBTCBalance" export * from "./useSize" diff --git a/dapp/src/hooks/useCountdown.ts b/dapp/src/hooks/useCountdown.ts new file mode 100644 index 000000000..69a9c91a2 --- /dev/null +++ b/dapp/src/hooks/useCountdown.ts @@ -0,0 +1,56 @@ +import { useState, useEffect } from "react" +import { TimeUnits } from "#/types" +import { ONE_SEC_IN_MILLISECONDS } from "#/constants" +import { + dateToUnixTimestamp, + unixTimestampToTimeUnits, + addLeadingZero, +} from "#/utils" + +/** + * It was decided to use an already implemented hook used by Threshold Network. + * Hook allows us to count down the time until the specified date and. + * After time has passed we can call the specified action. + * + * Source: + * https://github.com/threshold-network/components/blob/main/src/hooks/useCountdown.ts + */ +export const useCountdown = ( + targetDateInUnix: number, + addLeadingZeroes?: boolean, + onComplete?: (targetDateInUnix: number) => void, +): TimeUnits => { + const [diff, setDiff] = useState(targetDateInUnix - dateToUnixTimestamp()) + + useEffect(() => { + const countdownInterval = setInterval(() => { + const newDiff = targetDateInUnix - dateToUnixTimestamp() + if (newDiff === 0) { + if (onComplete) { + onComplete(targetDateInUnix) + } + clearInterval(countdownInterval) + } + + setDiff(newDiff) + }, ONE_SEC_IN_MILLISECONDS) + + return () => clearInterval(countdownInterval) + }, [targetDateInUnix, onComplete]) + + let { days, hours, minutes, seconds } = unixTimestampToTimeUnits(diff) + + if (addLeadingZeroes) { + days = addLeadingZero(Number(days)) + hours = addLeadingZero(Number(hours)) + minutes = addLeadingZero(Number(minutes)) + seconds = addLeadingZero(Number(seconds)) + } + + return { + days, + hours, + minutes, + seconds, + } +} diff --git a/dapp/src/theme/Button.ts b/dapp/src/theme/Button.ts index b424d1f53..ccb108bd7 100644 --- a/dapp/src/theme/Button.ts +++ b/dapp/src/theme/Button.ts @@ -45,6 +45,13 @@ export const buttonTheme: ComponentSingleStyleConfig = { _active: { bg: "transparent", }, + _loading: { + _disabled: { + borderColor: "white", + background: "opacity.white.5", + opacity: 1, + }, + }, } if (colorScheme === "gold") { return { diff --git a/dapp/src/theme/Modal.ts b/dapp/src/theme/Modal.ts index d667b316e..8436cc293 100644 --- a/dapp/src/theme/Modal.ts +++ b/dapp/src/theme/Modal.ts @@ -3,7 +3,7 @@ import { createMultiStyleConfigHelpers, defineStyle } from "@chakra-ui/react" const baseStyleDialog = defineStyle({ p: 4, - border: "2px", + borderWidth: "var(--chakra-space-modal_borderWidth)", boxShadow: "none", borderColor: "white", borderRadius: "xl", @@ -47,6 +47,11 @@ const baseStyleBody = defineStyle({ gap: 6, }) +const baseStyleFooter = defineStyle({ + flexDirection: "column", + gap: 6, +}) + const multiStyleConfig = createMultiStyleConfigHelpers(parts.keys) const baseStyle = multiStyleConfig.definePartsStyle({ @@ -55,6 +60,7 @@ const baseStyle = multiStyleConfig.definePartsStyle({ overlay: baseStyleOverlay, header: baseStyleHeader, body: baseStyleBody, + footer: baseStyleFooter, }) export const modalTheme = multiStyleConfig.defineMultiStyleConfig({ baseStyle }) diff --git a/dapp/src/theme/utils/semanticTokens.ts b/dapp/src/theme/utils/semanticTokens.ts index 41879fe4d..c25ccd8b7 100644 --- a/dapp/src/theme/utils/semanticTokens.ts +++ b/dapp/src/theme/utils/semanticTokens.ts @@ -3,6 +3,7 @@ export const semanticTokens = { header_height: 24, modal_shift: 48, toast_container_shift: 16, + modal_borderWidth: "2px", }, sizes: { sidebar_width: 80, diff --git a/dapp/src/types/index.ts b/dapp/src/types/index.ts index f36accc9c..16d93da3b 100644 --- a/dapp/src/types/index.ts +++ b/dapp/src/types/index.ts @@ -10,5 +10,6 @@ export * from "./location" export * from "./charts" export * from "./activity" export * from "./coingecko" +export * from "./time" export * from "./size" export * from "./toast" diff --git a/dapp/src/types/time.ts b/dapp/src/types/time.ts new file mode 100644 index 000000000..888fd0ffa --- /dev/null +++ b/dapp/src/types/time.ts @@ -0,0 +1,6 @@ +export type TimeUnits = { + days: string + hours: string + minutes: string + seconds: string +} diff --git a/dapp/src/utils/numbers.ts b/dapp/src/utils/numbers.ts index 70fa2c307..9cf07bcbf 100644 --- a/dapp/src/utils/numbers.ts +++ b/dapp/src/utils/numbers.ts @@ -206,3 +206,9 @@ export function getDesiredDecimals(amount: string | number, decimals: number) { const desiredDecimals = decimals - undecimaledAmount.length + 1 return desiredDecimals > 0 ? desiredDecimals : 2 } + +export const addLeadingZero = (num: number): string => + num >= 0 && num <= 9 ? `0${num}` : `${num}` + +export const getPercentValue = (value: number, maxValue: number) => + (value * 100) / maxValue diff --git a/dapp/src/utils/time.ts b/dapp/src/utils/time.ts index 30e6da46e..5fb59284c 100644 --- a/dapp/src/utils/time.ts +++ b/dapp/src/utils/time.ts @@ -7,10 +7,29 @@ import { ONE_WEEK_IN_SECONDS, ONE_YEAR_IN_SECONDS, } from "#/constants" +import { TimeUnits } from "#/types" export const dateToUnixTimestamp = (date: Date = new Date()) => Math.floor(date.getTime() / ONE_SEC_IN_MILLISECONDS) +export const unixTimestampToTimeUnits = (targetUnix: number): TimeUnits => { + const days = Math.floor(targetUnix / ONE_DAY_IN_SECONDS) + const hours = Math.floor( + (targetUnix % ONE_DAY_IN_SECONDS) / ONE_HOUR_IN_SECONDS, + ) + const minutes = Math.floor( + (targetUnix % ONE_HOUR_IN_SECONDS) / ONE_MINUTE_IN_SECONDS, + ) + const seconds = Math.floor(targetUnix % ONE_MINUTE_IN_SECONDS) + + return { + days: days.toString(), + hours: hours.toString(), + minutes: minutes.toString(), + seconds: seconds.toString(), + } +} + // unit, max diff, divisor const unitsToDivisor: [Intl.RelativeTimeFormatUnit, number, number][] = [ ["second", ONE_MINUTE_IN_SECONDS, 1], @@ -60,3 +79,14 @@ export const displayBlockTimestamp = (blockTimestamp: number) => { return getRelativeTime(blockTimestamp) } + +/** + * Returns the expiration timestamp from the start date considering the specified duration. + * If the startDate is not passed, the function will take the current time as the start date. + */ +export const getExpirationTimestamp = (duration: number, startDate?: Date) => { + const date = startDate ?? new Date() + const expirationDate = new Date(date.getTime() + duration) + + return dateToUnixTimestamp(expirationDate) +} diff --git a/dapp/src/web3/relayer-depositor-proxy.ts b/dapp/src/web3/relayer-depositor-proxy.ts index 6ef83b38e..7207df6f9 100644 --- a/dapp/src/web3/relayer-depositor-proxy.ts +++ b/dapp/src/web3/relayer-depositor-proxy.ts @@ -72,7 +72,7 @@ class RelayerDepositorProxy if (!extraData) throw new Error("Invalid extra data") - const { staker, referral } = + const { depositOwner, referral } = this.#bitcoinDepositor.decodeExtraData(extraData) // TODO: Catch and handle errors + sentry. @@ -81,7 +81,7 @@ class RelayerDepositorProxy { fundingTx, reveal, - staker: `0x${staker.identifierHex}`, + depositOwner: `0x${depositOwner.identifierHex}`, referral, }, ) diff --git a/netlify.toml b/netlify.toml index 59e82dbf2..aea5fdaae 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,7 +1,7 @@ [build] - # Don't run builds after the changes touching only the listed paths. - ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF ':(exclude).github/' ':(exclude).vscode/' ':(exclude)core/' ':(exclude).git-blame-ignore-revs' ':(exclude).gitignore' ':(exclude).npmrc' ':(exclude).nvmrc' ':(exclude).pre-commit-config.yaml' ':(exclude).prettierignore' ':(exclude).prettierrc.js' ':(exclude).syncpackrc' ':(exclude)LICENSE' ':(exclude)README.md'" +# Don't run builds after the changes touching only the listed paths. +ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF ':(exclude).github/' ':(exclude).vscode/' ':(exclude)solidity/' ':(exclude).git-blame-ignore-revs' ':(exclude).gitignore' ':(exclude).npmrc' ':(exclude).nvmrc' ':(exclude).pre-commit-config.yaml' ':(exclude).prettierignore' ':(exclude).prettierrc.js' ':(exclude).syncpackrc' ':(exclude)LICENSE' ':(exclude)README.md'" [context.production] - # Do not run builds for the production context. - ignore = "exit 0" +# Do not run builds for the production context. +ignore = "exit 0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3f525395..307077ecd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,121 +18,6 @@ importers: specifier: ^11.2.1 version: 11.2.1 - core: - dependencies: - '@keep-network/bitcoin-spv-sol': - specifier: 3.4.0-solc-0.8 - version: 3.4.0-solc-0.8 - '@keep-network/tbtc-v2': - specifier: development - version: 1.6.0-dev.24(@keep-network/keep-core@1.8.1-dev.0) - '@openzeppelin/contracts': - specifier: ^5.0.0 - version: 5.0.0 - '@openzeppelin/contracts-upgradeable': - specifier: ^5.0.2 - version: 5.0.2(@openzeppelin/contracts@5.0.0) - '@thesis-co/solidity-contracts': - specifier: github:thesis/solidity-contracts#c315b9d - version: github.com/thesis/solidity-contracts/c315b9d - '@types/chai-as-promised': - specifier: ^7.1.8 - version: 7.1.8 - chai-as-promised: - specifier: ^7.1.1 - version: 7.1.1(chai@4.3.10) - devDependencies: - '@keep-network/hardhat-helpers': - specifier: ^0.7.1 - version: 0.7.1(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-verify@2.0.1)(@openzeppelin/hardhat-upgrades@3.0.5)(ethers@6.8.1)(hardhat-deploy@0.11.43)(hardhat@2.19.1) - '@nomicfoundation/hardhat-chai-matchers': - specifier: ^2.0.2 - version: 2.0.2(@nomicfoundation/hardhat-ethers@3.0.5)(chai@4.3.10)(ethers@6.8.1)(hardhat@2.19.1) - '@nomicfoundation/hardhat-ethers': - specifier: ^3.0.5 - version: 3.0.5(ethers@6.8.1)(hardhat@2.19.1) - '@nomicfoundation/hardhat-network-helpers': - specifier: ^1.0.9 - version: 1.0.9(hardhat@2.19.1) - '@nomicfoundation/hardhat-toolbox': - specifier: ^4.0.0 - version: 4.0.0(@nomicfoundation/hardhat-chai-matchers@2.0.2)(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-network-helpers@1.0.9)(@nomicfoundation/hardhat-verify@2.0.1)(@typechain/ethers-v6@0.5.1)(@typechain/hardhat@9.1.0)(@types/chai@4.3.11)(@types/mocha@10.0.6)(@types/node@20.9.4)(chai@4.3.10)(ethers@6.8.1)(hardhat-gas-reporter@1.0.9)(hardhat@2.19.1)(solidity-coverage@0.8.5)(ts-node@10.9.1)(typechain@8.3.2)(typescript@5.3.2) - '@nomicfoundation/hardhat-verify': - specifier: ^2.0.1 - version: 2.0.1(hardhat@2.19.1) - '@nomiclabs/hardhat-etherscan': - specifier: ^3.1.7 - version: 3.1.7(hardhat@2.19.1) - '@openzeppelin/hardhat-upgrades': - specifier: ^3.0.5 - version: 3.0.5(@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: 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) - '@typechain/hardhat': - specifier: ^9.1.0 - version: 9.1.0(@typechain/ethers-v6@0.5.1)(ethers@6.8.1)(hardhat@2.19.1)(typechain@8.3.2) - '@types/chai': - specifier: ^4.3.11 - version: 4.3.11 - '@types/mocha': - specifier: ^10.0.6 - version: 10.0.6 - '@types/node': - specifier: ^20.9.4 - version: 20.9.4 - chai: - specifier: ^4.3.10 - version: 4.3.10 - eslint: - specifier: ^8.54.0 - version: 8.54.0 - ethers: - specifier: ^6.8.1 - version: 6.8.1 - hardhat: - specifier: ^2.19.1 - version: 2.19.1(ts-node@10.9.1)(typescript@5.3.2) - hardhat-contract-sizer: - specifier: ^2.10.0 - version: 2.10.0(hardhat@2.19.1) - hardhat-deploy: - specifier: ^0.11.43 - version: 0.11.43 - hardhat-gas-reporter: - specifier: ^1.0.9 - version: 1.0.9(hardhat@2.19.1) - prettier: - specifier: ^3.1.0 - version: 3.1.0 - prettier-plugin-solidity: - specifier: ^1.2.0 - version: 1.2.0(prettier@3.1.0) - solhint: - specifier: ^4.0.0 - version: 4.0.0 - solhint-config-thesis: - specifier: github:thesis/solhint-config - version: github.com/thesis/solhint-config/266de12d96d58f01171e20858b855ec80520de8d(solhint@4.0.0) - 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) - typechain: - specifier: ^8.3.2 - version: 8.3.2(typescript@5.3.2) - typescript: - specifier: ^5.3.2 - version: 5.3.2 - dapp: dependencies: '@acre-btc/sdk': @@ -173,7 +58,7 @@ importers: version: 0.23.13 axios: specifier: ^1.6.7 - version: 1.6.7 + version: 1.6.7(debug@4.3.4) ethers: specifier: ^6.10.0 version: 6.10.0 @@ -247,9 +132,9 @@ importers: sdk: dependencies: - '@acre-btc/core': + '@acre-btc/contracts': specifier: workspace:* - version: link:../core + version: link:../solidity '@keep-network/tbtc-v2.ts': specifier: 2.4.0-dev.3 version: 2.4.0-dev.3(@keep-network/keep-core@1.8.1-dev.0) @@ -288,6 +173,121 @@ importers: specifier: ^5.3.2 version: 5.3.2 + solidity: + dependencies: + '@keep-network/bitcoin-spv-sol': + specifier: 3.4.0-solc-0.8 + version: 3.4.0-solc-0.8 + '@keep-network/tbtc-v2': + specifier: development + version: 1.7.0-dev.4(@keep-network/keep-core@1.8.1-dev.0) + '@openzeppelin/contracts': + specifier: ^5.0.0 + version: 5.0.0 + '@openzeppelin/contracts-upgradeable': + specifier: ^5.0.2 + version: 5.0.2(@openzeppelin/contracts@5.0.0) + '@thesis-co/solidity-contracts': + specifier: github:thesis/solidity-contracts#c315b9d + version: github.com/thesis/solidity-contracts/c315b9d + '@types/chai-as-promised': + specifier: ^7.1.8 + version: 7.1.8 + chai-as-promised: + specifier: ^7.1.1 + version: 7.1.1(chai@4.3.10) + devDependencies: + '@keep-network/hardhat-helpers': + specifier: ^0.7.1 + version: 0.7.1(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-verify@2.0.1)(@openzeppelin/hardhat-upgrades@3.0.5)(ethers@6.10.0)(hardhat-deploy@0.11.43)(hardhat@2.19.1) + '@nomicfoundation/hardhat-chai-matchers': + specifier: ^2.0.2 + version: 2.0.2(@nomicfoundation/hardhat-ethers@3.0.5)(chai@4.3.10)(ethers@6.10.0)(hardhat@2.19.1) + '@nomicfoundation/hardhat-ethers': + specifier: ^3.0.5 + version: 3.0.5(ethers@6.10.0)(hardhat@2.19.1) + '@nomicfoundation/hardhat-network-helpers': + specifier: ^1.0.9 + version: 1.0.9(hardhat@2.19.1) + '@nomicfoundation/hardhat-toolbox': + specifier: ^4.0.0 + version: 4.0.0(@nomicfoundation/hardhat-chai-matchers@2.0.2)(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-network-helpers@1.0.9)(@nomicfoundation/hardhat-verify@2.0.1)(@typechain/ethers-v6@0.5.1)(@typechain/hardhat@9.1.0)(@types/chai@4.3.11)(@types/mocha@10.0.6)(@types/node@20.9.4)(chai@4.3.10)(ethers@6.10.0)(hardhat-gas-reporter@1.0.9)(hardhat@2.19.1)(solidity-coverage@0.8.5)(ts-node@10.9.1)(typechain@8.3.2)(typescript@5.3.2) + '@nomicfoundation/hardhat-verify': + specifier: ^2.0.1 + version: 2.0.1(hardhat@2.19.1) + '@nomiclabs/hardhat-etherscan': + specifier: ^3.1.7 + version: 3.1.7(hardhat@2.19.1) + '@openzeppelin/hardhat-upgrades': + specifier: ^3.0.5 + version: 3.0.5(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-verify@2.0.1)(ethers@6.10.0)(hardhat@2.19.1) + '@thesis-co/eslint-config': + 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.10.0)(typechain@8.3.2)(typescript@5.3.2) + '@typechain/hardhat': + specifier: ^9.1.0 + version: 9.1.0(@typechain/ethers-v6@0.5.1)(ethers@6.10.0)(hardhat@2.19.1)(typechain@8.3.2) + '@types/chai': + specifier: ^4.3.11 + version: 4.3.11 + '@types/mocha': + specifier: ^10.0.6 + version: 10.0.6 + '@types/node': + specifier: ^20.9.4 + version: 20.9.4 + chai: + specifier: ^4.3.10 + version: 4.3.10 + eslint: + specifier: ^8.54.0 + version: 8.54.0 + ethers: + specifier: ^6.8.1 + version: 6.10.0 + hardhat: + specifier: ^2.19.1 + version: 2.19.1(ts-node@10.9.1)(typescript@5.3.2) + hardhat-contract-sizer: + specifier: ^2.10.0 + version: 2.10.0(hardhat@2.19.1) + hardhat-deploy: + specifier: ^0.11.43 + version: 0.11.43 + hardhat-gas-reporter: + specifier: ^1.0.9 + version: 1.0.9(hardhat@2.19.1) + prettier: + specifier: ^3.1.0 + version: 3.1.0 + prettier-plugin-solidity: + specifier: ^1.2.0 + version: 1.2.0(prettier@3.1.0) + solhint: + specifier: ^4.0.0 + version: 4.0.0 + solhint-config-thesis: + specifier: github:thesis/solhint-config + version: github.com/thesis/solhint-config/266de12d96d58f01171e20858b855ec80520de8d(solhint@4.0.0) + 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) + typechain: + specifier: ^8.3.2 + version: 8.3.2(typescript@5.3.2) + typescript: + specifier: ^5.3.2 + version: 5.3.2 + subgraph: dependencies: '@graphprotocol/graph-cli': @@ -483,7 +483,7 @@ packages: '@babel/traverse': 7.23.4 '@babel/types': 7.23.4 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1782,7 +1782,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.4 '@babel/types': 7.23.4 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3506,7 +3506,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) espree: 9.6.1 globals: 13.23.0 ignore: 5.3.0 @@ -4161,7 +4161,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4503,7 +4503,7 @@ packages: - '@keep-network/keep-core' dev: false - /@keep-network/hardhat-helpers@0.7.1(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-verify@2.0.1)(@openzeppelin/hardhat-upgrades@3.0.5)(ethers@6.8.1)(hardhat-deploy@0.11.43)(hardhat@2.19.1): + /@keep-network/hardhat-helpers@0.7.1(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-verify@2.0.1)(@openzeppelin/hardhat-upgrades@3.0.5)(ethers@6.10.0)(hardhat-deploy@0.11.43)(hardhat@2.19.1): resolution: {integrity: sha512-de6Gy45JukZwGgZqVuR+Zq5PSqnmvKLDJn0/KrKT5tFzGspARUf1WzhDgTTB/D7gTK04sxlrL6WJM3XQ/wZEkw==} peerDependencies: '@nomicfoundation/hardhat-ethers': ^3.0.5 @@ -4513,10 +4513,10 @@ packages: hardhat: ^2.19.4 hardhat-deploy: ^0.11.45 dependencies: - '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.8.1)(hardhat@2.19.1) + '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.10.0)(hardhat@2.19.1) '@nomicfoundation/hardhat-verify': 2.0.1(hardhat@2.19.1) - '@openzeppelin/hardhat-upgrades': 3.0.5(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-verify@2.0.1)(ethers@6.8.1)(hardhat@2.19.1) - ethers: 6.8.1 + '@openzeppelin/hardhat-upgrades': 3.0.5(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-verify@2.0.1)(ethers@6.10.0)(hardhat@2.19.1) + ethers: 6.10.0 hardhat: 2.19.1(ts-node@10.9.1)(typescript@5.3.2) hardhat-deploy: 0.11.43 dev: true @@ -4638,8 +4638,8 @@ packages: - utf-8-validate dev: false - /@keep-network/tbtc-v2@1.6.0-dev.24(@keep-network/keep-core@1.8.1-dev.0): - resolution: {integrity: sha512-Nj/TYHlVg5J1ubVlLEbO7IQiqecDmVf2DPOqkICpveFxOiIbqzVtop4CsgEPvyRBomzHDUvZ6QFxYxJj/wbJbA==} + /@keep-network/tbtc-v2@1.7.0-dev.4(@keep-network/keep-core@1.8.1-dev.0): + resolution: {integrity: sha512-+DxR5XebK0DB5WIrQyCQG2osixBYpJhOuwQtLu3EDMsi4tFAPEh5MFjWG5LYeuEtX65p19mSC4Vj69/Z3jMgrA==} engines: {node: '>= 14.0.0'} dependencies: '@keep-network/bitcoin-spv-sol': 3.4.0-solc-0.8 @@ -5131,7 +5131,7 @@ packages: - utf-8-validate dev: true - /@nomicfoundation/hardhat-chai-matchers@2.0.2(@nomicfoundation/hardhat-ethers@3.0.5)(chai@4.3.10)(ethers@6.8.1)(hardhat@2.19.1): + /@nomicfoundation/hardhat-chai-matchers@2.0.2(@nomicfoundation/hardhat-ethers@3.0.5)(chai@4.3.10)(ethers@6.10.0)(hardhat@2.19.1): resolution: {integrity: sha512-9Wu9mRtkj0U9ohgXYFbB/RQDa+PcEdyBm2suyEtsJf3PqzZEEjLUZgWnMjlFhATMk/fp3BjmnYVPrwl+gr8oEw==} peerDependencies: '@nomicfoundation/hardhat-ethers': ^3.0.0 @@ -5139,24 +5139,24 @@ packages: ethers: ^6.1.0 hardhat: ^2.9.4 dependencies: - '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.8.1)(hardhat@2.19.1) + '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.10.0)(hardhat@2.19.1) '@types/chai-as-promised': 7.1.8 chai: 4.3.10 chai-as-promised: 7.1.1(chai@4.3.10) deep-eql: 4.1.3 - ethers: 6.8.1 + ethers: 6.10.0 hardhat: 2.19.1(ts-node@10.9.1)(typescript@5.3.2) ordinal: 1.0.3 dev: true - /@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.8.1)(hardhat@2.19.1): + /@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.10.0)(hardhat@2.19.1): resolution: {integrity: sha512-RNFe8OtbZK6Ila9kIlHp0+S80/0Bu/3p41HUpaRIoHLm6X3WekTd83vob3rE54Duufu1edCiBDxspBzi2rxHHw==} peerDependencies: ethers: ^6.1.0 hardhat: ^2.0.0 dependencies: debug: 4.3.4(supports-color@8.1.1) - ethers: 6.8.1 + ethers: 6.10.0 hardhat: 2.19.1(ts-node@10.9.1)(typescript@5.3.2) lodash.isequal: 4.5.0 transitivePeerDependencies: @@ -5172,7 +5172,7 @@ packages: hardhat: 2.19.1(ts-node@10.9.1)(typescript@5.3.2) dev: true - /@nomicfoundation/hardhat-toolbox@4.0.0(@nomicfoundation/hardhat-chai-matchers@2.0.2)(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-network-helpers@1.0.9)(@nomicfoundation/hardhat-verify@2.0.1)(@typechain/ethers-v6@0.5.1)(@typechain/hardhat@9.1.0)(@types/chai@4.3.11)(@types/mocha@10.0.6)(@types/node@20.9.4)(chai@4.3.10)(ethers@6.8.1)(hardhat-gas-reporter@1.0.9)(hardhat@2.19.1)(solidity-coverage@0.8.5)(ts-node@10.9.1)(typechain@8.3.2)(typescript@5.3.2): + /@nomicfoundation/hardhat-toolbox@4.0.0(@nomicfoundation/hardhat-chai-matchers@2.0.2)(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-network-helpers@1.0.9)(@nomicfoundation/hardhat-verify@2.0.1)(@typechain/ethers-v6@0.5.1)(@typechain/hardhat@9.1.0)(@types/chai@4.3.11)(@types/mocha@10.0.6)(@types/node@20.9.4)(chai@4.3.10)(ethers@6.10.0)(hardhat-gas-reporter@1.0.9)(hardhat@2.19.1)(solidity-coverage@0.8.5)(ts-node@10.9.1)(typechain@8.3.2)(typescript@5.3.2): resolution: {integrity: sha512-jhcWHp0aHaL0aDYj8IJl80v4SZXWMS1A2XxXa1CA6pBiFfJKuZinCkO6wb+POAt0LIfXB3gA3AgdcOccrcwBwA==} peerDependencies: '@nomicfoundation/hardhat-chai-matchers': ^2.0.0 @@ -5193,17 +5193,17 @@ packages: typechain: ^8.3.0 typescript: '>=4.5.0' dependencies: - '@nomicfoundation/hardhat-chai-matchers': 2.0.2(@nomicfoundation/hardhat-ethers@3.0.5)(chai@4.3.10)(ethers@6.8.1)(hardhat@2.19.1) - '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.8.1)(hardhat@2.19.1) + '@nomicfoundation/hardhat-chai-matchers': 2.0.2(@nomicfoundation/hardhat-ethers@3.0.5)(chai@4.3.10)(ethers@6.10.0)(hardhat@2.19.1) + '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.10.0)(hardhat@2.19.1) '@nomicfoundation/hardhat-network-helpers': 1.0.9(hardhat@2.19.1) '@nomicfoundation/hardhat-verify': 2.0.1(hardhat@2.19.1) - '@typechain/ethers-v6': 0.5.1(ethers@6.8.1)(typechain@8.3.2)(typescript@5.3.2) - '@typechain/hardhat': 9.1.0(@typechain/ethers-v6@0.5.1)(ethers@6.8.1)(hardhat@2.19.1)(typechain@8.3.2) + '@typechain/ethers-v6': 0.5.1(ethers@6.10.0)(typechain@8.3.2)(typescript@5.3.2) + '@typechain/hardhat': 9.1.0(@typechain/ethers-v6@0.5.1)(ethers@6.10.0)(hardhat@2.19.1)(typechain@8.3.2) '@types/chai': 4.3.11 '@types/mocha': 10.0.6 '@types/node': 20.9.4 chai: 4.3.10 - ethers: 6.8.1 + ethers: 6.10.0 hardhat: 2.19.1(ts-node@10.9.1)(typescript@5.3.2) hardhat-gas-reporter: 1.0.9(hardhat@2.19.1) solidity-coverage: 0.8.5(hardhat@2.19.1) @@ -5339,6 +5339,7 @@ packages: /@nomiclabs/hardhat-etherscan@3.1.7(hardhat@2.19.1): resolution: {integrity: sha512-tZ3TvSgpvsQ6B6OGmo1/Au6u8BrAkvs1mIC/eURA3xgIfznUZBhmpne8hv7BXUzw9xNL3fXdpOYgOQlVMTcoHQ==} + deprecated: The @nomiclabs/hardhat-etherscan package is deprecated, please use @nomicfoundation/hardhat-verify instead peerDependencies: hardhat: ^2.0.4 dependencies: @@ -5562,7 +5563,7 @@ packages: - encoding dev: true - /@openzeppelin/hardhat-upgrades@3.0.5(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-verify@2.0.1)(ethers@6.8.1)(hardhat@2.19.1): + /@openzeppelin/hardhat-upgrades@3.0.5(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-verify@2.0.1)(ethers@6.10.0)(hardhat@2.19.1): resolution: {integrity: sha512-7Klg1B6fH45+7Zxzr6d9mLqudrL9Uk6CUG5AeG5NckPfP4ZlQRo1squcQ8yJPwqDS8rQjfChiqKDelp4LTjyZQ==} hasBin: true peerDependencies: @@ -5574,7 +5575,7 @@ packages: '@nomicfoundation/hardhat-verify': optional: true dependencies: - '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.8.1)(hardhat@2.19.1) + '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.10.0)(hardhat@2.19.1) '@nomicfoundation/hardhat-verify': 2.0.1(hardhat@2.19.1) '@openzeppelin/defender-admin-client': 1.52.0(debug@4.3.4) '@openzeppelin/defender-base-client': 1.52.0(debug@4.3.4) @@ -5585,7 +5586,7 @@ packages: chalk: 4.1.2 debug: 4.3.4(supports-color@8.1.1) ethereumjs-util: 7.1.5 - ethers: 6.8.1 + ethers: 6.10.0 hardhat: 2.19.1(ts-node@10.9.1)(typescript@5.3.2) proper-lockfile: 4.1.2 undici: 6.7.1 @@ -6831,21 +6832,21 @@ packages: /@turist/time@0.0.2: resolution: {integrity: sha512-qLOvfmlG2vCVw5fo/oz8WAZYlpe5a5OurgTj3diIxJCdjRHpapC+vQCz3er9LV79Vcat+DifBjeAhOAdmndtDQ==} - /@typechain/ethers-v6@0.5.1(ethers@6.8.1)(typechain@8.3.2)(typescript@5.3.2): + /@typechain/ethers-v6@0.5.1(ethers@6.10.0)(typechain@8.3.2)(typescript@5.3.2): resolution: {integrity: sha512-F+GklO8jBWlsaVV+9oHaPh5NJdd6rAKN4tklGfInX1Q7h0xPgVLP39Jl3eCulPB5qexI71ZFHwbljx4ZXNfouA==} peerDependencies: ethers: 6.x typechain: ^8.3.2 typescript: '>=4.7.0' dependencies: - ethers: 6.8.1 + ethers: 6.10.0 lodash: 4.17.21 ts-essentials: 7.0.3(typescript@5.3.2) typechain: 8.3.2(typescript@5.3.2) typescript: 5.3.2 dev: true - /@typechain/hardhat@9.1.0(@typechain/ethers-v6@0.5.1)(ethers@6.8.1)(hardhat@2.19.1)(typechain@8.3.2): + /@typechain/hardhat@9.1.0(@typechain/ethers-v6@0.5.1)(ethers@6.10.0)(hardhat@2.19.1)(typechain@8.3.2): resolution: {integrity: sha512-mtaUlzLlkqTlfPwB3FORdejqBskSnh+Jl8AIJGjXNAQfRQ4ofHADPl1+oU7Z3pAJzmZbUXII8MhOLQltcHgKnA==} peerDependencies: '@typechain/ethers-v6': ^0.5.1 @@ -6853,8 +6854,8 @@ packages: hardhat: ^2.9.9 typechain: ^8.3.2 dependencies: - '@typechain/ethers-v6': 0.5.1(ethers@6.8.1)(typechain@8.3.2)(typescript@5.3.2) - ethers: 6.8.1 + '@typechain/ethers-v6': 0.5.1(ethers@6.10.0)(typechain@8.3.2)(typescript@5.3.2) + ethers: 6.10.0 fs-extra: 9.1.0 hardhat: 2.19.1(ts-node@10.9.1)(typescript@5.3.2) typechain: 8.3.2(typescript@5.3.2) @@ -7357,7 +7358,7 @@ packages: '@typescript-eslint/type-utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) '@typescript-eslint/utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.54.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -7402,7 +7403,7 @@ packages: '@typescript-eslint/types': 6.12.0 '@typescript-eslint/typescript-estree': 6.12.0(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.54.0 typescript: 5.3.2 transitivePeerDependencies: @@ -7453,7 +7454,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.12.0(typescript@5.3.2) '@typescript-eslint/utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.54.0 ts-api-utils: 1.0.3(typescript@5.3.2) typescript: 5.3.2 @@ -7500,7 +7501,7 @@ packages: dependencies: '@typescript-eslint/types': 6.12.0 '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -8318,19 +8319,9 @@ packages: /axios@0.21.4(debug@4.3.4): resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: - follow-redirects: 1.15.3(debug@4.3.4) - transitivePeerDependencies: - - debug - - /axios@1.6.7: - resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} - dependencies: - follow-redirects: 1.15.5 - form-data: 4.0.0 - proxy-from-env: 1.1.0 + follow-redirects: 1.15.5(debug@4.3.4) transitivePeerDependencies: - debug - dev: false /axios@1.6.7(debug@4.3.4): resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} @@ -8340,7 +8331,6 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: true /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} @@ -10279,17 +10269,6 @@ packages: dependencies: ms: 2.1.3 - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -11542,7 +11521,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -11876,23 +11855,6 @@ packages: transitivePeerDependencies: - bufferutil - utf-8-validate - dev: false - - /ethers@6.8.1: - resolution: {integrity: sha512-iEKm6zox5h1lDn6scuRWdIdFJUCGg3+/aQWu0F4K0GVyEZiktFkqrJbRjTn1FlYEPz7RKA707D6g5Kdk6j7Ljg==} - engines: {node: '>=14.0.0'} - dependencies: - '@adraffy/ens-normalize': 1.10.0 - '@noble/curves': 1.2.0 - '@noble/hashes': 1.3.2 - '@types/node': 18.15.13 - aes-js: 4.0.0-beta.5 - tslib: 2.4.0 - ws: 8.5.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: true /ethjs-unit@0.1.6: resolution: {integrity: sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==} @@ -12341,27 +12303,6 @@ packages: tslib: 2.6.2 dev: false - /follow-redirects@1.15.3(debug@4.3.4): - resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dependencies: - debug: 4.3.4(supports-color@8.1.1) - - /follow-redirects@1.15.5: - resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dev: false - /follow-redirects@1.15.5(debug@4.3.4): resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} engines: {node: '>=4.0'} @@ -12372,7 +12313,6 @@ packages: optional: true dependencies: debug: 4.3.4(supports-color@8.1.1) - dev: true /follow-redirects@1.5.10: resolution: {integrity: sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==} @@ -19060,7 +19000,7 @@ packages: dependencies: command-exists: 1.2.9 commander: 3.0.2 - follow-redirects: 1.15.3(debug@4.3.4) + follow-redirects: 1.15.5(debug@4.3.4) fs-extra: 0.30.0 js-sha3: 0.8.0 memorystream: 0.3.1 @@ -22132,6 +22072,7 @@ packages: /zksync-web3@0.14.4(ethers@5.7.2): resolution: {integrity: sha512-kYehMD/S6Uhe1g434UnaMN+sBr9nQm23Ywn0EUP5BfQCsbjcr3ORuS68PosZw8xUTu3pac7G6YMSnNHk+fwzvg==} + deprecated: This package has been deprecated in favor of zksync-ethers@5.0.0 peerDependencies: ethers: ^5.7.0 dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5305eb62f..e4234ab61 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,5 @@ packages: - - core/ + - solidity/ - dapp/ - website/ - sdk/ diff --git a/sdk/package.json b/sdk/package.json index 5a6759438..e29b1f487 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@keep-network/tbtc-v2.ts": "2.4.0-dev.3", - "@acre-btc/core": "workspace:*", + "@acre-btc/contracts": "workspace:*", "ethers": "^6.10.0" } } diff --git a/sdk/src/lib/contracts/bitcoin-depositor.ts b/sdk/src/lib/contracts/bitcoin-depositor.ts index 749f78b73..f221f39ce 100644 --- a/sdk/src/lib/contracts/bitcoin-depositor.ts +++ b/sdk/src/lib/contracts/bitcoin-depositor.ts @@ -5,7 +5,7 @@ import { DepositorProxy } from "./depositor-proxy" export { DepositReceipt } from "@keep-network/tbtc-v2.ts" export type DecodedExtraData = { - staker: ChainIdentifier + depositOwner: ChainIdentifier referral: number } @@ -24,14 +24,14 @@ export interface BitcoinDepositor extends DepositorProxy { getTbtcVaultChainIdentifier(): Promise /** - * Encodes staker address and referral as extra data. - * @param staker The address to which the stBTC shares will be minted. + * Encodes deposit owner address and referral as extra data. + * @param depositOwner The address to which the stBTC shares will be minted. * @param referral Data used for referral program. */ - encodeExtraData(staker: ChainIdentifier, referral: number): Hex + encodeExtraData(depositOwner: ChainIdentifier, referral: number): Hex /** - * Decodes staker address and referral from extra data. + * Decodes depositOwner address and referral from extra data. * @param extraData Encoded extra data. */ decodeExtraData(extraData: string): DecodedExtraData diff --git a/sdk/src/lib/ethereum/bitcoin-depositor.ts b/sdk/src/lib/ethereum/bitcoin-depositor.ts index d968ee5b6..680f63808 100644 --- a/sdk/src/lib/ethereum/bitcoin-depositor.ts +++ b/sdk/src/lib/ethereum/bitcoin-depositor.ts @@ -1,5 +1,5 @@ import { packRevealDepositParameters } from "@keep-network/tbtc-v2.ts" -import { BitcoinDepositor as BitcoinDepositorTypechain } from "@acre-btc/core/typechain/contracts/BitcoinDepositor" +import { BitcoinDepositor as BitcoinDepositorTypechain } from "@acre-btc/contracts/typechain/contracts/BitcoinDepositor" import { ZeroAddress, dataSlice, @@ -84,12 +84,12 @@ class EthereumBitcoinDepositor if (!extraData) throw new Error("Invalid extra data") - const { staker, referral } = this.decodeExtraData(extraData) + const { depositOwner, referral } = this.decodeExtraData(extraData) - const tx = await this.instance.initializeStake( + const tx = await this.instance.initializeDeposit( fundingTx, reveal, - `0x${staker.identifierHex}`, + `0x${depositOwner.identifierHex}`, referral, ) @@ -98,19 +98,19 @@ class EthereumBitcoinDepositor /** * @see {BitcoinDepositor#encodeExtraData} - * @dev Packs the data to bytes32: 20 bytes of staker address and 2 bytes of + * @dev Packs the data to bytes32: 20 bytes of deposit owner address and 2 bytes of * referral, 10 bytes of trailing zeros. */ // eslint-disable-next-line class-methods-use-this - encodeExtraData(staker: ChainIdentifier, referral: number): Hex { - const stakerAddress = `0x${staker.identifierHex}` + encodeExtraData(depositOwner: ChainIdentifier, referral: number): Hex { + const depositOwnerAddress = `0x${depositOwner.identifierHex}` - if (!isAddress(stakerAddress) || stakerAddress === ZeroAddress) - throw new Error("Invalid staker address") + if (!isAddress(depositOwnerAddress) || depositOwnerAddress === ZeroAddress) + throw new Error("Invalid deposit owner address") const encodedData = solidityPacked( ["address", "uint16"], - [stakerAddress, referral], + [depositOwnerAddress, referral], ) return Hex.from(zeroPadBytes(encodedData, 32)) @@ -118,15 +118,17 @@ class EthereumBitcoinDepositor /** * @see {BitcoinDepositor#decodeExtraData} - * @dev Unpacks the data from bytes32: 20 bytes of staker address and 2 + * @dev Unpacks the data from bytes32: 20 bytes of deposit owner address and 2 * bytes of referral, 10 bytes of trailing zeros. */ // eslint-disable-next-line class-methods-use-this decodeExtraData(extraData: string): DecodedExtraData { - const staker = EthereumAddress.from(getAddress(dataSlice(extraData, 0, 20))) + const depositOwner = EthereumAddress.from( + getAddress(dataSlice(extraData, 0, 20)), + ) const referral = Number(dataSlice(extraData, 20, 22)) - return { staker, referral } + return { depositOwner, referral } } } diff --git a/sdk/src/lib/ethereum/stbtc.ts b/sdk/src/lib/ethereum/stbtc.ts index a5087f2a6..4d3b04e60 100644 --- a/sdk/src/lib/ethereum/stbtc.ts +++ b/sdk/src/lib/ethereum/stbtc.ts @@ -1,4 +1,4 @@ -import { StBTC as StBTCTypechain } from "@acre-btc/core/typechain/contracts/StBTC" +import { StBTC as StBTCTypechain } from "@acre-btc/contracts/typechain/contracts/StBTC" import stBTC from "./artifacts/sepolia/stBTC.json" import { EthersContractConfig, diff --git a/sdk/src/modules/staking/stake-initialization.ts b/sdk/src/modules/staking/stake-initialization.ts index 869de4452..9ed9b31a5 100644 --- a/sdk/src/modules/staking/stake-initialization.ts +++ b/sdk/src/modules/staking/stake-initialization.ts @@ -27,6 +27,7 @@ type StakeOptions = { backoffStepMs: BackoffRetrierParameters[1] } +// TODO: Rename to `DepositInitialization` to be consistent with the naming. /** * Represents an instance of the staking flow. Staking flow requires a few steps * which should be done to stake BTC. diff --git a/sdk/test/lib/ethereum/data.ts b/sdk/test/lib/ethereum/data.ts index 597364a21..b4deab283 100644 --- a/sdk/test/lib/ethereum/data.ts +++ b/sdk/test/lib/ethereum/data.ts @@ -3,48 +3,60 @@ import { EthereumAddress } from "../../../src" // eslint-disable-next-line import/prefer-default-export export const extraDataValidTestData: { testDescription: string - staker: EthereumAddress + depositOwner: EthereumAddress referral: number extraData: string }[] = [ { - testDescription: "staker has leading zeros", - staker: EthereumAddress.from("0x000055d85E80A49B5930C4a77975d44f012D86C1"), + testDescription: "depositOwner has leading zeros", + depositOwner: EthereumAddress.from( + "0x000055d85E80A49B5930C4a77975d44f012D86C1", + ), referral: 6851, // hex: 0x1ac3 extraData: "0x000055d85e80a49b5930c4a77975d44f012d86c11ac300000000000000000000", }, { - testDescription: "staker has trailing zeros", - staker: EthereumAddress.from("0x2d2F8BC7923F7F806Dc9bb2e17F950b42CfE0000"), + testDescription: "depositOwner has trailing zeros", + depositOwner: EthereumAddress.from( + "0x2d2F8BC7923F7F806Dc9bb2e17F950b42CfE0000", + ), referral: 6851, // hex: 0x1ac3 extraData: "0x2d2f8bc7923f7f806dc9bb2e17f950b42cfe00001ac300000000000000000000", }, { testDescription: "referral is zero", - staker: EthereumAddress.from("0xeb098d6cDE6A202981316b24B19e64D82721e89E"), + depositOwner: EthereumAddress.from( + "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + ), referral: 0, extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e000000000000000000000000", }, { testDescription: "referral has leading zeros", - staker: EthereumAddress.from("0xeb098d6cDE6A202981316b24B19e64D82721e89E"), + depositOwner: EthereumAddress.from( + "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + ), referral: 31, // hex: 0x001f extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e001f00000000000000000000", }, { testDescription: "referral has trailing zeros", - staker: EthereumAddress.from("0xeb098d6cDE6A202981316b24B19e64D82721e89E"), + depositOwner: EthereumAddress.from( + "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + ), referral: 19712, // hex: 0x4d00 extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e4d0000000000000000000000", }, { testDescription: "referral is maximum value", - staker: EthereumAddress.from("0xeb098d6cDE6A202981316b24B19e64D82721e89E"), + depositOwner: EthereumAddress.from( + "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + ), referral: 65535, // max uint16 extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89effff00000000000000000000", diff --git a/sdk/test/lib/ethereum/tbtc-depositor.test.ts b/sdk/test/lib/ethereum/tbtc-depositor.test.ts index 7368af30a..07dcf6e98 100644 --- a/sdk/test/lib/ethereum/tbtc-depositor.test.ts +++ b/sdk/test/lib/ethereum/tbtc-depositor.test.ts @@ -21,7 +21,7 @@ describe("BitcoinDepositor", () => { const mockedContractInstance = { tbtcVault: jest.fn().mockImplementation(() => vaultAddress.identifierHex), - initializeStake: jest.fn(), + initializeDeposit: jest.fn(), } let depositor: EthereumBitcoinDepositor let depositorAddress: EthereumAddress @@ -33,7 +33,7 @@ describe("BitcoinDepositor", () => { () => mockedContractInstance as unknown as Contract, ) - // TODO: get the address from artifact imported from `core` package. + // TODO: get the address from artifact imported from `solidity` package. depositorAddress = EthereumAddress.from( await ethers.Wallet.createRandom().getAddress(), ) @@ -64,7 +64,7 @@ describe("BitcoinDepositor", () => { }) describe("revealDeposit", () => { - const staker = EthereumAddress.from( + const depositOwner = EthereumAddress.from( "0x000055d85E80A49B5930C4a77975d44f012D86C1", ) const bitcoinFundingTransaction = { @@ -79,11 +79,11 @@ describe("BitcoinDepositor", () => { refundPublicKeyHash: Hex.from("28e081f285138ccbe389c1eb8985716230129f89"), blindingFactor: Hex.from("f9f0c90d00039523"), refundLocktime: Hex.from("60bcea61"), - depositor: staker, + depositor: depositOwner, } describe("when extra data is defined", () => { const extraData = { - staker, + depositOwner, referral: 6851, hex: Hex.from( "0x000055d85e80a49b5930c4a77975d44f012d86c11ac300000000000000000000", @@ -103,7 +103,7 @@ describe("BitcoinDepositor", () => { let result: Hex beforeAll(async () => { - mockedContractInstance.initializeStake.mockReturnValue({ + mockedContractInstance.initializeDeposit.mockReturnValue({ hash: mockedTx.toPrefixedString(), }) @@ -135,7 +135,7 @@ describe("BitcoinDepositor", () => { ) }) - it("should initialize stake request", () => { + it("should initialize deposit", () => { const btcTxInfo = { version: bitcoinFundingTransaction.version.toPrefixedString(), inputVector: bitcoinFundingTransaction.inputs.toPrefixedString(), @@ -154,10 +154,10 @@ describe("BitcoinDepositor", () => { vault: `0x${vaultAddress.identifierHex}`, } - expect(mockedContractInstance.initializeStake).toHaveBeenCalledWith( + expect(mockedContractInstance.initializeDeposit).toHaveBeenCalledWith( btcTxInfo, revealInfo, - `0x${staker.identifierHex}`, + `0x${depositOwner.identifierHex}`, referral, ) expect(result.toPrefixedString()).toBe(mockedTx.toPrefixedString()) @@ -184,20 +184,20 @@ describe("BitcoinDepositor", () => { it.each(extraDataValidTestData)( "$testDescription", - ({ staker, referral, extraData }) => { - const result = depositor.encodeExtraData(staker, referral) + ({ depositOwner, referral, extraData }) => { + const result = depositor.encodeExtraData(depositOwner, referral) expect(spyOnSolidityPacked).toHaveBeenCalledWith( ["address", "uint16"], - [`0x${staker.identifierHex}`, referral], + [`0x${depositOwner.identifierHex}`, referral], ) expect(result.toPrefixedString()).toEqual(extraData) }, ) - describe("when staker is zero address", () => { - const staker = EthereumAddress.from(ZeroAddress) + describe("when deposit owner is zero address", () => { + const depositOwner = EthereumAddress.from(ZeroAddress) beforeEach(() => { spyOnSolidityPacked.mockClear() @@ -205,8 +205,8 @@ describe("BitcoinDepositor", () => { it("should throw an error", () => { expect(() => { - depositor.encodeExtraData(staker, 0) - }).toThrow("Invalid staker address") + depositor.encodeExtraData(depositOwner, 0) + }).toThrow("Invalid deposit owner address") expect(spyOnSolidityPacked).not.toHaveBeenCalled() }) }) @@ -219,8 +219,12 @@ describe("BitcoinDepositor", () => { it.each(extraDataValidTestData)( "$testDescription", - ({ staker: expectedStaker, extraData, referral: expectedReferral }) => { - const { staker, referral } = depositor.decodeExtraData(extraData) + ({ + depositOwner: expectedDepositOwner, + extraData, + referral: expectedReferral, + }) => { + const { depositOwner, referral } = depositor.decodeExtraData(extraData) expect(spyOnEthersDataSlice).toHaveBeenNthCalledWith( 1, @@ -236,7 +240,7 @@ describe("BitcoinDepositor", () => { 22, ) - expect(expectedStaker.equals(staker)).toBeTruthy() + expect(expectedDepositOwner.equals(depositOwner)).toBeTruthy() expect(expectedReferral).toBe(referral) }, ) diff --git a/core/.env b/solidity/.env similarity index 100% rename from core/.env rename to solidity/.env diff --git a/core/.env.example b/solidity/.env.example similarity index 100% rename from core/.env.example rename to solidity/.env.example diff --git a/core/.eslintignore b/solidity/.eslintignore similarity index 100% rename from core/.eslintignore rename to solidity/.eslintignore diff --git a/core/.eslintrc b/solidity/.eslintrc similarity index 100% rename from core/.eslintrc rename to solidity/.eslintrc diff --git a/core/.gitignore b/solidity/.gitignore similarity index 100% rename from core/.gitignore rename to solidity/.gitignore diff --git a/core/.mocharc.json b/solidity/.mocharc.json similarity index 100% rename from core/.mocharc.json rename to solidity/.mocharc.json diff --git a/core/.nvmrc b/solidity/.nvmrc similarity index 100% rename from core/.nvmrc rename to solidity/.nvmrc diff --git a/core/.prettierignore b/solidity/.prettierignore similarity index 100% rename from core/.prettierignore rename to solidity/.prettierignore diff --git a/core/.prettierrc.js b/solidity/.prettierrc.js similarity index 100% rename from core/.prettierrc.js rename to solidity/.prettierrc.js diff --git a/core/.solhint.json b/solidity/.solhint.json similarity index 100% rename from core/.solhint.json rename to solidity/.solhint.json diff --git a/solidity/.solhintignore b/solidity/.solhintignore new file mode 100644 index 000000000..e2af04a45 --- /dev/null +++ b/solidity/.solhintignore @@ -0,0 +1,2 @@ +node_modules/ +contracts/test/ \ No newline at end of file diff --git a/core/.tsconfig-eslint.json b/solidity/.tsconfig-eslint.json similarity index 100% rename from core/.tsconfig-eslint.json rename to solidity/.tsconfig-eslint.json diff --git a/solidity/README.md b/solidity/README.md new file mode 100644 index 000000000..651236aa9 --- /dev/null +++ b/solidity/README.md @@ -0,0 +1,48 @@ +# Acre Contracts + +Acre protocol smart contracts. + +[![Solidity](https://github.com/thesis/acre/actions/workflows/solidity.yaml/badge.svg?branch=main&event=push)](https://github.com/thesis/acre/actions/workflows/solidity.yaml) + +## Development + +### Installation + +This project uses [pnpm](https://pnpm.io/) as a package manager ([installation documentation](https://pnpm.io/installation)). + +To install the dependencies execute: + +```bash +pnpm install +``` + +### Testing + +To run the test execute: + +``` +$ pnpm test +``` + +### Deploying + +We deploy our contracts with +[hardhat-deploy](https://www.npmjs.com/package/hardhat-deploy) via + +``` +$ pnpm run deploy [--network ] +``` + +Check the `"networks"` entry of `hardhat.config.ts` for supported networks. + +## Contract Addresses + +The official mainnet and testnet contract addresses are listed below. + +### Mainnet + +TBD + +### Sepolia + +TBD diff --git a/core/contracts/BitcoinDepositor.sol b/solidity/contracts/BitcoinDepositor.sol similarity index 66% rename from core/contracts/BitcoinDepositor.sol rename to solidity/contracts/BitcoinDepositor.sol index c0de3cd37..2251dbd57 100644 --- a/core/contracts/BitcoinDepositor.sol +++ b/solidity/contracts/BitcoinDepositor.sol @@ -12,13 +12,13 @@ import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol"; import {stBTC} from "./stBTC.sol"; /// @title Bitcoin Depositor contract. -/// @notice The contract integrates Acre staking with tBTC minting. -/// User who wants to stake BTC in Acre should submit a Bitcoin transaction +/// @notice The contract integrates Acre depositing with tBTC minting. +/// User who wants to deposit BTC in Acre should submit a Bitcoin transaction /// to the most recently created off-chain ECDSA wallets of the tBTC Bridge /// using pay-to-script-hash (P2SH) or pay-to-witness-script-hash (P2WSH) /// containing hashed information about this Depositor contract address, -/// and staker's Ethereum address. -/// Then, the staker initiates tBTC minting by revealing their Ethereum +/// and deposit owner's Ethereum address. +/// Then, the deposit owner initiates tBTC minting by revealing their Ethereum /// address along with their deposit blinding factor, refund public key /// hash and refund locktime on the tBTC Bridge through this Depositor /// contract. @@ -32,22 +32,29 @@ import {stBTC} from "./stBTC.sol"; /// the off-chain ECDSA wallet may decide to pick the deposit transaction /// for sweeping, and when the sweep operation is confirmed on the Bitcoin /// network, the tBTC Bridge and tBTC vault mint the tBTC token to the -/// Depositor address. After tBTC is minted to the Depositor, on the stake -/// finalization tBTC is staked in Acre and stBTC shares are emitted -/// to the staker. +/// Depositor address. After tBTC is minted to the Depositor, on the deposit +/// finalization tBTC is deposited in Acre and stBTC shares are emitted +/// to the deposit owner. contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { using SafeERC20 for IERC20; - /// @notice State of the stake request. - enum StakeRequestState { + /// @notice Reflects the deposit state: + /// - Unknown deposit has not been initialized yet. + /// - Initialized deposit has been initialized with a call to + /// `initializeDeposit` function and is known to this contract. + /// - Finalized deposit led to tBTC ERC20 minting and was finalized + /// with a call to `finalizeDeposit` function that deposited tBTC + /// to the stBTC contract. + enum DepositState { Unknown, Initialized, Finalized } - /// @notice Mapping of stake requests. - /// @dev The key is a deposit key identifying the deposit. - mapping(uint256 => StakeRequestState) public stakeRequests; + /// @notice Holds the deposit state, keyed by the deposit key calculated for + /// the individual deposit during the call to `initializeDeposit` + /// function. + mapping(uint256 => DepositState) public deposits; /// @notice tBTC Token contract. IERC20 public tbtcToken; @@ -55,13 +62,13 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// @notice stBTC contract. stBTC public stbtc; - /// @notice Minimum amount of a single stake request (in tBTC token precision). + /// @notice Minimum amount of a single deposit (in tBTC token precision). /// @dev This parameter should be set to a value exceeding the minimum deposit - /// amount supported by tBTC Bridge. - uint256 public minStakeAmount; + /// amount supported by the tBTC Bridge. + uint256 public minDepositAmount; /// @notice Divisor used to compute the depositor fee taken from each deposit - /// and transferred to the treasury upon stake request finalization. + /// and transferred to the treasury upon deposit finalization. /// @dev That fee is computed as follows: /// `depositorFee = depositedAmount / depositorFeeDivisor` /// for example, if the depositor fee needs to be 2% of each deposit, @@ -69,29 +76,29 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// `1/50 = 0.02 = 2%`. uint64 public depositorFeeDivisor; - /// @notice Emitted when a stake request is initialized. + /// @notice Emitted when a deposit is initialized. /// @dev Deposit details can be fetched from {{ Bridge.DepositRevealed }} /// event emitted in the same transaction. /// @param depositKey Deposit key identifying the deposit. - /// @param caller Address that initialized the stake request. - /// @param staker The address to which the stBTC shares will be minted. + /// @param caller Address that initialized the deposit. + /// @param depositOwner The address to which the stBTC shares will be minted. /// @param initialAmount Amount of funding transaction. - event StakeRequestInitialized( + event DepositInitialized( uint256 indexed depositKey, address indexed caller, - address indexed staker, + address indexed depositOwner, uint256 initialAmount ); - /// @notice Emitted when a stake request is finalized. + /// @notice Emitted when a deposit is finalized. /// @dev Deposit details can be fetched from {{ ERC4626.Deposit }} /// event emitted in the same transaction. /// @param depositKey Deposit key identifying the deposit. - /// @param caller Address that finalized the stake request. + /// @param caller Address that finalized the deposit. /// @param initialAmount Amount of funding transaction. /// @param bridgedAmount Amount of tBTC tokens that was bridged by the tBTC bridge. /// @param depositorFee Depositor fee amount. - event StakeRequestFinalized( + event DepositFinalized( uint256 indexed depositKey, address indexed caller, uint16 indexed referral, @@ -100,10 +107,10 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { uint256 depositorFee ); - /// @notice Emitted when a minimum single stake amount is updated. - /// @param minStakeAmount New value of the minimum single stake + /// @notice Emitted when a minimum single deposit amount is updated. + /// @param minDepositAmount New value of the minimum single deposit /// amount (in tBTC token precision). - event MinStakeAmountUpdated(uint256 minStakeAmount); + event MinDepositAmountUpdated(uint256 minDepositAmount); /// @notice Emitted when a depositor fee divisor is updated. /// @param depositorFeeDivisor New value of the depositor fee divisor. @@ -115,14 +122,13 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// Reverts if the stBTC address is zero. error StbtcZeroAddress(); - /// @dev Staker address is zero. - error StakerIsZeroAddress(); + /// @dev Deposit owner address is zero. + error DepositOwnerIsZeroAddress(); - /// @dev Attempted to execute function for stake request in unexpected current - /// state. - error UnexpectedStakeRequestState( - StakeRequestState currentState, - StakeRequestState expectedState + /// @dev Attempted to execute function for deposit in unexpected current state. + error UnexpectedDepositState( + DepositState actualState, + DepositState expectedState ); /// @dev Calculated depositor fee exceeds the amount of minted tBTC tokens. @@ -131,10 +137,10 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { uint256 bridgedAmount ); - /// @dev Attempted to set minimum stake amount to a value lower than the + /// @dev Attempted to set minimum deposit amount to a value lower than the /// tBTC Bridge deposit dust threshold. - error MinStakeAmountLowerThanBridgeMinDeposit( - uint256 minStakeAmount, + error MinDepositAmountLowerThanBridgeMinDeposit( + uint256 minDepositAmount, uint256 bridgeMinDepositAmount ); @@ -169,18 +175,18 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { stbtc = stBTC(_stbtc); // TODO: Revisit initial values before mainnet deployment. - minStakeAmount = 0.015 * 1e18; // 0.015 BTC + minDepositAmount = 0.015 * 1e18; // 0.015 BTC depositorFeeDivisor = 1000; // 1/1000 == 10bps == 0.1% == 0.001 } - /// @notice This function allows staking process initialization for a Bitcoin + /// @notice This function allows depositing process initialization for a Bitcoin /// deposit made by an user with a P2(W)SH transaction. It uses the /// supplied information to reveal a deposit to the tBTC Bridge contract. /// @dev Requirements: /// - The revealed vault address must match the TBTCVault address, /// - All requirements from {Bridge#revealDepositWithExtraData} /// function must be met. - /// - `staker` must be the staker address used in the P2(W)SH BTC + /// - `depositOwner` must be the deposit owner address used in the P2(W)SH BTC /// deposit transaction as part of the extra data. /// - `referral` must be the referral info used in the P2(W)SH BTC /// deposit transaction as part of the extra data. @@ -188,15 +194,15 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// can be revealed only one time. /// @param fundingTx Bitcoin funding transaction data, see `IBridgeTypes.BitcoinTxInfo`. /// @param reveal Deposit reveal data, see `IBridgeTypes.DepositRevealInfo`. - /// @param staker The address to which the stBTC shares will be minted. + /// @param depositOwner The address to which the stBTC shares will be minted. /// @param referral Data used for referral program. - function initializeStake( + function initializeDeposit( IBridgeTypes.BitcoinTxInfo calldata fundingTx, IBridgeTypes.DepositRevealInfo calldata reveal, - address staker, + address depositOwner, uint16 referral ) external { - if (staker == address(0)) revert StakerIsZeroAddress(); + if (depositOwner == address(0)) revert DepositOwnerIsZeroAddress(); // We don't check if the request was already initialized, as this check // is enforced in `_initializeDeposit` when calling the @@ -204,47 +210,47 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { (uint256 depositKey, uint256 initialAmount) = _initializeDeposit( fundingTx, reveal, - encodeExtraData(staker, referral) + encodeExtraData(depositOwner, referral) ); - // Validate current stake request state. - if (stakeRequests[depositKey] != StakeRequestState.Unknown) - revert UnexpectedStakeRequestState( - stakeRequests[depositKey], - StakeRequestState.Unknown + // Validate current deposit state. + if (deposits[depositKey] != DepositState.Unknown) + revert UnexpectedDepositState( + deposits[depositKey], + DepositState.Unknown ); // Transition to a new state. - stakeRequests[depositKey] = StakeRequestState.Initialized; + deposits[depositKey] = DepositState.Initialized; - emit StakeRequestInitialized( + emit DepositInitialized( depositKey, msg.sender, - staker, + depositOwner, initialAmount ); } - /// @notice This function should be called for previously initialized stake + /// @notice This function should be called for previously initialized deposit /// request, after tBTC minting process completed, meaning tBTC was /// minted to this contract. - /// @dev It calculates the amount to stake based on the approximate minted + /// @dev It calculates the amount to deposit based on the approximate minted /// tBTC amount reduced by the depositor fee. /// @dev IMPORTANT NOTE: The minted tBTC amount used by this function is an /// approximation. See documentation of the /// {{AbstractTBTCDepositor#_calculateTbtcAmount}} responsible for calculating /// this value for more details. /// @param depositKey Deposit key identifying the deposit. - function finalizeStake(uint256 depositKey) external { - // Validate current stake request state. - if (stakeRequests[depositKey] != StakeRequestState.Initialized) - revert UnexpectedStakeRequestState( - stakeRequests[depositKey], - StakeRequestState.Initialized + function finalizeDeposit(uint256 depositKey) external { + // Validate current deposit state. + if (deposits[depositKey] != DepositState.Initialized) + revert UnexpectedDepositState( + deposits[depositKey], + DepositState.Initialized ); // Transition to a new state. - stakeRequests[depositKey] = StakeRequestState.Finalized; + deposits[depositKey] = DepositState.Finalized; ( uint256 initialAmount, @@ -269,9 +275,9 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { tbtcToken.safeTransfer(stbtc.treasury(), depositorFee); } - (address staker, uint16 referral) = decodeExtraData(extraData); + (address depositOwner, uint16 referral) = decodeExtraData(extraData); - emit StakeRequestFinalized( + emit DepositFinalized( depositKey, msg.sender, referral, @@ -280,33 +286,33 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { depositorFee ); - uint256 amountToStake = tbtcAmount - depositorFee; + uint256 amountToDeposit = tbtcAmount - depositorFee; // Deposit tBTC in stBTC. - tbtcToken.safeIncreaseAllowance(address(stbtc), amountToStake); + tbtcToken.safeIncreaseAllowance(address(stbtc), amountToDeposit); // slither-disable-next-line unused-return - stbtc.deposit(amountToStake, staker); + stbtc.deposit(amountToDeposit, depositOwner); } - /// @notice Updates the minimum stake amount. + /// @notice Updates the minimum deposit amount. /// @dev It requires that the new value is greater or equal to the tBTC Bridge /// deposit dust threshold, to ensure deposit will be able to be bridged. - /// @param newMinStakeAmount New minimum stake amount (in tBTC precision). - function updateMinStakeAmount( - uint256 newMinStakeAmount + /// @param newMinDepositAmount New minimum deposit amount (in tBTC precision). + function updateMinDepositAmount( + uint256 newMinDepositAmount ) external onlyOwner { uint256 minBridgeDepositAmount = _minDepositAmount(); // Check if new value is at least equal the tBTC Bridge Deposit Dust Threshold. - if (newMinStakeAmount < minBridgeDepositAmount) - revert MinStakeAmountLowerThanBridgeMinDeposit( - newMinStakeAmount, + if (newMinDepositAmount < minBridgeDepositAmount) + revert MinDepositAmountLowerThanBridgeMinDeposit( + newMinDepositAmount, minBridgeDepositAmount ); - minStakeAmount = newMinStakeAmount; + minDepositAmount = newMinDepositAmount; - emit MinStakeAmountUpdated(newMinStakeAmount); + emit MinDepositAmountUpdated(newMinDepositAmount); } /// @notice Updates the depositor fee divisor. @@ -314,45 +320,35 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { function updateDepositorFeeDivisor( uint64 newDepositorFeeDivisor ) external onlyOwner { - // TODO: Introduce a parameters update process. depositorFeeDivisor = newDepositorFeeDivisor; emit DepositorFeeDivisorUpdated(newDepositorFeeDivisor); } - /// @notice Minimum stake amount (in tBTC token precision). - /// @dev This function should be used by dApp to check the minimum amount - /// for the stake request. - /// @dev It is not enforced in the `initializeStakeRequest` function, as - /// it is intended to be used in the dApp staking form. - function minStake() external view returns (uint256) { - return minStakeAmount; - } - - /// @notice Encodes staker address and referral as extra data. - /// @dev Packs the data to bytes32: 20 bytes of staker address and + /// @notice Encodes deposit owner address and referral as extra data. + /// @dev Packs the data to bytes32: 20 bytes of deposit owner address and /// 2 bytes of referral, 10 bytes of trailing zeros. - /// @param staker The address to which the stBTC shares will be minted. + /// @param depositOwner The address to which the stBTC shares will be minted. /// @param referral Data used for referral program. /// @return Encoded extra data. function encodeExtraData( - address staker, + address depositOwner, uint16 referral ) public pure returns (bytes32) { - return bytes32(abi.encodePacked(staker, referral)); + return bytes32(abi.encodePacked(depositOwner, referral)); } - /// @notice Decodes staker address and referral from extra data. - /// @dev Unpacks the data from bytes32: 20 bytes of staker address and + /// @notice Decodes deposit owner address and referral from extra data. + /// @dev Unpacks the data from bytes32: 20 bytes of deposit owner address and /// 2 bytes of referral, 10 bytes of trailing zeros. /// @param extraData Encoded extra data. - /// @return staker The address to which the stBTC shares will be minted. + /// @return depositOwner The address to which the stBTC shares will be minted. /// @return referral Data used for referral program. function decodeExtraData( bytes32 extraData - ) public pure returns (address staker, uint16 referral) { - // First 20 bytes of extra data is staker address. - staker = address(uint160(bytes20(extraData))); + ) public pure returns (address depositOwner, uint16 referral) { + // First 20 bytes of extra data is deposit owner address. + depositOwner = address(uint160(bytes20(extraData))); // Next 2 bytes of extra data is referral info. referral = uint16(bytes2(extraData << (8 * 20))); } diff --git a/core/contracts/BitcoinRedeemer.sol b/solidity/contracts/BitcoinRedeemer.sol similarity index 98% rename from core/contracts/BitcoinRedeemer.sol rename to solidity/contracts/BitcoinRedeemer.sol index b3c51ba18..159a81dfd 100644 --- a/core/contracts/BitcoinRedeemer.sol +++ b/solidity/contracts/BitcoinRedeemer.sol @@ -138,7 +138,7 @@ contract BitcoinRedeemer is Ownable2StepUpgradeable, IReceiveApproval { /// setup. This contract remains upgradable to have flexibility to handle /// adjustments to tBTC Bridge changes. /// @dev Redemption data should include a `redeemer` address matching the - /// address of the staker who is redeeming the shares. In case anything + /// address of the deposit owner who is redeeming the shares. In case anything /// goes wrong during the tBTC unminting process, the redeemer will be /// able to claim the tBTC tokens back from the tBTC Bank contract. /// @param owner The owner of the stBTC tokens. diff --git a/solidity/contracts/MezoAllocator.sol b/solidity/contracts/MezoAllocator.sol new file mode 100644 index 000000000..59eac6adb --- /dev/null +++ b/solidity/contracts/MezoAllocator.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {ZeroAddress} from "./utils/Errors.sol"; +import "./stBTC.sol"; +import "./interfaces/IDispatcher.sol"; + +/// @title IMezoPortal +/// @dev Interface for the Mezo's Portal contract. +interface IMezoPortal { + /// @notice DepositInfo keeps track of the deposit balance and unlock time. + /// Each deposit is tracked separately and associated with a specific + /// token. Some tokens can be deposited but can not be locked - in + /// that case the unlockAt is the block timestamp of when the deposit + /// was created. The same is true for tokens that can be locked but + /// the depositor decided not to lock them. + struct DepositInfo { + uint96 balance; + uint32 unlockAt; + } + + /// @notice Deposit and optionally lock tokens for the given period. + /// @dev Lock period will be normalized to weeks. If non-zero, it must not + /// be shorter than the minimum lock period and must not be longer than + /// the maximum lock period. + /// @param token token address to deposit + /// @param amount amount of tokens to deposit + /// @param lockPeriod lock period in seconds, 0 to not lock the deposit + function deposit(address token, uint96 amount, uint32 lockPeriod) external; + + /// @notice Withdraw deposited tokens. + /// Deposited lockable tokens can be withdrawn at any time if + /// there is no lock set on the deposit or the lock period has passed. + /// There is no way to withdraw locked deposit. Tokens that are not + /// lockable can be withdrawn at any time. Deposit can be withdrawn + /// partially. + /// @param token deposited token address + /// @param depositId id of the deposit + /// @param amount amount of the token to be withdrawn from the deposit + function withdraw(address token, uint256 depositId, uint96 amount) external; + + /// @notice The number of deposits created. Includes the deposits that + /// were fully withdrawn. This is also the identifier of the most + /// recently created deposit. + function depositCount() external view returns (uint256); + + /// @notice Get the balance and unlock time of a given deposit. + /// @param depositor depositor address + /// @param token token address to get the balance + /// @param depositId id of the deposit + function getDeposit( + address depositor, + address token, + uint256 depositId + ) external view returns (DepositInfo memory); +} + +/// @notice MezoAllocator routes tBTC to/from MezoPortal. +contract MezoAllocator is IDispatcher, Ownable2StepUpgradeable { + using SafeERC20 for IERC20; + + /// @notice Address of the MezoPortal contract. + IMezoPortal public mezoPortal; + /// @notice tBTC token contract. + IERC20 public tbtc; + /// @notice stBTC token vault contract. + stBTC public stbtc; + /// @notice Keeps track of the addresses that are allowed to trigger deposit + /// allocations. + mapping(address => bool) public isMaintainer; + /// @notice List of maintainers. + address[] public maintainers; + /// @notice keeps track of the latest deposit ID assigned in Mezo Portal. + uint256 public depositId; + /// @notice Keeps track of the total amount of tBTC allocated to MezoPortal. + uint96 public depositBalance; + + /// @notice Emitted when tBTC is deposited to MezoPortal. + event DepositAllocated( + uint256 indexed oldDepositId, + uint256 indexed newDepositId, + uint256 addedAmount, + uint256 newDepositAmount + ); + /// @notice Emitted when tBTC is withdrawn from MezoPortal. + event DepositWithdrawn(uint256 indexed depositId, uint256 amount); + /// @notice Emitted when the maintainer address is updated. + event MaintainerAdded(address indexed maintainer); + /// @notice Emitted when the maintainer address is updated. + event MaintainerRemoved(address indexed maintainer); + /// @notice Emitted when tBTC is released from MezoPortal. + event DepositReleased(uint256 indexed depositId, uint256 amount); + /// @notice Reverts if the caller is not a maintainer. + error CallerNotMaintainer(); + /// @notice Reverts if the caller is not the stBTC contract. + error CallerNotStbtc(); + /// @notice Reverts if the maintainer is not registered. + error MaintainerNotRegistered(); + /// @notice Reverts if the maintainer has been already registered. + error MaintainerAlreadyRegistered(); + + modifier onlyMaintainer() { + if (!isMaintainer[msg.sender]) { + revert CallerNotMaintainer(); + } + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the MezoAllocator contract. + /// @param _mezoPortal Address of the MezoPortal contract. + /// @param _tbtc Address of the tBTC token contract. + function initialize( + address _mezoPortal, + address _tbtc, + address _stbtc + ) public initializer { + __Ownable2Step_init(); + __Ownable_init(msg.sender); + + if (_mezoPortal == address(0)) { + revert ZeroAddress(); + } + if (_tbtc == address(0)) { + revert ZeroAddress(); + } + if (address(_stbtc) == address(0)) { + revert ZeroAddress(); + } + + mezoPortal = IMezoPortal(_mezoPortal); + tbtc = IERC20(_tbtc); + stbtc = stBTC(_stbtc); + } + + /// @notice Allocate tBTC to MezoPortal. Each allocation creates a new "rolling" + /// deposit meaning that the previous Acre's deposit is fully withdrawn + /// before a new deposit with added amount is created. This mimics a + /// "top up" functionality with the difference that a new deposit id + /// is created and the previous deposit id is no longer in use. + /// @dev This function can be invoked periodically by a maintainer. + function allocate() external onlyMaintainer { + if (depositBalance > 0) { + // Free all Acre's tBTC from MezoPortal before creating a new deposit. + // slither-disable-next-line reentrancy-no-eth + mezoPortal.withdraw(address(tbtc), depositId, depositBalance); + } + + // Fetch unallocated tBTC from stBTC contract. + uint256 addedAmount = tbtc.balanceOf(address(stbtc)); + // slither-disable-next-line arbitrary-send-erc20 + tbtc.safeTransferFrom(address(stbtc), address(this), addedAmount); + + // Create a new deposit in the MezoPortal. + depositBalance = uint96(tbtc.balanceOf(address(this))); + tbtc.forceApprove(address(mezoPortal), depositBalance); + // 0 denotes no lock period for this deposit. + mezoPortal.deposit(address(tbtc), depositBalance, 0); + uint256 oldDepositId = depositId; + // MezoPortal doesn't return depositId, so we have to read depositCounter + // which assigns depositId to the current deposit. + depositId = mezoPortal.depositCount(); + + // slither-disable-next-line reentrancy-events + emit DepositAllocated( + oldDepositId, + depositId, + addedAmount, + depositBalance + ); + } + + /// @notice Withdraws tBTC from MezoPortal and transfers it to stBTC. + /// This function can withdraw partial or a full amount of tBTC from + /// MezoPortal for a given deposit id. + /// @param amount Amount of tBTC to withdraw. + function withdraw(uint256 amount) external { + if (msg.sender != address(stbtc)) revert CallerNotStbtc(); + + emit DepositWithdrawn(depositId, amount); + mezoPortal.withdraw(address(tbtc), depositId, uint96(amount)); + // slither-disable-next-line reentrancy-benign + depositBalance -= uint96(amount); + tbtc.safeTransfer(address(stbtc), amount); + } + + /// @notice Releases deposit in full from MezoPortal. + /// @dev This is a special function that can be used to migrate funds during + /// allocator upgrade or in case of emergencies. + function releaseDeposit() external onlyOwner { + uint96 amount = mezoPortal + .getDeposit(address(this), address(tbtc), depositId) + .balance; + + emit DepositReleased(depositId, amount); + depositBalance = 0; + mezoPortal.withdraw(address(tbtc), depositId, amount); + tbtc.safeTransfer(address(stbtc), tbtc.balanceOf(address(this))); + } + + /// @notice Updates the maintainer address. + /// @param maintainerToAdd Address of the new maintainer. + function addMaintainer(address maintainerToAdd) external onlyOwner { + if (maintainerToAdd == address(0)) { + revert ZeroAddress(); + } + if (isMaintainer[maintainerToAdd]) { + revert MaintainerAlreadyRegistered(); + } + maintainers.push(maintainerToAdd); + isMaintainer[maintainerToAdd] = true; + + emit MaintainerAdded(maintainerToAdd); + } + + /// @notice Removes the maintainer address. + /// @param maintainerToRemove Address of the maintainer to remove. + function removeMaintainer(address maintainerToRemove) external onlyOwner { + if (!isMaintainer[maintainerToRemove]) { + revert MaintainerNotRegistered(); + } + delete (isMaintainer[maintainerToRemove]); + + for (uint256 i = 0; i < maintainers.length; i++) { + if (maintainers[i] == maintainerToRemove) { + maintainers[i] = maintainers[maintainers.length - 1]; + // slither-disable-next-line costly-loop + maintainers.pop(); + break; + } + } + + emit MaintainerRemoved(maintainerToRemove); + } + + /// @notice Returns the total amount of tBTC allocated to MezoPortal. + function totalAssets() external view returns (uint256) { + return depositBalance; + } + + /// @notice Returns the list of maintainers. + function getMaintainers() external view returns (address[] memory) { + return maintainers; + } +} diff --git a/core/contracts/PausableOwnable.sol b/solidity/contracts/PausableOwnable.sol similarity index 98% rename from core/contracts/PausableOwnable.sol rename to solidity/contracts/PausableOwnable.sol index 257b9d222..7e888475b 100644 --- a/core/contracts/PausableOwnable.sol +++ b/solidity/contracts/PausableOwnable.sol @@ -92,7 +92,6 @@ abstract contract PausableOwnable is /// @param newPauseAdmin New account that can trigger emergency /// stop mechanism. function updatePauseAdmin(address newPauseAdmin) external onlyOwner { - // TODO: Introduce a parameters update process. if (newPauseAdmin == address(0)) { revert ZeroAddress(); } diff --git a/core/contracts/bridge/ITBTCToken.sol b/solidity/contracts/bridge/ITBTCToken.sol similarity index 100% rename from core/contracts/bridge/ITBTCToken.sol rename to solidity/contracts/bridge/ITBTCToken.sol diff --git a/solidity/contracts/interfaces/IDispatcher.sol b/solidity/contracts/interfaces/IDispatcher.sol new file mode 100644 index 000000000..4c762240c --- /dev/null +++ b/solidity/contracts/interfaces/IDispatcher.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +/// @title IDispatcher +/// @notice Interface for the Dispatcher contract. +interface IDispatcher { + /// @notice Withdraw assets from the Dispatcher. + function withdraw(uint256 amount) external; + + /// @notice Returns the total amount of assets held by the Dispatcher. + function totalAssets() external view returns (uint256); +} diff --git a/core/contracts/lib/ERC4626Fees.sol b/solidity/contracts/lib/ERC4626Fees.sol similarity index 100% rename from core/contracts/lib/ERC4626Fees.sol rename to solidity/contracts/lib/ERC4626Fees.sol diff --git a/core/contracts/stBTC.sol b/solidity/contracts/stBTC.sol similarity index 84% rename from core/contracts/stBTC.sol rename to solidity/contracts/stBTC.sol index 857ff19a4..613c8c301 100644 --- a/core/contracts/stBTC.sol +++ b/solidity/contracts/stBTC.sol @@ -5,18 +5,17 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol"; -import "./Dispatcher.sol"; import "./PausableOwnable.sol"; import "./lib/ERC4626Fees.sol"; +import "./interfaces/IDispatcher.sol"; import {ZeroAddress} from "./utils/Errors.sol"; /// @title stBTC /// @notice This contract implements the ERC-4626 tokenized vault standard. By /// staking tBTC, users acquire a liquid staking token called stBTC, -/// commonly referred to as "shares". The staked tBTC is securely -/// deposited into Acre's vaults, where it generates yield over time. +/// commonly referred to as "shares". /// Users have the flexibility to redeem stBTC, enabling them to -/// withdraw their staked tBTC along with the accrued yield. +/// withdraw their deposited tBTC along with the accrued yield. /// @dev ERC-4626 is a standard to optimize and unify the technical parameters /// of yield-bearing vaults. This contract facilitates the minting and /// burning of shares (stBTC), which are represented as standard ERC20 @@ -25,8 +24,9 @@ import {ZeroAddress} from "./utils/Errors.sol"; contract stBTC is ERC4626Fees, PausableOwnable { using SafeERC20 for IERC20; - /// Dispatcher contract that routes tBTC from stBTC to a given vault and back. - Dispatcher public dispatcher; + /// Dispatcher contract that routes tBTC from stBTC to a given allocation + /// contract and back. + IDispatcher public dispatcher; /// Address of the treasury wallet, where fees should be transferred to. address public treasury; @@ -98,7 +98,6 @@ contract stBTC is ERC4626Fees, PausableOwnable { /// @notice Updates treasury wallet address. /// @param newTreasury New treasury wallet address. function updateTreasury(address newTreasury) external onlyOwner { - // TODO: Introduce a parameters update process. if (newTreasury == address(0)) { revert ZeroAddress(); } @@ -117,18 +116,15 @@ contract stBTC is ERC4626Fees, PausableOwnable { function updateMinimumDepositAmount( uint256 newMinimumDepositAmount ) external onlyOwner { - // TODO: Introduce a parameters update process. minimumDepositAmount = newMinimumDepositAmount; emit MinimumDepositAmountUpdated(newMinimumDepositAmount); } - // 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. + /// allowance to transfer deposited tBTC. /// @param newDispatcher Address of the new dispatcher contract. - function updateDispatcher(Dispatcher newDispatcher) external onlyOwner { + function updateDispatcher(IDispatcher newDispatcher) external onlyOwner { if (address(newDispatcher) == address(0)) { revert ZeroAddress(); } @@ -138,10 +134,6 @@ contract stBTC is ERC4626Fees, PausableOwnable { 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); @@ -151,8 +143,6 @@ contract stBTC is ERC4626Fees, PausableOwnable { IERC20(asset()).forceApprove(address(dispatcher), type(uint256).max); } - // TODO: Implement a governed upgrade process that initiates an update and - // then finalizes it after a delay. /// @notice Update the entry fee basis points. /// @param newEntryFeeBasisPoints New value of the fee basis points. function updateEntryFeeBasisPoints( @@ -163,8 +153,6 @@ contract stBTC is ERC4626Fees, PausableOwnable { emit EntryFeeBasisPointsUpdated(newEntryFeeBasisPoints); } - // TODO: Implement a governed upgrade process that initiates an update and - // then finalizes it after a delay. /// @notice Update the exit fee basis points. /// @param newExitFeeBasisPoints New value of the fee basis points. function updateExitFeeBasisPoints( @@ -175,6 +163,13 @@ contract stBTC is ERC4626Fees, PausableOwnable { emit ExitFeeBasisPointsUpdated(newExitFeeBasisPoints); } + /// @notice Returns the total amount of assets held by the vault across all + /// allocations and this contract. + function totalAssets() public view override returns (uint256) { + return + IERC20(asset()).balanceOf(address(this)) + dispatcher.totalAssets(); + } + /// @notice Calls `receiveApproval` function on spender previously approving /// the spender to withdraw from the caller multiple times, up to /// the `amount` amount. If this function is called again, it @@ -245,19 +240,43 @@ contract stBTC is ERC4626Fees, PausableOwnable { } } + /// @notice Withdraws assets from the vault and transfers them to the + /// receiver. + /// @dev Withdraw unallocated assets first and and if not enough, then pull + /// the assets from the dispatcher. + /// @param assets Amount of assets to withdraw. + /// @param receiver The address to which the assets will be transferred. + /// @param owner The address of the owner of the shares. function withdraw( uint256 assets, address receiver, address owner ) public override whenNotPaused returns (uint256) { + uint256 currentAssetsBalance = IERC20(asset()).balanceOf(address(this)); + if (assets > currentAssetsBalance) { + dispatcher.withdraw(assets - currentAssetsBalance); + } + return super.withdraw(assets, receiver, owner); } + /// @notice Redeems shares for assets and transfers them to the receiver. + /// @dev Redeem unallocated assets first and and if not enough, then pull + /// the assets from the dispatcher. + /// @param shares Amount of shares to redeem. + /// @param receiver The address to which the assets will be transferred. + /// @param owner The address of the owner of the shares. function redeem( uint256 shares, address receiver, address owner ) public override whenNotPaused returns (uint256) { + uint256 assets = convertToAssets(shares); + uint256 currentAssetsBalance = IERC20(asset()).balanceOf(address(this)); + if (assets > currentAssetsBalance) { + dispatcher.withdraw(assets - currentAssetsBalance); + } + return super.redeem(shares, receiver, owner); } diff --git a/core/contracts/test/BitcoinDepositorHarness.sol b/solidity/contracts/test/BitcoinDepositorHarness.sol similarity index 100% rename from core/contracts/test/BitcoinDepositorHarness.sol rename to solidity/contracts/test/BitcoinDepositorHarness.sol diff --git a/solidity/contracts/test/MezoPortalStub.sol b/solidity/contracts/test/MezoPortalStub.sol new file mode 100644 index 000000000..d38b86de8 --- /dev/null +++ b/solidity/contracts/test/MezoPortalStub.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract MezoPortalStub { + using SafeERC20 for IERC20; + + uint256 public depositCount; + + function withdraw( + address token, + uint256 depositId, + uint96 amount + ) external { + IERC20(token).safeTransfer(msg.sender, amount); + } + + function deposit(address token, uint96 amount, uint32 lockPeriod) external { + depositCount++; + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + } + + function getDeposit( + address depositor, + address token, + uint256 depositId + ) external view returns (uint96 balance, uint256 unlockAt) { + return ( + uint96(IERC20(token).balanceOf(address(this))), + block.timestamp + ); + } +} diff --git a/core/contracts/test/TestERC20.sol b/solidity/contracts/test/TestERC20.sol similarity index 100% rename from core/contracts/test/TestERC20.sol rename to solidity/contracts/test/TestERC20.sol diff --git a/core/contracts/test/TestTBTC.sol b/solidity/contracts/test/TestTBTC.sol similarity index 100% rename from core/contracts/test/TestTBTC.sol rename to solidity/contracts/test/TestTBTC.sol diff --git a/core/contracts/test/upgrades/BitcoinDepositorV2.sol b/solidity/contracts/test/upgrades/BitcoinDepositorV2.sol similarity index 64% rename from core/contracts/test/upgrades/BitcoinDepositorV2.sol rename to solidity/contracts/test/upgrades/BitcoinDepositorV2.sol index 106629430..85f02f3c8 100644 --- a/core/contracts/test/upgrades/BitcoinDepositorV2.sol +++ b/solidity/contracts/test/upgrades/BitcoinDepositorV2.sol @@ -18,16 +18,23 @@ import {stBTC} from "../../stBTC.sol"; contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { using SafeERC20 for IERC20; - /// @notice State of the stake request. - enum StakeRequestState { + /// @notice Reflects the deposit state: + /// - Unknown deposit has not been initialized yet. + /// - Initialized deposit has been initialized with a call to + /// `initializeDeposit` function and is known to this contract. + /// - Finalized deposit led to tBTC ERC20 minting and was finalized + /// with a call to `finalizeDeposit` function that deposited tBTC + /// to the stBTC contract. + enum DepositState { Unknown, Initialized, Finalized } - /// @notice Mapping of stake requests. - /// @dev The key is a deposit key identifying the deposit. - mapping(uint256 => StakeRequestState) public stakeRequests; + /// @notice Holds the deposit state, keyed by the deposit key calculated for + /// the individual deposit during the call to `initializeDeposit` + /// function. + mapping(uint256 => DepositState) public deposits; /// @notice tBTC Token contract. IERC20 public tbtcToken; @@ -35,13 +42,13 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// @notice stBTC contract. stBTC public stbtc; - /// @notice Minimum amount of a single stake request (in tBTC token precision). + /// @notice Minimum amount of a single deposit (in tBTC token precision). /// @dev This parameter should be set to a value exceeding the minimum deposit - /// amount supported by tBTC Bridge. - uint256 public minStakeAmount; + /// amount supported by the tBTC Bridge. + uint256 public minDepositAmount; /// @notice Divisor used to compute the depositor fee taken from each deposit - /// and transferred to the treasury upon stake request finalization. + /// and transferred to the treasury upon deposit finalization. /// @dev That fee is computed as follows: /// `depositorFee = depositedAmount / depositorFeeDivisor` /// for example, if the depositor fee needs to be 2% of each deposit, @@ -52,29 +59,29 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { // TEST: New variable; uint256 public newVariable; - /// @notice Emitted when a stake request is initialized. + /// @notice Emitted when a deposit is initialized. /// @dev Deposit details can be fetched from {{ Bridge.DepositRevealed }} /// event emitted in the same transaction. /// @param depositKey Deposit key identifying the deposit. - /// @param caller Address that initialized the stake request. - /// @param staker The address to which the stBTC shares will be minted. + /// @param caller Address that initialized the deposit. + /// @param depositOwner The address to which the stBTC shares will be minted. /// @param initialAmount Amount of funding transaction. - event StakeRequestInitialized( + event DepositInitialized( uint256 indexed depositKey, address indexed caller, - address indexed staker, + address indexed depositOwner, uint256 initialAmount ); - /// @notice Emitted when a stake request is finalized. + /// @notice Emitted when a deposit is finalized. /// @dev Deposit details can be fetched from {{ ERC4626.Deposit }} /// event emitted in the same transaction. /// @param depositKey Deposit key identifying the deposit. - /// @param caller Address that finalized the stake request. + /// @param caller Address that finalized the deposit. /// @param initialAmount Amount of funding transaction. /// @param bridgedAmount Amount of tBTC tokens that was bridged by the tBTC bridge. /// @param depositorFee Depositor fee amount. - event StakeRequestFinalized( + event DepositFinalized( uint256 indexed depositKey, address indexed caller, uint16 indexed referral, @@ -83,10 +90,10 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { uint256 depositorFee ); - /// @notice Emitted when a minimum single stake amount is updated. - /// @param minStakeAmount New value of the minimum single stake + /// @notice Emitted when a minimum single deposit amount is updated. + /// @param minDepositAmount New value of the minimum single deposit /// amount (in tBTC token precision). - event MinStakeAmountUpdated(uint256 minStakeAmount); + event MinDepositAmountUpdated(uint256 minDepositAmount); /// @notice Emitted when a depositor fee divisor is updated. /// @param depositorFeeDivisor New value of the depositor fee divisor. @@ -101,14 +108,13 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// Reverts if the stBTC address is zero. error StbtcZeroAddress(); - /// @dev Staker address is zero. - error StakerIsZeroAddress(); + /// @dev Deposit owner address is zero. + error DepositOwnerIsZeroAddress(); - /// @dev Attempted to execute function for stake request in unexpected current - /// state. - error UnexpectedStakeRequestState( - StakeRequestState currentState, - StakeRequestState expectedState + /// @dev Attempted to execute function for deposit in unexpected current state. + error UnexpectedDepositState( + DepositState actualState, + DepositState expectedState ); /// @dev Calculated depositor fee exceeds the amount of minted tBTC tokens. @@ -117,10 +123,10 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { uint256 bridgedAmount ); - /// @dev Attempted to set minimum stake amount to a value lower than the + /// @dev Attempted to set minimum deposit amount to a value lower than the /// tBTC Bridge deposit dust threshold. - error MinStakeAmountLowerThanBridgeMinDeposit( - uint256 minStakeAmount, + error MinDepositAmountLowerThanBridgeMinDeposit( + uint256 minDepositAmount, uint256 bridgeMinDepositAmount ); @@ -139,14 +145,14 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { newVariable = _newVariable; } - /// @notice This function allows staking process initialization for a Bitcoin + /// @notice This function allows depositing process initialization for a Bitcoin /// deposit made by an user with a P2(W)SH transaction. It uses the /// supplied information to reveal a deposit to the tBTC Bridge contract. /// @dev Requirements: /// - The revealed vault address must match the TBTCVault address, /// - All requirements from {Bridge#revealDepositWithExtraData} /// function must be met. - /// - `staker` must be the staker address used in the P2(W)SH BTC + /// - `depositOwner` must be the deposit owner address used in the P2(W)SH BTC /// deposit transaction as part of the extra data. /// - `referral` must be the referral info used in the P2(W)SH BTC /// deposit transaction as part of the extra data. @@ -154,15 +160,15 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// can be revealed only one time. /// @param fundingTx Bitcoin funding transaction data, see `IBridgeTypes.BitcoinTxInfo`. /// @param reveal Deposit reveal data, see `IBridgeTypes.DepositRevealInfo`. - /// @param staker The address to which the stBTC shares will be minted. + /// @param depositOwner The address to which the stBTC shares will be minted. /// @param referral Data used for referral program. - function initializeStake( + function initializeDeposit( IBridgeTypes.BitcoinTxInfo calldata fundingTx, IBridgeTypes.DepositRevealInfo calldata reveal, - address staker, + address depositOwner, uint16 referral ) external { - if (staker == address(0)) revert StakerIsZeroAddress(); + if (depositOwner == address(0)) revert DepositOwnerIsZeroAddress(); // We don't check if the request was already initialized, as this check // is enforced in `_initializeDeposit` when calling the @@ -170,47 +176,47 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { (uint256 depositKey, uint256 initialAmount) = _initializeDeposit( fundingTx, reveal, - encodeExtraData(staker, referral) + encodeExtraData(depositOwner, referral) ); - // Validate current stake request state. - if (stakeRequests[depositKey] != StakeRequestState.Unknown) - revert UnexpectedStakeRequestState( - stakeRequests[depositKey], - StakeRequestState.Unknown + // Validate current deposit state. + if (deposits[depositKey] != DepositState.Unknown) + revert UnexpectedDepositState( + deposits[depositKey], + DepositState.Unknown ); // Transition to a new state. - stakeRequests[depositKey] = StakeRequestState.Initialized; + deposits[depositKey] = DepositState.Initialized; - emit StakeRequestInitialized( + emit DepositInitialized( depositKey, msg.sender, - staker, + depositOwner, initialAmount ); } - /// @notice This function should be called for previously initialized stake + /// @notice This function should be called for previously initialized deposit /// request, after tBTC minting process completed, meaning tBTC was /// minted to this contract. - /// @dev It calculates the amount to stake based on the approximate minted + /// @dev It calculates the amount to deposit based on the approximate minted /// tBTC amount reduced by the depositor fee. /// @dev IMPORTANT NOTE: The minted tBTC amount used by this function is an /// approximation. See documentation of the /// {{AbstractTBTCDepositor#_calculateTbtcAmount}} responsible for calculating /// this value for more details. /// @param depositKey Deposit key identifying the deposit. - function finalizeStake(uint256 depositKey) external { - // Validate current stake request state. - if (stakeRequests[depositKey] != StakeRequestState.Initialized) - revert UnexpectedStakeRequestState( - stakeRequests[depositKey], - StakeRequestState.Initialized + function finalizeDeposit(uint256 depositKey) external { + // Validate current deposit state. + if (deposits[depositKey] != DepositState.Initialized) + revert UnexpectedDepositState( + deposits[depositKey], + DepositState.Initialized ); // Transition to a new state. - stakeRequests[depositKey] = StakeRequestState.Finalized; + deposits[depositKey] = DepositState.Finalized; ( uint256 initialAmount, @@ -235,9 +241,9 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { tbtcToken.safeTransfer(stbtc.treasury(), depositorFee); } - (address staker, uint16 referral) = decodeExtraData(extraData); + (address depositOwner, uint16 referral) = decodeExtraData(extraData); - emit StakeRequestFinalized( + emit DepositFinalized( depositKey, msg.sender, referral, @@ -246,33 +252,33 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { depositorFee ); - uint256 amountToStake = tbtcAmount - depositorFee; + uint256 amountToDeposit = tbtcAmount - depositorFee; // Deposit tBTC in stBTC. - tbtcToken.safeIncreaseAllowance(address(stbtc), amountToStake); + tbtcToken.safeIncreaseAllowance(address(stbtc), amountToDeposit); // slither-disable-next-line unused-return - stbtc.deposit(amountToStake, staker); + stbtc.deposit(amountToDeposit, depositOwner); } - /// @notice Updates the minimum stake amount. + /// @notice Updates the minimum deposit amount. /// @dev It requires that the new value is greater or equal to the tBTC Bridge /// deposit dust threshold, to ensure deposit will be able to be bridged. - /// @param newMinStakeAmount New minimum stake amount (in tBTC precision). - function updateMinStakeAmount( - uint256 newMinStakeAmount + /// @param newMinDepositAmount New minimum deposit amount (in tBTC precision). + function updateMinDepositAmount( + uint256 newMinDepositAmount ) external onlyOwner { uint256 minBridgeDepositAmount = _minDepositAmount(); // Check if new value is at least equal the tBTC Bridge Deposit Dust Threshold. - if (newMinStakeAmount < minBridgeDepositAmount) - revert MinStakeAmountLowerThanBridgeMinDeposit( - newMinStakeAmount, + if (newMinDepositAmount < minBridgeDepositAmount) + revert MinDepositAmountLowerThanBridgeMinDeposit( + newMinDepositAmount, minBridgeDepositAmount ); - minStakeAmount = newMinStakeAmount; + minDepositAmount = newMinDepositAmount; - emit MinStakeAmountUpdated(newMinStakeAmount); + emit MinDepositAmountUpdated(newMinDepositAmount); // TEST: Emit newly added event. emit NewEvent(); @@ -283,45 +289,35 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { function updateDepositorFeeDivisor( uint64 newDepositorFeeDivisor ) external onlyOwner { - // TODO: Introduce a parameters update process. depositorFeeDivisor = newDepositorFeeDivisor; emit DepositorFeeDivisorUpdated(newDepositorFeeDivisor); } - /// @notice Minimum stake amount (in tBTC token precision). - /// @dev This function should be used by dApp to check the minimum amount - /// for the stake request. - /// @dev It is not enforced in the `initializeStakeRequest` function, as - /// it is intended to be used in the dApp staking form. - function minStake() external view returns (uint256) { - return minStakeAmount; - } - - /// @notice Encodes staker address and referral as extra data. - /// @dev Packs the data to bytes32: 20 bytes of staker address and + /// @notice Encodes deposit owner address and referral as extra data. + /// @dev Packs the data to bytes32: 20 bytes of deposit owner address and /// 2 bytes of referral, 10 bytes of trailing zeros. - /// @param staker The address to which the stBTC shares will be minted. + /// @param depositOwner The address to which the stBTC shares will be minted. /// @param referral Data used for referral program. /// @return Encoded extra data. function encodeExtraData( - address staker, + address depositOwner, uint16 referral ) public pure returns (bytes32) { - return bytes32(abi.encodePacked(staker, referral)); + return bytes32(abi.encodePacked(depositOwner, referral)); } - /// @notice Decodes staker address and referral from extra data. - /// @dev Unpacks the data from bytes32: 20 bytes of staker address and + /// @notice Decodes deposit owner address and referral from extra data. + /// @dev Unpacks the data from bytes32: 20 bytes of deposit owner address and /// 2 bytes of referral, 10 bytes of trailing zeros. /// @param extraData Encoded extra data. - /// @return staker The address to which the stBTC shares will be minted. + /// @return depositOwner The address to which the stBTC shares will be minted. /// @return referral Data used for referral program. function decodeExtraData( bytes32 extraData - ) public pure returns (address staker, uint16 referral) { - // First 20 bytes of extra data is staker address. - staker = address(uint160(bytes20(extraData))); + ) public pure returns (address depositOwner, uint16 referral) { + // First 20 bytes of extra data is deposit owner address. + depositOwner = address(uint160(bytes20(extraData))); // Next 2 bytes of extra data is referral info. referral = uint16(bytes2(extraData << (8 * 20))); } diff --git a/solidity/contracts/test/upgrades/MezoAllocatorV2.sol b/solidity/contracts/test/upgrades/MezoAllocatorV2.sol new file mode 100644 index 000000000..2457510a4 --- /dev/null +++ b/solidity/contracts/test/upgrades/MezoAllocatorV2.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {ZeroAddress} from "../../utils/Errors.sol"; +import "../../stBTC.sol"; +import "../../interfaces/IDispatcher.sol"; + +/// @title IMezoPortal +/// @dev Interface for the Mezo's Portal contract. +interface IMezoPortal { + /// @notice DepositInfo keeps track of the deposit balance and unlock time. + /// Each deposit is tracked separately and associated with a specific + /// token. Some tokens can be deposited but can not be locked - in + /// that case the unlockAt is the block timestamp of when the deposit + /// was created. The same is true for tokens that can be locked but + /// the depositor decided not to lock them. + struct DepositInfo { + uint96 balance; + uint32 unlockAt; + } + + /// @notice Deposit and optionally lock tokens for the given period. + /// @dev Lock period will be normalized to weeks. If non-zero, it must not + /// be shorter than the minimum lock period and must not be longer than + /// the maximum lock period. + /// @param token token address to deposit + /// @param amount amount of tokens to deposit + /// @param lockPeriod lock period in seconds, 0 to not lock the deposit + function deposit(address token, uint96 amount, uint32 lockPeriod) external; + + /// @notice Withdraw deposited tokens. + /// Deposited lockable tokens can be withdrawn at any time if + /// there is no lock set on the deposit or the lock period has passed. + /// There is no way to withdraw locked deposit. Tokens that are not + /// lockable can be withdrawn at any time. Deposit can be withdrawn + /// partially. + /// @param token deposited token address + /// @param depositId id of the deposit + /// @param amount amount of the token to be withdrawn from the deposit + function withdraw(address token, uint256 depositId, uint96 amount) external; + + /// @notice The number of deposits created. Includes the deposits that + /// were fully withdrawn. This is also the identifier of the most + /// recently created deposit. + function depositCount() external view returns (uint256); + + /// @notice Get the balance and unlock time of a given deposit. + /// @param depositor depositor address + /// @param token token address to get the balance + /// @param depositId id of the deposit + function getDeposit( + address depositor, + address token, + uint256 depositId + ) external view returns (DepositInfo memory); +} + +/// @notice MezoAllocator routes tBTC to/from MezoPortal. +contract MezoAllocatorV2 is IDispatcher, Ownable2StepUpgradeable { + using SafeERC20 for IERC20; + + /// @notice Address of the MezoPortal contract. + IMezoPortal public mezoPortal; + /// @notice tBTC token contract. + IERC20 public tbtc; + /// @notice stBTC token vault contract. + stBTC public stbtc; + /// @notice Keeps track of the addresses that are allowed to trigger deposit + /// allocations. + mapping(address => bool) public isMaintainer; + /// @notice List of maintainers. + address[] public maintainers; + /// @notice keeps track of the latest deposit ID assigned in Mezo Portal. + uint256 public depositId; + /// @notice Keeps track of the total amount of tBTC allocated to MezoPortal. + uint96 public depositBalance; + + // TEST: New variable. + uint256 public newVariable; + + /// @notice Emitted when tBTC is deposited to MezoPortal. + event DepositAllocated( + uint256 indexed oldDepositId, + uint256 indexed newDepositId, + uint256 addedAmount, + uint256 newDepositAmount + ); + /// @notice Emitted when tBTC is withdrawn from MezoPortal. + event DepositWithdrawn(uint256 indexed depositId, uint256 amount); + /// @notice Emitted when the maintainer address is updated. + event MaintainerAdded(address indexed maintainer); + /// @notice Emitted when the maintainer address is updated. + event MaintainerRemoved(address indexed maintainer); + /// @notice Emitted when tBTC is released from MezoPortal. + event DepositReleased(uint256 indexed depositId, uint256 amount); + // TEST: New event. + event NewEvent(); + /// @notice Reverts if the caller is not a maintainer. + error CallerNotMaintainer(); + /// @notice Reverts if the caller is not the stBTC contract. + error CallerNotStbtc(); + /// @notice Reverts if the maintainer is already registered. + error MaintainerNotRegistered(); + /// @notice Reverts if the caller is already a maintainer. + error MaintainerAlreadyRegistered(); + + modifier onlyMaintainer() { + if (!isMaintainer[msg.sender]) { + revert CallerNotMaintainer(); + } + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address _mezoPortal, + address _tbtc, + address _stbtc + ) public initializer { + // TEST: Removed content of initialize function. Initialize shouldn't be + // called again during the upgrade because of the `initializer` + // modifier. + } + + // TEST: Initializer for V2. + function initializeV2(uint256 _newVariable) public reinitializer(2) { + newVariable = _newVariable; + } + + /// @notice Allocate tBTC to MezoPortal. Each allocation creates a new "rolling" + /// deposit meaning that the previous Acre's deposit is fully withdrawn + /// before a new deposit with added amount is created. This mimics a + /// "top up" functionality with the difference that a new deposit id + /// is created and the previous deposit id is no longer in use. + /// @dev This function can be invoked periodically by a maintainer. + function allocate() external onlyMaintainer { + if (depositBalance > 0) { + // Free all Acre's tBTC from MezoPortal before creating a new deposit. + // slither-disable-next-line reentrancy-no-eth + mezoPortal.withdraw(address(tbtc), depositId, depositBalance); + } + + // Fetch unallocated tBTC from stBTC contract. + uint256 addedAmount = tbtc.balanceOf(address(stbtc)); + // slither-disable-next-line arbitrary-send-erc20 + tbtc.safeTransferFrom(address(stbtc), address(this), addedAmount); + + // Create a new deposit in the MezoPortal. + depositBalance = uint96(tbtc.balanceOf(address(this))); + tbtc.forceApprove(address(mezoPortal), depositBalance); + // 0 denotes no lock period for this deposit. + mezoPortal.deposit(address(tbtc), depositBalance, 0); + uint256 oldDepositId = depositId; + // MezoPortal doesn't return depositId, so we have to read depositCounter + // which assigns depositId to the current deposit. + depositId = mezoPortal.depositCount(); + + // slither-disable-next-line reentrancy-events + emit DepositAllocated( + oldDepositId, + depositId, + addedAmount, + depositBalance + ); + } + + /// @notice Withdraws tBTC from MezoPortal and transfers it to stBTC. + /// This function can withdraw partial or a full amount of tBTC from + /// MezoPortal for a given deposit id. + /// @param amount Amount of tBTC to withdraw. + function withdraw(uint256 amount) external { + if (msg.sender != address(stbtc)) revert CallerNotStbtc(); + + emit DepositWithdrawn(depositId, amount); + mezoPortal.withdraw(address(tbtc), depositId, uint96(amount)); + // slither-disable-next-line reentrancy-benign + depositBalance -= uint96(amount); + tbtc.safeTransfer(address(stbtc), amount); + } + + /// @notice Releases deposit in full from MezoPortal. + /// @dev This is a special function that can be used to migrate funds during + /// allocator upgrade or in case of emergencies. + function releaseDeposit() external onlyOwner { + uint96 amount = mezoPortal + .getDeposit(address(this), address(tbtc), depositId) + .balance; + + emit DepositReleased(depositId, amount); + depositBalance = 0; + mezoPortal.withdraw(address(tbtc), depositId, amount); + tbtc.safeTransfer(address(stbtc), tbtc.balanceOf(address(this))); + } + + /// @notice Updates the maintainer address. + /// @param maintainerToAdd Address of the new maintainer. + // TEST: Modified function. + function addMaintainer(address maintainerToAdd) external onlyOwner { + if (maintainerToAdd == address(0)) { + revert ZeroAddress(); + } + if (isMaintainer[maintainerToAdd]) { + revert MaintainerAlreadyRegistered(); + } + maintainers.push(maintainerToAdd); + isMaintainer[maintainerToAdd] = true; + + emit MaintainerAdded(maintainerToAdd); + + // TEST: Emit new event. + emit NewEvent(); + } + + /// @notice Removes the maintainer address. + /// @param maintainerToRemove Address of the maintainer to remove. + function removeMaintainer(address maintainerToRemove) external onlyOwner { + if (!isMaintainer[maintainerToRemove]) { + revert MaintainerNotRegistered(); + } + delete (isMaintainer[maintainerToRemove]); + + for (uint256 i = 0; i < maintainers.length; i++) { + if (maintainers[i] == maintainerToRemove) { + maintainers[i] = maintainers[maintainers.length - 1]; + // slither-disable-next-line costly-loop + maintainers.pop(); + break; + } + } + + emit MaintainerRemoved(maintainerToRemove); + } + + /// @notice Returns the total amount of tBTC allocated to MezoPortal. + function totalAssets() external view returns (uint256) { + return depositBalance; + } + + /// @notice Returns the list of maintainers. + function getMaintainers() external view returns (address[] memory) { + return maintainers; + } +} diff --git a/core/contracts/test/upgrades/stBTCV2.sol b/solidity/contracts/test/upgrades/stBTCV2.sol similarity index 97% rename from core/contracts/test/upgrades/stBTCV2.sol rename to solidity/contracts/test/upgrades/stBTCV2.sol index 85c29e96e..69cf8e8c6 100644 --- a/core/contracts/test/upgrades/stBTCV2.sol +++ b/solidity/contracts/test/upgrades/stBTCV2.sol @@ -5,9 +5,9 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol"; -import "../../Dispatcher.sol"; import "../../PausableOwnable.sol"; import "../../lib/ERC4626Fees.sol"; +import "../../interfaces/IDispatcher.sol"; import {ZeroAddress} from "../../utils/Errors.sol"; /// @title stBTCV2 @@ -17,7 +17,7 @@ contract stBTCV2 is ERC4626Fees, PausableOwnable { using SafeERC20 for IERC20; /// Dispatcher contract that routes tBTC from stBTC to a given vault and back. - Dispatcher public dispatcher; + IDispatcher public dispatcher; /// Address of the treasury wallet, where fees should be transferred to. address public treasury; @@ -117,9 +117,9 @@ contract stBTCV2 is ERC4626Fees, PausableOwnable { // 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. + /// allowance to transfer deposited tBTC. /// @param newDispatcher Address of the new dispatcher contract. - function updateDispatcher(Dispatcher newDispatcher) external onlyOwner { + function updateDispatcher(IDispatcher newDispatcher) external onlyOwner { if (address(newDispatcher) == address(0)) { revert ZeroAddress(); } diff --git a/core/contracts/utils/Errors.sol b/solidity/contracts/utils/Errors.sol similarity index 100% rename from core/contracts/utils/Errors.sol rename to solidity/contracts/utils/Errors.sol diff --git a/solidity/deploy/00_resolve_mezo_portal.ts b/solidity/deploy/00_resolve_mezo_portal.ts new file mode 100644 index 000000000..412c468d6 --- /dev/null +++ b/solidity/deploy/00_resolve_mezo_portal.ts @@ -0,0 +1,38 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { + HardhatNetworkConfig, + HardhatRuntimeEnvironment, +} from "hardhat/types" +import { isNonZeroAddress } from "../helpers/address" +import { waitConfirmationsNumber } from "../helpers/deployment" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { log } = deployments + const { deployer } = await getNamedAccounts() + + const mezoPortal = await deployments.getOrNull("MezoPortal") + + if (mezoPortal && isNonZeroAddress(mezoPortal.address)) { + log(`using MezoPortal contract at ${mezoPortal.address}`) + } else if ( + (hre.network.config as HardhatNetworkConfig)?.forking?.enabled && + hre.network.name !== "hardhat" + ) { + throw new Error("deployed MezoPortal contract not found") + } else { + log("deploying Mezo Portal contract stub") + + await deployments.deploy("MezoPortal", { + contract: "MezoPortalStub", + args: [], + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }) + } +} + +export default func + +func.tags = ["MezoPortal"] diff --git a/core/deploy/00_resolve_tbtc_bridge.ts b/solidity/deploy/00_resolve_tbtc_bridge.ts similarity index 100% rename from core/deploy/00_resolve_tbtc_bridge.ts rename to solidity/deploy/00_resolve_tbtc_bridge.ts diff --git a/core/deploy/00_resolve_tbtc_token.ts b/solidity/deploy/00_resolve_tbtc_token.ts similarity index 100% rename from core/deploy/00_resolve_tbtc_token.ts rename to solidity/deploy/00_resolve_tbtc_token.ts diff --git a/core/deploy/00_resolve_tbtc_vault.ts b/solidity/deploy/00_resolve_tbtc_vault.ts similarity index 100% rename from core/deploy/00_resolve_tbtc_vault.ts rename to solidity/deploy/00_resolve_tbtc_vault.ts diff --git a/core/deploy/01_deploy_stbtc.ts b/solidity/deploy/01_deploy_stbtc.ts similarity index 100% rename from core/deploy/01_deploy_stbtc.ts rename to solidity/deploy/01_deploy_stbtc.ts diff --git a/solidity/deploy/02_deploy_mezo_allocator.ts b/solidity/deploy/02_deploy_mezo_allocator.ts new file mode 100644 index 000000000..694fad100 --- /dev/null +++ b/solidity/deploy/02_deploy_mezo_allocator.ts @@ -0,0 +1,36 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" +import { waitForTransaction } from "../helpers/deployment" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments, helpers } = hre + const { governance } = await getNamedAccounts() + const { deployer } = await helpers.signers.getNamedSigners() + + const tbtc = await deployments.get("TBTC") + const stbtc = await deployments.get("stBTC") + const mezoPortal = await deployments.get("MezoPortal") + + const [, deployment] = await helpers.upgrades.deployProxy("MezoAllocator", { + factoryOpts: { + signer: deployer, + }, + initializerArgs: [mezoPortal.address, tbtc.address, stbtc.address], + proxyOpts: { + kind: "transparent", + initialOwner: governance, + }, + }) + + if (deployment.transactionHash && hre.network.tags.etherscan) { + await waitForTransaction(hre, deployment.transactionHash) + await helpers.etherscan.verify(deployment) + } + + // TODO: Add Tenderly verification +} + +export default func + +func.tags = ["MezoAllocator"] +func.dependencies = ["TBTC", "stBTC", "MezoPortal"] diff --git a/core/deploy/03_deploy_bitcoin_depositor.ts b/solidity/deploy/03_deploy_bitcoin_depositor.ts similarity index 100% rename from core/deploy/03_deploy_bitcoin_depositor.ts rename to solidity/deploy/03_deploy_bitcoin_depositor.ts diff --git a/core/deploy/04_deploy_bitcoin_redeemer.ts b/solidity/deploy/04_deploy_bitcoin_redeemer.ts similarity index 100% rename from core/deploy/04_deploy_bitcoin_redeemer.ts rename to solidity/deploy/04_deploy_bitcoin_redeemer.ts diff --git a/core/deploy/11_acre_update_dispatcher.ts b/solidity/deploy/11_stbtc_update_dispatcher.ts similarity index 76% rename from core/deploy/11_acre_update_dispatcher.ts rename to solidity/deploy/11_stbtc_update_dispatcher.ts index 6ddb718d8..9fc08fc51 100644 --- a/core/deploy/11_acre_update_dispatcher.ts +++ b/solidity/deploy/11_stbtc_update_dispatcher.ts @@ -5,7 +5,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, deployments } = hre const { deployer } = await getNamedAccounts() - const dispatcher = await deployments.get("Dispatcher") + const dispatcher = await deployments.get("MezoAllocator") await deployments.execute( "stBTC", @@ -17,5 +17,5 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func -func.tags = ["AcreUpdateDispatcher"] -func.dependencies = ["stBTC", "Dispatcher"] +func.tags = ["stBTCUpdateDispatcher"] +func.dependencies = ["stBTC", "MezoAllocator"] diff --git a/core/deploy/12_dispatcher_update_maintainer.ts b/solidity/deploy/12_mezo_allocator_update_maintainer.ts similarity index 56% rename from core/deploy/12_dispatcher_update_maintainer.ts rename to solidity/deploy/12_mezo_allocator_update_maintainer.ts index 8f616fac0..3d1bd2c5f 100644 --- a/core/deploy/12_dispatcher_update_maintainer.ts +++ b/solidity/deploy/12_mezo_allocator_update_maintainer.ts @@ -1,19 +1,24 @@ import type { HardhatRuntimeEnvironment } from "hardhat/types" import type { DeployFunction } from "hardhat-deploy/types" +import { waitConfirmationsNumber } from "../helpers/deployment" 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", + "MezoAllocator", + { + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }, + "addMaintainer", maintainer, ) } export default func -func.tags = ["DispatcherUpdateMaintainer"] -func.dependencies = ["Dispatcher"] +func.dependencies = ["MezoAllocator"] +func.tags = ["MezoAllocatorAddMaintainer"] diff --git a/core/deploy/13_stbtc_update_minimum_deposit_amount.ts b/solidity/deploy/13_stbtc_update_minimum_deposit_amount.ts similarity index 100% rename from core/deploy/13_stbtc_update_minimum_deposit_amount.ts rename to solidity/deploy/13_stbtc_update_minimum_deposit_amount.ts diff --git a/core/deploy/14_update_pause_admin_stbtc.ts b/solidity/deploy/14_update_pause_admin_stbtc.ts similarity index 100% rename from core/deploy/14_update_pause_admin_stbtc.ts rename to solidity/deploy/14_update_pause_admin_stbtc.ts diff --git a/core/deploy/21_transfer_ownership_stbtc.ts b/solidity/deploy/21_transfer_ownership_stbtc.ts similarity index 100% rename from core/deploy/21_transfer_ownership_stbtc.ts rename to solidity/deploy/21_transfer_ownership_stbtc.ts diff --git a/core/deploy/23_transfer_ownership_bitcoin_depositor.ts b/solidity/deploy/23_transfer_ownership_bitcoin_depositor.ts similarity index 100% rename from core/deploy/23_transfer_ownership_bitcoin_depositor.ts rename to solidity/deploy/23_transfer_ownership_bitcoin_depositor.ts diff --git a/core/deploy/24_transfer_ownership_bitcoin_redeemer.ts b/solidity/deploy/24_transfer_ownership_bitcoin_redeemer.ts similarity index 100% rename from core/deploy/24_transfer_ownership_bitcoin_redeemer.ts rename to solidity/deploy/24_transfer_ownership_bitcoin_redeemer.ts diff --git a/core/deploy/22_transfer_ownership_dispatcher.ts b/solidity/deploy/24_transfer_ownership_mezo_allocator.ts similarity index 52% rename from core/deploy/22_transfer_ownership_dispatcher.ts rename to solidity/deploy/24_transfer_ownership_mezo_allocator.ts index 427cf388e..f6add8a43 100644 --- a/core/deploy/22_transfer_ownership_dispatcher.ts +++ b/solidity/deploy/24_transfer_ownership_mezo_allocator.ts @@ -1,24 +1,33 @@ import type { HardhatRuntimeEnvironment } from "hardhat/types" import type { DeployFunction } from "hardhat-deploy/types" +import { waitConfirmationsNumber } from "../helpers/deployment" const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, deployments } = hre const { deployer, governance } = await getNamedAccounts() const { log } = deployments - log(`transferring ownership of Dispatcher contract to ${governance}`) + log(`transferring ownership of MezoAllocator contract to ${governance}`) await deployments.execute( - "Dispatcher", - { from: deployer, log: true, waitConfirmations: 1 }, + "MezoAllocator", + { + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }, "transferOwnership", governance, ) if (hre.network.name !== "mainnet") { await deployments.execute( - "Dispatcher", - { from: governance, log: true, waitConfirmations: 1 }, + "MezoAllocator", + { + from: governance, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }, "acceptOwnership", ) } @@ -26,6 +35,5 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func -func.tags = ["TransferOwnershipDispatcher"] -func.dependencies = ["Dispatcher"] -func.runAtTheEnd = true +func.tags = ["TransferOwnershipMezoAllocator"] +func.dependencies = ["MezoAllocator"] diff --git a/core/deployments/sepolia/.chainId b/solidity/deployments/sepolia/.chainId similarity index 100% rename from core/deployments/sepolia/.chainId rename to solidity/deployments/sepolia/.chainId diff --git a/core/deployments/sepolia/BitcoinDepositor.json b/solidity/deployments/sepolia/BitcoinDepositor.json similarity index 100% rename from core/deployments/sepolia/BitcoinDepositor.json rename to solidity/deployments/sepolia/BitcoinDepositor.json diff --git a/core/deployments/sepolia/solcInputs/49f0432287e96a47d66ba17ae7bf5d96.json b/solidity/deployments/sepolia/solcInputs/49f0432287e96a47d66ba17ae7bf5d96.json similarity index 100% rename from core/deployments/sepolia/solcInputs/49f0432287e96a47d66ba17ae7bf5d96.json rename to solidity/deployments/sepolia/solcInputs/49f0432287e96a47d66ba17ae7bf5d96.json diff --git a/core/deployments/sepolia/solcInputs/b22c277b248ba02f9ec5bf62d176f9ce.json b/solidity/deployments/sepolia/solcInputs/b22c277b248ba02f9ec5bf62d176f9ce.json similarity index 100% rename from core/deployments/sepolia/solcInputs/b22c277b248ba02f9ec5bf62d176f9ce.json rename to solidity/deployments/sepolia/solcInputs/b22c277b248ba02f9ec5bf62d176f9ce.json diff --git a/core/deployments/sepolia/stBTC.json b/solidity/deployments/sepolia/stBTC.json similarity index 100% rename from core/deployments/sepolia/stBTC.json rename to solidity/deployments/sepolia/stBTC.json diff --git a/core/external/mainnet/Bridge.json b/solidity/external/mainnet/Bridge.json similarity index 100% rename from core/external/mainnet/Bridge.json rename to solidity/external/mainnet/Bridge.json diff --git a/solidity/external/mainnet/MezoPortal.json b/solidity/external/mainnet/MezoPortal.json new file mode 100644 index 000000000..07a2d5bce --- /dev/null +++ b/solidity/external/mainnet/MezoPortal.json @@ -0,0 +1,828 @@ +{ + "address": "0xAB13B8eecf5AA2460841d75da5d5D861fD5B8A39", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + } + ], + "name": "DepositLocked", + "type": "error" + }, + { + "inputs": [], + "name": "DepositNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "IncorrectAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "depositor", + "type": "address" + } + ], + "name": "IncorrectDepositor", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "lockPeriod", + "type": "uint256" + } + ], + "name": "IncorrectLockPeriod", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "enum Portal.TokenAbility", + "name": "ability", + "type": "uint8" + } + ], + "name": "IncorrectTokenAbility", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "IncorrectTokenAddress", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "depositBalance", + "type": "uint256" + } + ], + "name": "InsufficientDepositAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "name": "InsufficientTokenAbility", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "LockPeriodOutOfRange", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "newUnlockAt", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "existingUnlockAt", + "type": "uint32" + } + ], + "name": "LockPeriodTooShort", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "name": "TokenAlreadySupported", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "TokenNotSupported", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Deposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "Locked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "maxLockPeriod", + "type": "uint32" + } + ], + "name": "MaxLockPeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "minLockPeriod", + "type": "uint32" + } + ], + "name": "MinLockPeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "name": "SupportedTokenAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "internalType": "struct Portal.SupportedToken", + "name": "supportedToken", + "type": "tuple" + } + ], + "name": "addSupportedToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "depositCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "depositOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "depositFor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "deposits", + "outputs": [ + { + "internalType": "uint96", + "name": "balance", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + } + ], + "name": "getDeposit", + "outputs": [ + { + "components": [ + { + "internalType": "uint96", + "name": "balance", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + } + ], + "internalType": "struct Portal.DepositInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "internalType": "struct Portal.SupportedToken[]", + "name": "supportedTokens", + "type": "tuple[]" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "lock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "maxLockPeriod", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minLockPeriod", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "receiveApproval", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_maxLockPeriod", + "type": "uint32" + } + ], + "name": "setMaxLockPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_minLockPeriod", + "type": "uint32" + } + ], + "name": "setMinLockPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "tokenAbility", + "outputs": [ + { + "internalType": "enum Portal.TokenAbility", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "transactionHash": "0x3e13a1ece7173342ac0146c63dbfb2fb60b2badfb0991a493a868ff0db55540b", + "numDeployments": 1, + "implementation": "0xeAAf2B9e90aA6400d83e07606F5F2A5432502216", + "devdoc": "Contract deployed as upgradable proxy" +} \ No newline at end of file diff --git a/core/external/mainnet/TBTC.json b/solidity/external/mainnet/TBTC.json similarity index 100% rename from core/external/mainnet/TBTC.json rename to solidity/external/mainnet/TBTC.json diff --git a/core/external/mainnet/TBTCVault.json b/solidity/external/mainnet/TBTCVault.json similarity index 100% rename from core/external/mainnet/TBTCVault.json rename to solidity/external/mainnet/TBTCVault.json diff --git a/core/external/sepolia/Bridge.json b/solidity/external/sepolia/Bridge.json similarity index 100% rename from core/external/sepolia/Bridge.json rename to solidity/external/sepolia/Bridge.json diff --git a/solidity/external/sepolia/MezoPortal.json b/solidity/external/sepolia/MezoPortal.json new file mode 100644 index 000000000..2ba1a0e07 --- /dev/null +++ b/solidity/external/sepolia/MezoPortal.json @@ -0,0 +1,828 @@ +{ + "address": "0x6978E3e11b8Bc34ea836C1706fC742aC4Cb6b0Db", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + } + ], + "name": "DepositLocked", + "type": "error" + }, + { + "inputs": [], + "name": "DepositNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "IncorrectAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "depositor", + "type": "address" + } + ], + "name": "IncorrectDepositor", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "lockPeriod", + "type": "uint256" + } + ], + "name": "IncorrectLockPeriod", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "enum Portal.TokenAbility", + "name": "ability", + "type": "uint8" + } + ], + "name": "IncorrectTokenAbility", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "IncorrectTokenAddress", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "depositBalance", + "type": "uint256" + } + ], + "name": "InsufficientDepositAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "name": "InsufficientTokenAbility", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "LockPeriodOutOfRange", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "newUnlockAt", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "existingUnlockAt", + "type": "uint32" + } + ], + "name": "LockPeriodTooShort", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "name": "TokenAlreadySupported", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "TokenNotSupported", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Deposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "Locked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "maxLockPeriod", + "type": "uint32" + } + ], + "name": "MaxLockPeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "minLockPeriod", + "type": "uint32" + } + ], + "name": "MinLockPeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "name": "SupportedTokenAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "internalType": "struct Portal.SupportedToken", + "name": "supportedToken", + "type": "tuple" + } + ], + "name": "addSupportedToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "depositCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "depositOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "depositFor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "deposits", + "outputs": [ + { + "internalType": "uint96", + "name": "balance", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + } + ], + "name": "getDeposit", + "outputs": [ + { + "components": [ + { + "internalType": "uint96", + "name": "balance", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + } + ], + "internalType": "struct Portal.DepositInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "internalType": "struct Portal.SupportedToken[]", + "name": "supportedTokens", + "type": "tuple[]" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "lock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "maxLockPeriod", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minLockPeriod", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "receiveApproval", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_maxLockPeriod", + "type": "uint32" + } + ], + "name": "setMaxLockPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_minLockPeriod", + "type": "uint32" + } + ], + "name": "setMinLockPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "tokenAbility", + "outputs": [ + { + "internalType": "enum Portal.TokenAbility", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "transactionHash": "0xc4b6c7f1865b884f292f456a648464bfa3e5f67133d4b0a92abd11c48945fee2", + "numDeployments": 1, + "implementation": "0x7641f007de71e849c1b75A3e430e8CA13d4bF646", + "devdoc": "Contract deployed as upgradable proxy" +} \ No newline at end of file diff --git a/core/external/sepolia/TBTC.json b/solidity/external/sepolia/TBTC.json similarity index 100% rename from core/external/sepolia/TBTC.json rename to solidity/external/sepolia/TBTC.json diff --git a/core/external/sepolia/TBTCVault.json b/solidity/external/sepolia/TBTCVault.json similarity index 100% rename from core/external/sepolia/TBTCVault.json rename to solidity/external/sepolia/TBTCVault.json diff --git a/core/hardhat.config.ts b/solidity/hardhat.config.ts similarity index 100% rename from core/hardhat.config.ts rename to solidity/hardhat.config.ts diff --git a/core/helpers/address.ts b/solidity/helpers/address.ts similarity index 100% rename from core/helpers/address.ts rename to solidity/helpers/address.ts diff --git a/core/helpers/deployment.ts b/solidity/helpers/deployment.ts similarity index 100% rename from core/helpers/deployment.ts rename to solidity/helpers/deployment.ts diff --git a/core/package.json b/solidity/package.json similarity index 98% rename from core/package.json rename to solidity/package.json index 4ed08daac..6f867cbaf 100644 --- a/core/package.json +++ b/solidity/package.json @@ -1,5 +1,5 @@ { - "name": "@acre-btc/core", + "name": "@acre-btc/contracts", "version": "0.0.1", "description": "Bitcoin Liquid Staking", "license": "GPL-3.0-only", diff --git a/core/scripts/fetch_external_artifacts.sh b/solidity/scripts/fetch_external_artifacts.sh similarity index 100% rename from core/scripts/fetch_external_artifacts.sh rename to solidity/scripts/fetch_external_artifacts.sh diff --git a/core/slither.config.json b/solidity/slither.config.json similarity index 100% rename from core/slither.config.json rename to solidity/slither.config.json diff --git a/core/test/BitcoinDepositor.test.ts b/solidity/test/BitcoinDepositor.test.ts similarity index 75% rename from core/test/BitcoinDepositor.test.ts rename to solidity/test/BitcoinDepositor.test.ts index 2c0fd02f7..79b7c82aa 100644 --- a/core/test/BitcoinDepositor.test.ts +++ b/solidity/test/BitcoinDepositor.test.ts @@ -6,7 +6,7 @@ import { expect } from "chai" import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { ContractTransactionResponse, MaxUint256, ZeroAddress } from "ethers" -import { StakeRequestState } from "../types" +import { DepositState } from "../types" import type { StBTC, @@ -46,7 +46,7 @@ describe("BitcoinDepositor", () => { const initialDepositAmount = to1ePrecision(10000, 10) // 10000 satoshi const bridgedTbtcAmount = to1ePrecision(897501, 8) // 8975,01 satoshi const depositorFee = to1ePrecision(10, 10) // 10 satoshi - const amountToStake = to1ePrecision(896501, 8) // 8965,01 satoshi + const amountToDeposit = to1ePrecision(896501, 8) // 8965,01 satoshi let bitcoinDepositor: BitcoinDepositor let tbtcBridge: BridgeStub @@ -87,24 +87,27 @@ describe("BitcoinDepositor", () => { .updateDepositorFeeDivisor(defaultDepositorFeeDivisor) }) - describe("initializeStake", () => { + describe("initializeDeposit", () => { beforeAfterSnapshotWrapper() - describe("when staker is zero address", () => { + describe("when depositOwner is zero address", () => { it("should revert", async () => { await expect( - bitcoinDepositor.initializeStake( + bitcoinDepositor.initializeDeposit( tbtcDepositData.fundingTxInfo, tbtcDepositData.reveal, ZeroAddress, 0, ), - ).to.be.revertedWithCustomError(bitcoinDepositor, "StakerIsZeroAddress") + ).to.be.revertedWithCustomError( + bitcoinDepositor, + "DepositOwnerIsZeroAddress", + ) }) }) - describe("when staker is non zero address", () => { - describe("when stake is not in progress", () => { + describe("when depositOwner is non zero address", () => { + describe("when deposit is not in progress", () => { describe("when tbtc vault address is incorrect", () => { beforeAfterSnapshotWrapper() @@ -115,10 +118,10 @@ describe("BitcoinDepositor", () => { await expect( bitcoinDepositor .connect(thirdParty) - .initializeStake( + .initializeDeposit( tbtcDepositData.fundingTxInfo, { ...tbtcDepositData.reveal, vault: invalidTbtcVault }, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, tbtcDepositData.referral, ), ).to.be.revertedWith("Vault address mismatch") @@ -134,31 +137,31 @@ describe("BitcoinDepositor", () => { before(async () => { tx = await bitcoinDepositor .connect(thirdParty) - .initializeStake( + .initializeDeposit( tbtcDepositData.fundingTxInfo, tbtcDepositData.reveal, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, tbtcDepositData.referral, ) }) - it("should emit StakeRequestInitialized event", async () => { + it("should emit DepositInitialized event", async () => { await expect(tx) - .to.emit(bitcoinDepositor, "StakeRequestInitialized") + .to.emit(bitcoinDepositor, "DepositInitialized") .withArgs( tbtcDepositData.depositKey, thirdParty.address, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, initialDepositAmount, ) }) - it("should update stake state", async () => { - const stakeRequest = await bitcoinDepositor.stakeRequests( + it("should update deposit state", async () => { + const deposit = await bitcoinDepositor.deposits( tbtcDepositData.depositKey, ) - expect(stakeRequest).to.be.equal(StakeRequestState.Initialized) + expect(deposit).to.be.equal(DepositState.Initialized) }) it("should reveal the deposit to the bridge contract with extra data", async () => { @@ -185,10 +188,10 @@ describe("BitcoinDepositor", () => { await expect( bitcoinDepositor .connect(thirdParty) - .initializeStake( + .initializeDeposit( tbtcDepositData.fundingTxInfo, tbtcDepositData.reveal, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, 0, ), ).to.be.not.reverted @@ -197,49 +200,49 @@ describe("BitcoinDepositor", () => { }) }) - describe("when stake is already in progress", () => { + describe("when deposit is already in progress", () => { beforeAfterSnapshotWrapper() before(async () => { - await initializeStake() + await initializeDeposit() }) it("should revert", async () => { await expect( bitcoinDepositor .connect(thirdParty) - .initializeStake( + .initializeDeposit( tbtcDepositData.fundingTxInfo, tbtcDepositData.reveal, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, tbtcDepositData.referral, ), ).to.be.revertedWith("Deposit already revealed") }) }) - describe("when stake is already finalized", () => { + describe("when deposit is already finalized", () => { beforeAfterSnapshotWrapper() before(async () => { - await initializeStake() + await initializeDeposit() // Simulate deposit request finalization. await finalizeMinting(tbtcDepositData.depositKey) await bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey) + .finalizeDeposit(tbtcDepositData.depositKey) }) it("should revert", async () => { await expect( bitcoinDepositor .connect(thirdParty) - .initializeStake( + .initializeDeposit( tbtcDepositData.fundingTxInfo, tbtcDepositData.reveal, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, tbtcDepositData.referral, ), ).to.be.revertedWith("Deposit already revealed") @@ -248,29 +251,29 @@ describe("BitcoinDepositor", () => { }) }) - describe("finalizeStake", () => { + describe("finalizeDeposit", () => { beforeAfterSnapshotWrapper() - describe("when stake has not been initialized", () => { + describe("when deposit has not been initialized", () => { it("should revert", async () => { await expect( bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey), + .finalizeDeposit(tbtcDepositData.depositKey), ) .to.be.revertedWithCustomError( bitcoinDepositor, - "UnexpectedStakeRequestState", + "UnexpectedDepositState", ) - .withArgs(StakeRequestState.Unknown, StakeRequestState.Initialized) + .withArgs(DepositState.Unknown, DepositState.Initialized) }) }) - describe("when stake has been initialized", () => { + describe("when deposit has been initialized", () => { beforeAfterSnapshotWrapper() before(async () => { - await initializeStake() + await initializeDeposit() }) describe("when deposit was not bridged", () => { @@ -278,13 +281,13 @@ describe("BitcoinDepositor", () => { await expect( bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey), + .finalizeDeposit(tbtcDepositData.depositKey), ).to.be.revertedWith("Deposit not finalized by the bridge") }) }) describe("when deposit was bridged", () => { - describe("when stake has not been finalized", () => { + describe("when deposit has not been finalized", () => { describe("when depositor contract balance is lower than bridged amount", () => { beforeAfterSnapshotWrapper() @@ -300,7 +303,7 @@ describe("BitcoinDepositor", () => { await expect( bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey), + .finalizeDeposit(tbtcDepositData.depositKey), ) .to.be.revertedWithCustomError( stbtc, @@ -309,7 +312,7 @@ describe("BitcoinDepositor", () => { .withArgs( await bitcoinDepositor.getAddress(), mintedAmount - depositorFee, - amountToStake, + amountToDeposit, ) }) }) @@ -325,15 +328,15 @@ describe("BitcoinDepositor", () => { describe("when depositor fee divisor is not zero", () => { beforeAfterSnapshotWrapper() - const expectedAssetsAmount = amountToStake - const expectedReceivedSharesAmount = amountToStake + const expectedAssetsAmount = amountToDeposit + const expectedReceivedSharesAmount = amountToDeposit let tx: ContractTransactionResponse before(async () => { tx = await bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey) + .finalizeDeposit(tbtcDepositData.depositKey) }) it("should transfer depositor fee", async () => { @@ -344,17 +347,17 @@ describe("BitcoinDepositor", () => { ) }) - it("should update stake state", async () => { - const stakeRequest = await bitcoinDepositor.stakeRequests( + it("should update deposit state", async () => { + const depositState = await bitcoinDepositor.deposits( tbtcDepositData.depositKey, ) - expect(stakeRequest).to.be.equal(StakeRequestState.Finalized) + expect(depositState).to.be.equal(DepositState.Finalized) }) - it("should emit StakeRequestFinalized event", async () => { + it("should emit DepositFinalized event", async () => { await expect(tx) - .to.emit(bitcoinDepositor, "StakeRequestFinalized") + .to.emit(bitcoinDepositor, "DepositFinalized") .withArgs( tbtcDepositData.depositKey, thirdParty.address, @@ -370,25 +373,25 @@ describe("BitcoinDepositor", () => { .to.emit(stbtc, "Deposit") .withArgs( await bitcoinDepositor.getAddress(), - tbtcDepositData.staker, + tbtcDepositData.depositOwner, expectedAssetsAmount, expectedReceivedSharesAmount, ) }) - it("should stake in Acre contract", async () => { + it("should deposit in Acre contract", async () => { await expect( tx, "invalid minted stBTC amount", ).to.changeTokenBalances( stbtc, - [tbtcDepositData.staker], + [tbtcDepositData.depositOwner], [expectedReceivedSharesAmount], ) await expect( tx, - "invalid staked tBTC amount", + "invalid deposited tBTC amount", ).to.changeTokenBalances(tbtc, [stbtc], [expectedAssetsAmount]) }) }) @@ -396,8 +399,9 @@ describe("BitcoinDepositor", () => { describe("when depositor fee divisor is zero", () => { beforeAfterSnapshotWrapper() - const expectedAssetsAmount = amountToStake + depositorFee - const expectedReceivedSharesAmount = amountToStake + depositorFee + const expectedAssetsAmount = amountToDeposit + depositorFee + const expectedReceivedSharesAmount = + amountToDeposit + depositorFee let tx: ContractTransactionResponse @@ -408,24 +412,24 @@ describe("BitcoinDepositor", () => { tx = await bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey) + .finalizeDeposit(tbtcDepositData.depositKey) }) it("should not transfer depositor fee", async () => { await expect(tx).to.changeTokenBalances(tbtc, [treasury], [0]) }) - it("should update stake state", async () => { - const stakeRequest = await bitcoinDepositor.stakeRequests( + it("should update deposit state", async () => { + const deposit = await bitcoinDepositor.deposits( tbtcDepositData.depositKey, ) - expect(stakeRequest).to.be.equal(StakeRequestState.Finalized) + expect(deposit).to.be.equal(DepositState.Finalized) }) - it("should emit StakeRequestFinalized event", async () => { + it("should emit DepositFinalized event", async () => { await expect(tx) - .to.emit(bitcoinDepositor, "StakeRequestFinalized") + .to.emit(bitcoinDepositor, "DepositFinalized") .withArgs( tbtcDepositData.depositKey, thirdParty.address, @@ -441,25 +445,25 @@ describe("BitcoinDepositor", () => { .to.emit(stbtc, "Deposit") .withArgs( await bitcoinDepositor.getAddress(), - tbtcDepositData.staker, + tbtcDepositData.depositOwner, expectedAssetsAmount, expectedReceivedSharesAmount, ) }) - it("should stake in Acre contract", async () => { + it("should deposit in Acre contract", async () => { await expect( tx, "invalid minted stBTC amount", ).to.changeTokenBalances( stbtc, - [tbtcDepositData.staker], + [tbtcDepositData.depositOwner], [expectedReceivedSharesAmount], ) await expect( tx, - "invalid staked tBTC amount", + "invalid deposited tBTC amount", ).to.changeTokenBalances(tbtc, [stbtc], [expectedAssetsAmount]) }) }) @@ -477,7 +481,7 @@ describe("BitcoinDepositor", () => { await expect( bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey), + .finalizeDeposit(tbtcDepositData.depositKey), ) .to.be.revertedWithCustomError( bitcoinDepositor, @@ -489,40 +493,37 @@ describe("BitcoinDepositor", () => { }) }) - describe("when stake has been finalized", () => { + describe("when deposit has been finalized", () => { beforeAfterSnapshotWrapper() before(async () => { // Simulate deposit request finalization. await finalizeMinting(tbtcDepositData.depositKey) - // Finalize stake. + // Finalize deposit. await bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey) + .finalizeDeposit(tbtcDepositData.depositKey) }) it("should revert", async () => { await expect( bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey), + .finalizeDeposit(tbtcDepositData.depositKey), ) .to.be.revertedWithCustomError( bitcoinDepositor, - "UnexpectedStakeRequestState", - ) - .withArgs( - StakeRequestState.Finalized, - StakeRequestState.Initialized, + "UnexpectedDepositState", ) + .withArgs(DepositState.Finalized, DepositState.Initialized) }) }) }) }) }) - describe("updateMinStakeAmount", () => { + describe("updateMinDepositAmount", () => { beforeAfterSnapshotWrapper() describe("when caller is not governance", () => { @@ -530,7 +531,7 @@ describe("BitcoinDepositor", () => { it("should revert", async () => { await expect( - bitcoinDepositor.connect(thirdParty).updateMinStakeAmount(1234), + bitcoinDepositor.connect(thirdParty).updateMinDepositAmount(1234), ) .to.be.revertedWithCustomError( bitcoinDepositor, @@ -541,7 +542,7 @@ describe("BitcoinDepositor", () => { }) describe("when caller is governance", () => { - const testUpdateMinStakeAmount = (newValue: bigint) => + const testupdateMinDepositAmount = (newValue: bigint) => function () { beforeAfterSnapshotWrapper() @@ -550,17 +551,17 @@ describe("BitcoinDepositor", () => { before(async () => { tx = await bitcoinDepositor .connect(governance) - .updateMinStakeAmount(newValue) + .updateMinDepositAmount(newValue) }) - it("should emit MinStakeAmountUpdated event", async () => { + it("should emit MinDepositAmountUpdated event", async () => { await expect(tx) - .to.emit(bitcoinDepositor, "MinStakeAmountUpdated") + .to.emit(bitcoinDepositor, "MinDepositAmountUpdated") .withArgs(newValue) }) it("should update value correctly", async () => { - expect(await bitcoinDepositor.minStakeAmount()).to.be.eq(newValue) + expect(await bitcoinDepositor.minDepositAmount()).to.be.eq(newValue) }) } @@ -568,44 +569,44 @@ describe("BitcoinDepositor", () => { // Deposit dust threshold: 1000000 satoshi = 0.01 BTC // tBTC Bridge stores the dust threshold in satoshi precision, - // we need to convert it to the tBTC token precision as `updateMinStakeAmount` + // we need to convert it to the tBTC token precision as `updateMinDepositAmount` // function expects this precision. const bridgeDepositDustThreshold = to1ePrecision( defaultDepositDustThreshold, 10, ) - describe("when new stake amount is less than bridge deposit dust threshold", () => { + describe("when new deposit amount is less than bridge deposit dust threshold", () => { beforeAfterSnapshotWrapper() - const newMinStakeAmount = bridgeDepositDustThreshold - 1n + const newMinDepositAmount = bridgeDepositDustThreshold - 1n it("should revert", async () => { await expect( bitcoinDepositor .connect(governance) - .updateMinStakeAmount(newMinStakeAmount), + .updateMinDepositAmount(newMinDepositAmount), ) .to.be.revertedWithCustomError( bitcoinDepositor, - "MinStakeAmountLowerThanBridgeMinDeposit", + "MinDepositAmountLowerThanBridgeMinDeposit", ) - .withArgs(newMinStakeAmount, bridgeDepositDustThreshold) + .withArgs(newMinDepositAmount, bridgeDepositDustThreshold) }) }) describe( - "when new stake amount is equal to bridge deposit dust threshold", - testUpdateMinStakeAmount(bridgeDepositDustThreshold), + "when new deposit amount is equal to bridge deposit dust threshold", + testupdateMinDepositAmount(bridgeDepositDustThreshold), ) describe( - "when new stake amount is greater than bridge deposit dust threshold", - testUpdateMinStakeAmount(bridgeDepositDustThreshold + 1n), + "when new deposit amount is greater than bridge deposit dust threshold", + testupdateMinDepositAmount(bridgeDepositDustThreshold + 1n), ) describe( - "when new stake amount is equal max uint256", - testUpdateMinStakeAmount(MaxUint256), + "when new deposit amount is equal max uint256", + testupdateMinDepositAmount(MaxUint256), ) }) }) @@ -669,24 +670,24 @@ describe("BitcoinDepositor", () => { const extraDataValidTestData = new Map< string, { - staker: string + depositOwner: string referral: number extraData: string } >([ [ - "staker has leading zeros", + "depositOwner has leading zeros", { - staker: "0x000055d85E80A49B5930C4a77975d44f012D86C1", + depositOwner: "0x000055d85E80A49B5930C4a77975d44f012D86C1", referral: 6851, // hex: 0x1ac3 extraData: "0x000055d85e80a49b5930c4a77975d44f012d86c11ac300000000000000000000", }, ], [ - "staker has trailing zeros", + "depositOwner has trailing zeros", { - staker: "0x2d2F8BC7923F7F806Dc9bb2e17F950b42CfE0000", + depositOwner: "0x2d2F8BC7923F7F806Dc9bb2e17F950b42CfE0000", referral: 6851, // hex: 0x1ac3 extraData: "0x2d2f8bc7923f7f806dc9bb2e17f950b42cfe00001ac300000000000000000000", @@ -695,7 +696,7 @@ describe("BitcoinDepositor", () => { [ "referral is zero", { - staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + depositOwner: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", referral: 0, extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e000000000000000000000000", @@ -704,7 +705,7 @@ describe("BitcoinDepositor", () => { [ "referral has leading zeros", { - staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + depositOwner: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", referral: 31, // hex: 0x001f extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e001f00000000000000000000", @@ -713,7 +714,7 @@ describe("BitcoinDepositor", () => { [ "referral has trailing zeros", { - staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + depositOwner: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", referral: 19712, // hex: 0x4d00 extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e4d0000000000000000000000", @@ -722,7 +723,7 @@ describe("BitcoinDepositor", () => { [ "referral is maximum value", { - staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + depositOwner: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", referral: 65535, // max uint16 extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89effff00000000000000000000", @@ -733,10 +734,10 @@ describe("BitcoinDepositor", () => { describe("encodeExtraData", () => { extraDataValidTestData.forEach( // eslint-disable-next-line @typescript-eslint/no-shadow - ({ staker, referral, extraData: expectedExtraData }, testName) => { + ({ depositOwner, referral, extraData: expectedExtraData }, testName) => { it(testName, async () => { expect( - await bitcoinDepositor.encodeExtraData(staker, referral), + await bitcoinDepositor.encodeExtraData(depositOwner, referral), ).to.be.equal(expectedExtraData) }) }, @@ -746,14 +747,20 @@ describe("BitcoinDepositor", () => { describe("decodeExtraData", () => { extraDataValidTestData.forEach( ( - { staker: expectedStaker, referral: expectedReferral, extraData }, + { + depositOwner: expoectedDepositOwner, + referral: expectedReferral, + extraData, + }, testName, ) => { it(testName, async () => { - const [actualStaker, actualReferral] = + const [actualDepositOwner, actualReferral] = await bitcoinDepositor.decodeExtraData(extraData) - expect(actualStaker, "invalid staker").to.be.equal(expectedStaker) + expect(actualDepositOwner, "invalid depositOwner").to.be.equal( + expoectedDepositOwner, + ) expect(actualReferral, "invalid referral").to.be.equal( expectedReferral, ) @@ -767,24 +774,26 @@ describe("BitcoinDepositor", () => { // value. const extraData = "0xeb098d6cde6a202981316b24b19e64d82721e89e1ac3105f9919321ea7d75f58" - const expectedStaker = "0xeb098d6cDE6A202981316b24B19e64D82721e89E" + const expectedDepositOwner = "0xeb098d6cDE6A202981316b24B19e64D82721e89E" const expectedReferral = 6851 // hex: 0x1ac3 - const [actualStaker, actualReferral] = + const [actualDepositOwner, actualReferral] = await bitcoinDepositor.decodeExtraData(extraData) - expect(actualStaker, "invalid staker").to.be.equal(expectedStaker) + expect(actualDepositOwner, "invalid depositOwner").to.be.equal( + expectedDepositOwner, + ) expect(actualReferral, "invalid referral").to.be.equal(expectedReferral) }) }) - async function initializeStake() { + async function initializeDeposit() { await bitcoinDepositor .connect(thirdParty) - .initializeStake( + .initializeDeposit( tbtcDepositData.fundingTxInfo, tbtcDepositData.reveal, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, tbtcDepositData.referral, ) } diff --git a/core/test/BitcoinDepositor.upgrade.test.ts b/solidity/test/BitcoinDepositor.upgrade.test.ts similarity index 88% rename from core/test/BitcoinDepositor.upgrade.test.ts rename to solidity/test/BitcoinDepositor.upgrade.test.ts index 46ea0155c..6194712d5 100644 --- a/core/test/BitcoinDepositor.upgrade.test.ts +++ b/solidity/test/BitcoinDepositor.upgrade.test.ts @@ -42,19 +42,19 @@ describe("BitcoinDepositor contract upgrade", () => { const newVariable = 1n let bitcoinDepositorV2: BitcoinDepositorV2 let v1InitialParameters: { - minStakeAmount: bigint + minDepositAmount: bigint depositorFeeDivisor: bigint } beforeAfterSnapshotWrapper() before(async () => { - const minStakeAmount = await bitcoinDepositor.minStakeAmount() + const minDepositAmount = await bitcoinDepositor.minDepositAmount() const depositorFeeDivisor = await bitcoinDepositor.depositorFeeDivisor() v1InitialParameters = { - minStakeAmount, + minDepositAmount, depositorFeeDivisor, } @@ -98,8 +98,8 @@ describe("BitcoinDepositor contract upgrade", () => { ) expect(await bitcoinDepositorV2.stbtc()).to.eq(await stbtc.getAddress()) - expect(await bitcoinDepositorV2.minStakeAmount()).to.eq( - v1InitialParameters.minStakeAmount, + expect(await bitcoinDepositorV2.minDepositAmount()).to.eq( + v1InitialParameters.minDepositAmount, ) expect(await bitcoinDepositorV2.depositorFeeDivisor()).to.eq( v1InitialParameters.depositorFeeDivisor, @@ -107,14 +107,14 @@ describe("BitcoinDepositor contract upgrade", () => { }) }) - describe("upgraded `updateMinStakeAmount` function", () => { - const newMinStakeAmount: bigint = to1e18(1000) + describe("upgraded `updateMinDepositAmount` function", () => { + const newMinDepositAmount: bigint = to1e18(1000) let tx: ContractTransactionResponse before(async () => { tx = await bitcoinDepositorV2 .connect(governance) - .updateMinStakeAmount(newMinStakeAmount) + .updateMinDepositAmount(newMinDepositAmount) }) it("should emit `NewEvent` event", async () => { diff --git a/core/test/BitcoinRedeemer.test.ts b/solidity/test/BitcoinRedeemer.test.ts similarity index 95% rename from core/test/BitcoinRedeemer.test.ts rename to solidity/test/BitcoinRedeemer.test.ts index 2874cf9cd..396391945 100644 --- a/core/test/BitcoinRedeemer.test.ts +++ b/solidity/test/BitcoinRedeemer.test.ts @@ -133,12 +133,7 @@ describe("BitcoinRedeemer", () => { to1e18(1), tbtcRedemptionData.redemptionData, ), - ) - .to.be.revertedWithCustomError( - stbtc, - "ERC4626ExceededMaxRedeem", - ) - .withArgs(await depositor.getAddress(), to1e18(1), 0) + ).to.be.revertedWithCustomError(stbtc, "ERC20InsufficientBalance") }) }) @@ -170,16 +165,10 @@ describe("BitcoinRedeemer", () => { amountToRedeem, tbtcRedemptionData.redemptionData, ), + ).to.be.revertedWithCustomError( + stbtc, + "ERC20InsufficientBalance", ) - .to.be.revertedWithCustomError( - stbtc, - "ERC4626ExceededMaxRedeem", - ) - .withArgs( - await depositor.getAddress(), - amountToRedeem, - depositAmount, - ) }) }) diff --git a/core/test/Deployment.test.ts b/solidity/test/Deployment.test.ts similarity index 75% rename from core/test/Deployment.test.ts rename to solidity/test/Deployment.test.ts index 5914e98bb..d318d9652 100644 --- a/core/test/Deployment.test.ts +++ b/solidity/test/Deployment.test.ts @@ -6,30 +6,30 @@ import { helpers } from "hardhat" import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { deployment } from "./helpers/context" -import type { StBTC as stBTC, Dispatcher, TestERC20 } from "../typechain" +import type { StBTC as stBTC, TestERC20, MezoAllocator } from "../typechain" const { getNamedSigners } = helpers.signers async function fixture() { - const { tbtc, stbtc, dispatcher } = await deployment() + const { tbtc, stbtc, mezoAllocator } = await deployment() const { governance, maintainer, treasury } = await getNamedSigners() - return { stbtc, dispatcher, tbtc, governance, maintainer, treasury } + return { stbtc, mezoAllocator, tbtc, governance, maintainer, treasury } } describe("Deployment", () => { let stbtc: stBTC - let dispatcher: Dispatcher + let mezoAllocator: MezoAllocator let tbtc: TestERC20 let maintainer: HardhatEthersSigner let treasury: HardhatEthersSigner before(async () => { - ;({ stbtc, dispatcher, tbtc, maintainer, treasury } = + ;({ stbtc, mezoAllocator, tbtc, maintainer, treasury } = await loadFixture(fixture)) }) - describe("Acre", () => { + describe("stBTC", () => { describe("constructor", () => { context("when treasury has been set", () => { it("should be set to a treasury address", async () => { @@ -45,7 +45,7 @@ describe("Deployment", () => { it("should be set to a dispatcher address by the deployment script", async () => { const actualDispatcher = await stbtc.dispatcher() - expect(actualDispatcher).to.be.equal(await dispatcher.getAddress()) + expect(actualDispatcher).to.be.equal(await mezoAllocator.getAddress()) }) it("should approve max amount for the dispatcher", async () => { @@ -61,13 +61,13 @@ describe("Deployment", () => { }) }) - describe("Dispatcher", () => { + describe("MezoAllocator", () => { 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() + const isMaintainer = await mezoAllocator.isMaintainer(maintainer) - expect(actualMaintainer).to.be.equal(await maintainer.getAddress()) + expect(isMaintainer).to.be.equal(true) }) }) }) diff --git a/solidity/test/MezoAllocator.test.ts b/solidity/test/MezoAllocator.test.ts new file mode 100644 index 000000000..23467a497 --- /dev/null +++ b/solidity/test/MezoAllocator.test.ts @@ -0,0 +1,443 @@ +import { helpers } 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 { beforeAfterSnapshotWrapper, deployment } from "./helpers" + +import { + StBTC as stBTC, + TestERC20, + MezoAllocator, + IMezoPortal, +} from "../typechain" + +import { to1e18 } from "./utils" + +const { getNamedSigners, getUnnamedSigners } = helpers.signers + +async function fixture() { + const { tbtc, stbtc, mezoAllocator, mezoPortal } = await deployment() + const { governance, maintainer } = await getNamedSigners() + const [depositor, thirdParty] = await getUnnamedSigners() + + return { + governance, + thirdParty, + depositor, + maintainer, + tbtc, + stbtc, + mezoAllocator, + mezoPortal, + } +} + +describe("MezoAllocator", () => { + let tbtc: TestERC20 + let stbtc: stBTC + let mezoAllocator: MezoAllocator + let mezoPortal: IMezoPortal + + let thirdParty: HardhatEthersSigner + let depositor: HardhatEthersSigner + let maintainer: HardhatEthersSigner + let governance: HardhatEthersSigner + + before(async () => { + ;({ + thirdParty, + depositor, + maintainer, + governance, + tbtc, + stbtc, + mezoAllocator, + mezoPortal, + } = await loadFixture(fixture)) + }) + + describe("allocate", () => { + beforeAfterSnapshotWrapper() + + context("when a caller is not a maintainer", () => { + it("should revert", async () => { + await expect( + mezoAllocator.connect(thirdParty).allocate(), + ).to.be.revertedWithCustomError(mezoAllocator, "CallerNotMaintainer") + }) + }) + + context("when the caller is maintainer", () => { + context("when a first deposit is made", () => { + let tx: ContractTransactionResponse + + before(async () => { + await tbtc.mint(await stbtc.getAddress(), to1e18(6)) + tx = await mezoAllocator.connect(maintainer).allocate() + }) + + it("should deposit and transfer tBTC to Mezo Portal", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [await mezoPortal.getAddress()], + [to1e18(6)], + ) + }) + + it("should not store any tBTC in Mezo Allocator", async () => { + expect( + await tbtc.balanceOf(await mezoAllocator.getAddress()), + ).to.equal(0) + }) + + it("should increment the deposit id", async () => { + const actualDepositId = await mezoAllocator.depositId() + expect(actualDepositId).to.equal(1) + }) + + it("should increase tracked deposit balance amount", async () => { + const depositBalance = await mezoAllocator.depositBalance() + expect(depositBalance).to.equal(to1e18(6)) + }) + + it("should emit DepositAllocated event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "DepositAllocated") + .withArgs(0, 1, to1e18(6), to1e18(6)) + }) + }) + + context("when a second deposit is made", () => { + let tx: ContractTransactionResponse + + before(async () => { + await tbtc.mint(await stbtc.getAddress(), to1e18(5)) + + tx = await mezoAllocator.connect(maintainer).allocate() + }) + + it("should increment the deposit id", async () => { + const actualDepositId = await mezoAllocator.depositId() + expect(actualDepositId).to.equal(2) + }) + + it("should emit DepositAllocated event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "DepositAllocated") + .withArgs(1, 2, to1e18(5), to1e18(11)) + }) + + it("should deposit and transfer tBTC to Mezo Portal", async () => { + expect(await tbtc.balanceOf(await mezoPortal.getAddress())).to.equal( + to1e18(11), + ) + }) + + it("should increase tracked deposit balance amount", async () => { + const depositBalance = await mezoAllocator.depositBalance() + expect(depositBalance).to.equal(to1e18(11)) + }) + + it("should not store any tBTC in Mezo Allocator", async () => { + expect( + await tbtc.balanceOf(await mezoAllocator.getAddress()), + ).to.equal(0) + }) + + it("should not store any tBTC in stBTC", async () => { + expect(await tbtc.balanceOf(await stbtc.getAddress())).to.equal(0) + }) + }) + }) + }) + + describe("withdraw", () => { + beforeAfterSnapshotWrapper() + + context("when a caller is not stBTC", () => { + it("should revert", async () => { + await expect( + mezoAllocator.connect(thirdParty).withdraw(1n), + ).to.be.revertedWithCustomError(mezoAllocator, "CallerNotStbtc") + }) + }) + + context("when the caller is stBTC contract", () => { + context("when there is no deposit", () => { + it("should revert", async () => { + await expect(stbtc.withdraw(1n, depositor, depositor)) + .to.be.revertedWithCustomError(tbtc, "ERC20InsufficientBalance") + .withArgs(await mezoPortal.getAddress(), 0, 1n) + }) + }) + + context("when there is a deposit", () => { + let tx: ContractTransactionResponse + + before(async () => { + await tbtc.mint(depositor, to1e18(5)) + await tbtc.approve(await stbtc.getAddress(), to1e18(5)) + await stbtc.connect(depositor).deposit(to1e18(5), depositor) + await mezoAllocator.connect(maintainer).allocate() + }) + + context("when the deposit is not fully withdrawn", () => { + before(async () => { + tx = await stbtc.withdraw(to1e18(2), depositor, depositor) + }) + + it("should transfer 2 tBTC back to a depositor", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [depositor.address], + [to1e18(2)], + ) + }) + + it("should emit DepositWithdrawn event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "DepositWithdrawn") + .withArgs(1, to1e18(2)) + }) + + it("should decrease tracked deposit balance amount", async () => { + const depositBalance = await mezoAllocator.depositBalance() + expect(depositBalance).to.equal(to1e18(3)) + }) + + it("should decrease Mezo Portal balance", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [await mezoPortal.getAddress()], + [-to1e18(2)], + ) + }) + }) + + context("when the deposit is fully withdrawn", () => { + before(async () => { + tx = await stbtc.withdraw(to1e18(3), depositor, depositor) + }) + + it("should transfer 3 tBTC back to a depositor", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [depositor.address], + [to1e18(3)], + ) + }) + + it("should emit DepositWithdrawn event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "DepositWithdrawn") + .withArgs(1, to1e18(3)) + }) + + it("should decrease tracked deposit balance amount to zero", async () => { + const depositBalance = await mezoAllocator.depositBalance() + expect(depositBalance).to.equal(0) + }) + + it("should decrease Mezo Portal balance", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [await mezoPortal.getAddress()], + [-to1e18(3)], + ) + }) + }) + }) + }) + }) + + describe("totalAssets", () => { + beforeAfterSnapshotWrapper() + + context("when there is no deposit", () => { + it("should return 0", async () => { + const totalAssets = await mezoAllocator.totalAssets() + expect(totalAssets).to.equal(0) + }) + }) + + context("when there is a deposit", () => { + before(async () => { + await tbtc.mint(await stbtc.getAddress(), to1e18(5)) + await mezoAllocator.connect(maintainer).allocate() + }) + + it("should return the total assets value", async () => { + const totalAssets = await mezoAllocator.totalAssets() + expect(totalAssets).to.equal(to1e18(5)) + }) + }) + }) + + describe("addMaintainer", () => { + beforeAfterSnapshotWrapper() + + context("when a caller is not a governance", () => { + it("should revert", async () => { + await expect( + mezoAllocator.connect(thirdParty).addMaintainer(depositor.address), + ).to.be.revertedWithCustomError( + mezoAllocator, + "OwnableUnauthorizedAccount", + ) + }) + }) + + context("when a caller is governance", () => { + context("when a maintainer is added", () => { + let tx: ContractTransactionResponse + + before(async () => { + tx = await mezoAllocator + .connect(governance) + .addMaintainer(thirdParty.address) + }) + + it("should add a maintainer", async () => { + expect(await mezoAllocator.isMaintainer(thirdParty.address)).to.equal( + true, + ) + }) + + it("should emit MaintainerAdded event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "MaintainerAdded") + .withArgs(thirdParty.address) + }) + + it("should add a new maintainer to the list", async () => { + const maintainers = await mezoAllocator.getMaintainers() + expect(maintainers).to.deep.equal([ + maintainer.address, + thirdParty.address, + ]) + }) + + it("should not allow to add the same maintainer twice", async () => { + await expect( + mezoAllocator.connect(governance).addMaintainer(thirdParty.address), + ).to.be.revertedWithCustomError( + mezoAllocator, + "MaintainerAlreadyRegistered", + ) + }) + + it("should not allow to add a zero address as a maintainer", async () => { + await expect( + mezoAllocator.connect(governance).addMaintainer(ZeroAddress), + ).to.be.revertedWithCustomError(mezoAllocator, "ZeroAddress") + }) + }) + }) + }) + + describe("removeMaintainer", () => { + beforeAfterSnapshotWrapper() + + context("when a caller is not a governance", () => { + it("should revert", async () => { + await expect( + mezoAllocator.connect(thirdParty).removeMaintainer(depositor.address), + ).to.be.revertedWithCustomError( + mezoAllocator, + "OwnableUnauthorizedAccount", + ) + }) + }) + + context("when a caller is governance", () => { + context("when a maintainer is removed", () => { + let tx: ContractTransactionResponse + + before(async () => { + await mezoAllocator + .connect(governance) + .addMaintainer(thirdParty.address) + tx = await mezoAllocator + .connect(governance) + .removeMaintainer(thirdParty.address) + }) + + it("should remove a maintainer", async () => { + expect(await mezoAllocator.isMaintainer(thirdParty.address)).to.equal( + false, + ) + }) + + it("should emit MaintainerRemoved event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "MaintainerRemoved") + .withArgs(thirdParty.address) + }) + + it("should remove a maintainer from the list", async () => { + const maintainers = await mezoAllocator.getMaintainers() + expect(maintainers).to.deep.equal([maintainer.address]) + }) + + it("should not allow to remove a maintainer twice", async () => { + await expect( + mezoAllocator + .connect(governance) + .removeMaintainer(thirdParty.address), + ).to.be.revertedWithCustomError( + mezoAllocator, + "MaintainerNotRegistered", + ) + }) + }) + }) + }) + + describe("releaseDeposit", () => { + beforeAfterSnapshotWrapper() + + context("when a caller is not governance", () => { + it("should revert", async () => { + await expect( + mezoAllocator.connect(thirdParty).releaseDeposit(), + ).to.be.revertedWithCustomError( + mezoAllocator, + "OwnableUnauthorizedAccount", + ) + }) + }) + + context("when the caller is governance", () => { + context("when there is a deposit", () => { + let tx: ContractTransactionResponse + + before(async () => { + await tbtc.mint(await stbtc.getAddress(), to1e18(5)) + await mezoAllocator.connect(maintainer).allocate() + tx = await mezoAllocator.connect(governance).releaseDeposit() + }) + + it("should emit DepositReleased event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "DepositReleased") + .withArgs(1, to1e18(5)) + }) + + it("should decrease tracked deposit balance amount to zero", async () => { + const depositBalance = await mezoAllocator.depositBalance() + expect(depositBalance).to.equal(0) + }) + + it("should decrease Mezo Portal balance", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [mezoPortal, stbtc], + [-to1e18(5), to1e18(5)], + ) + }) + }) + }) + }) +}) diff --git a/solidity/test/MezoAllocator.upgrade.test.ts b/solidity/test/MezoAllocator.upgrade.test.ts new file mode 100644 index 000000000..b547df11e --- /dev/null +++ b/solidity/test/MezoAllocator.upgrade.test.ts @@ -0,0 +1,91 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" +import { expect } from "chai" +import { ethers, helpers } from "hardhat" +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" +import { ContractTransactionResponse } from "ethers" +import { beforeAfterSnapshotWrapper, deployment } from "./helpers" +import { + TestERC20, + MezoAllocator, + MezoAllocatorV2, + IMezoPortal, + StBTC, +} from "../typechain" + +async function fixture() { + const { stbtc, tbtc, mezoAllocator, mezoPortal } = await deployment() + + return { stbtc, tbtc, mezoAllocator, mezoPortal } +} + +describe("MezoAllocator contract upgrade", () => { + let mezoPortal: IMezoPortal + let tbtc: TestERC20 + let stbtc: StBTC + let mezoAllocator: MezoAllocator + let governance: HardhatEthersSigner + + before(async () => { + ;({ stbtc, tbtc, mezoAllocator, mezoPortal } = await loadFixture(fixture)) + ;({ governance } = await helpers.signers.getNamedSigners()) + }) + + context("when upgrading to a valid contract", () => { + let allocatorV2: MezoAllocatorV2 + const newVariable = 1n + + beforeAfterSnapshotWrapper() + + before(async () => { + const [upgradedAllocator] = await helpers.upgrades.upgradeProxy( + "MezoAllocator", + "MezoAllocatorV2", + { + factoryOpts: { signer: governance }, + proxyOpts: { + call: { + fn: "initializeV2", + args: [newVariable], + }, + }, + }, + ) + + allocatorV2 = upgradedAllocator as unknown as MezoAllocatorV2 + }) + + it("new instance should have the same address as the old one", async () => { + expect(await allocatorV2.getAddress()).to.equal( + await mezoAllocator.getAddress(), + ) + }) + + describe("contract variables", () => { + it("should initialize new variable correctly", async () => { + expect(await allocatorV2.newVariable()).to.eq(newVariable) + }) + + it("should keep v1 initial parameters", async () => { + expect(await allocatorV2.mezoPortal()).to.eq( + await mezoPortal.getAddress(), + ) + expect(await allocatorV2.tbtc()).to.eq(await tbtc.getAddress()) + expect(await allocatorV2.stbtc()).to.eq(await stbtc.getAddress()) + }) + }) + + describe("upgraded `addMaintainer` function", () => { + let tx: ContractTransactionResponse + + before(async () => { + const newAddress = await ethers.Wallet.createRandom().getAddress() + + tx = await allocatorV2.connect(governance).addMaintainer(newAddress) + }) + + it("should emit `NewEvent` event", async () => { + await expect(tx).to.emit(allocatorV2, "NewEvent") + }) + }) + }) +}) diff --git a/core/test/data/tbtc.ts b/solidity/test/data/tbtc.ts similarity index 97% rename from core/test/data/tbtc.ts rename to solidity/test/data/tbtc.ts index fcf97c50d..923635e4f 100644 --- a/core/test/data/tbtc.ts +++ b/solidity/test/data/tbtc.ts @@ -35,7 +35,7 @@ export const tbtcDepositData = { vault: "0x594cfd89700040163727828AE20B52099C58F02C", }, // 20-bytes of extraData - staker: "0xa9B38eA6435c8941d6eDa6a46b68E3e211719699", + depositOwner: "0xa9B38eA6435c8941d6eDa6a46b68E3e211719699", // 2-bytes of extraData referral: "0x5bd1", extraData: diff --git a/core/test/helpers.test.ts b/solidity/test/helpers.test.ts similarity index 100% rename from core/test/helpers.test.ts rename to solidity/test/helpers.test.ts diff --git a/core/test/helpers/context.ts b/solidity/test/helpers/context.ts similarity index 79% rename from core/test/helpers/context.ts rename to solidity/test/helpers/context.ts index 5d6f518ea..b4d864fcb 100644 --- a/core/test/helpers/context.ts +++ b/solidity/test/helpers/context.ts @@ -3,10 +3,10 @@ import { getDeployedContract } from "./contract" import type { StBTC as stBTC, - Dispatcher, BridgeStub, - TestERC4626, TBTCVaultStub, + MezoAllocator, + MezoPortalStub, BitcoinDepositor, BitcoinRedeemer, TestTBTC, @@ -26,9 +26,9 @@ export async function deployment() { const tbtcBridge: BridgeStub = await getDeployedContract("Bridge") const tbtcVault: TBTCVaultStub = await getDeployedContract("TBTCVault") - const dispatcher: Dispatcher = await getDeployedContract("Dispatcher") - - const vault: TestERC4626 = await getDeployedContract("Vault") + const mezoAllocator: MezoAllocator = + await getDeployedContract("MezoAllocator") + const mezoPortal: MezoPortalStub = await getDeployedContract("MezoPortal") return { tbtc, @@ -37,7 +37,7 @@ export async function deployment() { bitcoinRedeemer, tbtcBridge, tbtcVault, - dispatcher, - vault, + mezoAllocator, + mezoPortal, } } diff --git a/core/test/helpers/contract.ts b/solidity/test/helpers/contract.ts similarity index 100% rename from core/test/helpers/contract.ts rename to solidity/test/helpers/contract.ts diff --git a/core/test/helpers/index.ts b/solidity/test/helpers/index.ts similarity index 100% rename from core/test/helpers/index.ts rename to solidity/test/helpers/index.ts diff --git a/core/test/helpers/snapshot.ts b/solidity/test/helpers/snapshot.ts similarity index 100% rename from core/test/helpers/snapshot.ts rename to solidity/test/helpers/snapshot.ts diff --git a/core/test/stBTC.test.ts b/solidity/test/stBTC.test.ts similarity index 98% rename from core/test/stBTC.test.ts rename to solidity/test/stBTC.test.ts index 4f5bec95a..0346f69c4 100644 --- a/core/test/stBTC.test.ts +++ b/solidity/test/stBTC.test.ts @@ -12,12 +12,12 @@ import { beforeAfterSnapshotWrapper, deployment } from "./helpers" import { to1e18 } from "./utils" -import type { StBTC as stBTC, TestERC20, Dispatcher } from "../typechain" +import type { StBTC as stBTC, TestERC20, MezoAllocator } from "../typechain" const { getNamedSigners, getUnnamedSigners } = helpers.signers async function fixture() { - const { tbtc, stbtc, dispatcher } = await deployment() + const { tbtc, stbtc, mezoAllocator } = await deployment() const { governance, treasury, pauseAdmin } = await getNamedSigners() const [depositor1, depositor2, thirdParty] = await getUnnamedSigners() @@ -31,10 +31,10 @@ async function fixture() { tbtc, depositor1, depositor2, - dispatcher, governance, thirdParty, treasury, + mezoAllocator, pauseAdmin, } } @@ -46,7 +46,7 @@ describe("stBTC", () => { let stbtc: stBTC let tbtc: TestERC20 - let dispatcher: Dispatcher + let mezoAllocator: MezoAllocator let governance: HardhatEthersSigner let depositor1: HardhatEthersSigner @@ -61,10 +61,10 @@ describe("stBTC", () => { tbtc, depositor1, depositor2, - dispatcher, governance, thirdParty, treasury, + mezoAllocator, pauseAdmin, } = await loadFixture(fixture)) @@ -659,7 +659,7 @@ describe("stBTC", () => { const amountToDeposit = to1e18(1) let tx: ContractTransactionResponse let amountToRedeem: bigint - let amountStaked: bigint + let amountDeposited: bigint let shares: bigint before(async () => { @@ -670,10 +670,10 @@ describe("stBTC", () => { await stbtc .connect(depositor1) .deposit(amountToDeposit, depositor1.address) - amountStaked = + amountDeposited = amountToDeposit - feeOnTotal(amountToDeposit, entryFeeBasisPoints) amountToRedeem = - amountStaked - feeOnTotal(amountStaked, exitFeeBasisPoints) + amountDeposited - feeOnTotal(amountDeposited, exitFeeBasisPoints) tx = await stbtc .connect(depositor1) .redeem(shares, thirdParty, depositor1) @@ -714,7 +714,7 @@ describe("stBTC", () => { await expect(tx).to.changeTokenBalances( tbtc, [treasury.address], - [feeOnTotal(amountStaked, exitFeeBasisPoints)], + [feeOnTotal(amountDeposited, exitFeeBasisPoints)], ) }) }) @@ -989,7 +989,7 @@ describe("stBTC", () => { ) }) - it("should transfer tBTC tokens to a Staker", async () => { + it("should transfer tBTC tokens to a deposit owner", async () => { await expect(withdrawTx).to.changeTokenBalances( tbtc, [depositor1.address], @@ -1098,7 +1098,7 @@ describe("stBTC", () => { before(async () => { // Dispatcher is set by the deployment scripts. See deployment tests // where initial parameters are checked. - dispatcherAddress = await dispatcher.getAddress() + dispatcherAddress = await mezoAllocator.getAddress() newDispatcher = await ethers.Wallet.createRandom().getAddress() stbtcAddress = await stbtc.getAddress() diff --git a/core/test/stBTC.upgrade.test.ts b/solidity/test/stBTC.upgrade.test.ts similarity index 85% rename from core/test/stBTC.upgrade.test.ts rename to solidity/test/stBTC.upgrade.test.ts index 3bf77cbc1..f22c3f997 100644 --- a/core/test/stBTC.upgrade.test.ts +++ b/solidity/test/stBTC.upgrade.test.ts @@ -76,21 +76,23 @@ describe("stBTC contract upgrade", () => { }) describe("upgraded `deposit` function", () => { - let amountToStake: bigint - let staker: HardhatEthersSigner + let amountToDeposit: bigint + let depositOwner: HardhatEthersSigner let tx: ContractTransactionResponse before(async () => { - ;[staker] = await helpers.signers.getUnnamedSigners() - amountToStake = v1MinimumDepositAmount + 1n + ;[depositOwner] = await helpers.signers.getUnnamedSigners() + amountToDeposit = v1MinimumDepositAmount + 1n - await tbtc.mint(staker, amountToStake) + await tbtc.mint(depositOwner, amountToDeposit) await tbtc - .connect(staker) - .approve(await stbtcV2.getAddress(), amountToStake) + .connect(depositOwner) + .approve(await stbtcV2.getAddress(), amountToDeposit) - tx = await stbtcV2.connect(staker).deposit(amountToStake, staker) + tx = await stbtcV2 + .connect(depositOwner) + .deposit(amountToDeposit, depositOwner) }) it("should emit `NewEvent` event", async () => { diff --git a/core/test/utils/index.ts b/solidity/test/utils/index.ts similarity index 100% rename from core/test/utils/index.ts rename to solidity/test/utils/index.ts diff --git a/core/test/utils/number.ts b/solidity/test/utils/number.ts similarity index 100% rename from core/test/utils/number.ts rename to solidity/test/utils/number.ts diff --git a/core/tsconfig.export.json b/solidity/tsconfig.export.json similarity index 100% rename from core/tsconfig.export.json rename to solidity/tsconfig.export.json diff --git a/core/tsconfig.json b/solidity/tsconfig.json similarity index 100% rename from core/tsconfig.json rename to solidity/tsconfig.json diff --git a/core/types/index.ts b/solidity/types/index.ts similarity index 73% rename from core/types/index.ts rename to solidity/types/index.ts index e84cb597b..19e2cf0f5 100644 --- a/core/types/index.ts +++ b/solidity/types/index.ts @@ -1,5 +1,5 @@ /* eslint-disable import/prefer-default-export */ -export enum StakeRequestState { +export enum DepositState { Unknown, Initialized, Finalized,