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/package.json b/dapp/package.json index f93626084..c8cdf2427 100644 --- a/dapp/package.json +++ b/dapp/package.json @@ -25,6 +25,7 @@ "@sentry/react": "^7.98.0", "@sentry/types": "^7.102.0", "@tanstack/react-table": "^8.11.3", + "@types/react-slick": "^0.23.13", "axios": "^1.6.7", "ethers": "^6.10.0", "formik": "^2.4.5", @@ -34,6 +35,7 @@ "react-number-format": "^5.3.1", "react-redux": "^9.1.0", "react-router-dom": "^6.22.0", + "react-slick": "^0.30.2", "recharts": "^2.12.0" }, "devDependencies": { diff --git a/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx b/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx new file mode 100644 index 000000000..ea95c22b7 --- /dev/null +++ b/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx @@ -0,0 +1,66 @@ +import React from "react" +import { ArrowUpRight } from "#/assets/icons" +import { Box, Icon } from "@chakra-ui/react" +import { Variants, motion } from "framer-motion" +import { chakraUnitToPx } from "#/theme/utils" + +const arrowUpVariants: Variants = { + initial: { + x: 0, + y: -5, + }, + animate: (boxSizePx: number) => ({ + x: [0, boxSizePx], + y: [-5, -boxSizePx], + transition: { + duration: 0.4, + ease: "easeInOut", + }, + }), +} + +const arrowBottomVariants: Variants = { + initial: (boxSizePx: number) => ({ + x: -boxSizePx, + y: boxSizePx, + }), + animate: (boxSizePx: number) => ({ + x: [-boxSizePx, 0], + y: [boxSizePx, -5], + transition: { + duration: 0.4, + ease: "easeInOut", + }, + }), +} + +type ArrowUpRightAnimatedIconProps = { + boxSize?: number + color?: string +} + +export function ArrowUpRightAnimatedIcon({ + boxSize = 4, + color = "brand.400", +}: ArrowUpRightAnimatedIconProps) { + const boxSizePx = chakraUnitToPx(boxSize) + return ( + + {[ + { id: "arrow-up", variants: arrowUpVariants }, + { id: "arrow-bottom", variants: arrowBottomVariants }, + ].map(({ id, variants }) => ( + + + + ))} + + ) +} diff --git a/dapp/src/assets/icons/animated/index.ts b/dapp/src/assets/icons/animated/index.ts new file mode 100644 index 000000000..1a4701531 --- /dev/null +++ b/dapp/src/assets/icons/animated/index.ts @@ -0,0 +1 @@ +export * from "./ArrowUpRightAnimatedIcon" diff --git a/dapp/src/components/GlobalStyles/index.tsx b/dapp/src/components/GlobalStyles/index.tsx index e8b11945a..5c93f8dc2 100644 --- a/dapp/src/components/GlobalStyles/index.tsx +++ b/dapp/src/components/GlobalStyles/index.tsx @@ -41,6 +41,19 @@ export default function GlobalStyles() { font-weight: 900; font-style: normal; } + // React-slick package: Chakra-ui with react-slick package doesn't + // generate flex style for auto-generated slick-track wrapper. + // Instead of importing default styles for react-slick carousel + // we only add what we need - flex for the .slick-track. + .slick-track { + display: flex; + } + // React-slick package: Hiding arrows instead of disabling them in case + // when carousel is not fully completed by slides. + [data-id="slick-arrow-prev"]:disabled:has(~ [data-id="slick-arrow-next"]:disabled), + [data-id="slick-arrow-prev"]:disabled ~ [data-id="slick-arrow-next"]:disabled{ + display: none; + } `} /> ) diff --git a/dapp/src/components/LiquidStakingTokenPopover.tsx b/dapp/src/components/LiquidStakingTokenPopover.tsx index b59b5b720..229a37ffa 100644 --- a/dapp/src/components/LiquidStakingTokenPopover.tsx +++ b/dapp/src/components/LiquidStakingTokenPopover.tsx @@ -10,7 +10,7 @@ import { IconButton, } from "@chakra-ui/react" import { SizeType } from "#/types" -import { useDocsDrawer, useWalletContext } from "#/hooks" +import { useDocsDrawer, useSharesBalance, useWalletContext } from "#/hooks" import { TextMd, TextSm } from "./shared/Typography" import Alert from "./shared/Alert" import { CurrencyBalance } from "./shared/CurrencyBalance" @@ -23,6 +23,7 @@ export function LiquidStakingTokenPopover({ }: LiquidStakingTokenPopoverProps) { const { isConnected } = useWalletContext() const { onOpen: openDocsDrawer } = useDocsDrawer() + const sharesBalance = useSharesBalance() return ( @@ -50,7 +51,7 @@ export function LiquidStakingTokenPopover({ Liquid staking token diff --git a/dapp/src/components/shared/ActivityBar/index.tsx b/dapp/src/components/shared/ActivityBar/index.tsx deleted file mode 100644 index 4eb65f505..000000000 --- a/dapp/src/components/shared/ActivityBar/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useCallback, useState } from "react" -import { Link as ReactRouterLink } from "react-router-dom" -import { Flex, Link as ChakraLink, FlexboxProps } from "@chakra-ui/react" -import ActivityCard from "./ActivityCard" -import { mockedActivities } from "./mock-activities" - -function ActivityBar(props: FlexboxProps) { - const [activities, setActivities] = useState(mockedActivities) - - const onRemove = useCallback( - (activityHash: string) => { - const filteredActivities = activities.filter( - (activity) => activity.txHash !== activityHash, - ) - setActivities(filteredActivities) - }, - [activities], - ) - return ( - - {activities.map((activity) => ( - - - - ))} - - ) -} - -export default ActivityBar diff --git a/dapp/src/components/shared/ActivityBar/mock-activities.ts b/dapp/src/components/shared/ActivityBar/mock-activities.ts deleted file mode 100644 index 022f925fd..000000000 --- a/dapp/src/components/shared/ActivityBar/mock-activities.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ActivityInfo } from "#/types" - -export const mockedActivities: ActivityInfo[] = [ - { - amount: 324000000, - action: "stake", - currency: "bitcoin", - txHash: "2dc2341e6c8463b8731eeb356e52acb7", - status: "syncing", - }, - { - amount: 524000000, - action: "unstake", - currency: "bitcoin", - txHash: "92eb5ffee6ae2fec3ad71c777531578f", - status: "pending", - }, - { - amount: 224000000, - action: "receive", - currency: "bitcoin", - txHash: "0cc175b9c0f1b6a831c399e269772661", - status: "completed", - }, -] diff --git a/dapp/src/components/shared/ActivityBar/ActivityCard.tsx b/dapp/src/components/shared/ActivityCard/ActivityCard.tsx similarity index 53% rename from dapp/src/components/shared/ActivityBar/ActivityCard.tsx rename to dapp/src/components/shared/ActivityCard/ActivityCard.tsx index dcf0ca46e..fe83ae3a9 100644 --- a/dapp/src/components/shared/ActivityBar/ActivityCard.tsx +++ b/dapp/src/components/shared/ActivityCard/ActivityCard.tsx @@ -9,37 +9,48 @@ import { Tooltip, CloseButton, } from "@chakra-ui/react" -import { useLocation } from "react-router-dom" -import { ActivityInfo, LocationState } from "#/types" -import { capitalize } from "#/utils" +import { useNavigate } from "react-router-dom" +import { ActivityInfo } from "#/types" import { ChevronRightIcon } from "#/assets/icons" import { CurrencyBalance } from "#/components/shared/CurrencyBalance" import StatusInfo from "#/components/shared/StatusInfo" import { TextSm } from "#/components/shared/Typography" -import ActivityCardContainer from "./ActivityCardContainer" +import { routerPath } from "#/router/path" +import { useActivities } from "#/hooks" +import { ActivityCardWrapper } from "./ActivityCardWrapper" type ActivityCardType = CardProps & { activity: ActivityInfo - onRemove: (txHash: string) => void + onRemove: (activity: ActivityInfo) => void } -function ActivityCard({ activity, onRemove }: ActivityCardType) { - const state = useLocation().state as LocationState | null - const isActive = state ? activity.txHash === state.activity.txHash : false - const isCompleted = activity.status === "completed" +export function ActivityCard({ activity, onRemove }: ActivityCardType) { + const navigate = useNavigate() + const { isCompleted, isSelected } = useActivities() + + const isActivitySelected = isSelected(activity) + const isActivityCompleted = isCompleted(activity) + + const onClick = useCallback(() => { + navigate(`${routerPath.activity}/${activity.txHash}`) + }, [activity.txHash, navigate]) const onClose = useCallback( (event: React.MouseEvent) => { - event.preventDefault() - if (activity.txHash) { - onRemove(activity.txHash) + event.stopPropagation() + if (activity) { + onRemove(activity) } }, - [onRemove, activity.txHash], + [activity, onRemove], ) return ( - + - {isCompleted ? ( + {isActivityCompleted ? ( @@ -57,15 +68,15 @@ function ActivityCard({ activity, onRemove }: ActivityCardType) { )} - - - {capitalize(activity.action)} + + + {activity.action} @@ -76,8 +87,6 @@ function ActivityCard({ activity, onRemove }: ActivityCardType) { fontWeight="medium" /> - + ) } - -export default ActivityCard diff --git a/dapp/src/components/shared/ActivityBar/ActivityCardContainer.tsx b/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx similarity index 80% rename from dapp/src/components/shared/ActivityBar/ActivityCardContainer.tsx rename to dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx index 2c6256e38..4ac9c0691 100644 --- a/dapp/src/components/shared/ActivityBar/ActivityCardContainer.tsx +++ b/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx @@ -1,10 +1,10 @@ import React from "react" import { CardProps, Card } from "@chakra-ui/react" -type ActivityCardContainerProps = CardProps & { - isCompleted: boolean - isActive: boolean +type ActivityCardWrapperProps = CardProps & { children: React.ReactNode + isCompleted: boolean + isActive?: boolean } const completedStyles = { @@ -34,18 +34,19 @@ const activeStyles = { }, } -function ActivityCardContainer({ +export function ActivityCardWrapper({ isActive, isCompleted, children, ...props -}: ActivityCardContainerProps) { +}: ActivityCardWrapperProps) { return ( {children} ) } - -export default ActivityCardContainer diff --git a/dapp/src/components/shared/ActivityCard/index.tsx b/dapp/src/components/shared/ActivityCard/index.tsx new file mode 100644 index 000000000..30379b1d9 --- /dev/null +++ b/dapp/src/components/shared/ActivityCard/index.tsx @@ -0,0 +1 @@ +export * from "./ActivityCard" diff --git a/dapp/src/components/shared/Carousel.tsx b/dapp/src/components/shared/Carousel.tsx new file mode 100644 index 000000000..3ca5e2b2a --- /dev/null +++ b/dapp/src/components/shared/Carousel.tsx @@ -0,0 +1,33 @@ +import React, { forwardRef } from "react" +import { FlexProps, Flex } from "@chakra-ui/react" +import Slider, { Settings as SliderProps } from "react-slick" + +const carouselSettings: SliderProps = { + dots: false, + infinite: false, + draggable: false, + variableWidth: true, + speed: 500, + slidesToShow: 12, + slidesToScroll: 1, +} + +type CarouselProps = FlexProps & + SliderProps & { + children: React.ReactNode + } + +export const Carousel = forwardRef( + (props, ref) => ( + + {props.children} + + ), +) diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index f010fa303..2c6a4c3db 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -17,4 +17,6 @@ export * from "./useInitApp" export * from "./useCurrencyConversion" export * from "./useDepositTelemetry" export * from "./useFetchBTCPriceUSD" +export * from "./useActivities" +export * from "./useFetchBTCBalance" export * from "./useSize" diff --git a/dapp/src/hooks/store/index.ts b/dapp/src/hooks/store/index.ts index 20f33eb3c..ac22e8a41 100644 --- a/dapp/src/hooks/store/index.ts +++ b/dapp/src/hooks/store/index.ts @@ -1,2 +1,4 @@ export * from "./useAppDispatch" export * from "./useAppSelector" +export * from "./useEstimatedBTCBalance" +export * from "./useSharesBalance" diff --git a/dapp/src/hooks/store/useEstimatedBTCBalance.ts b/dapp/src/hooks/store/useEstimatedBTCBalance.ts new file mode 100644 index 000000000..b8ce0bb67 --- /dev/null +++ b/dapp/src/hooks/store/useEstimatedBTCBalance.ts @@ -0,0 +1,6 @@ +import { selectEstimatedBtcBalance } from "#/store/btc" +import { useAppSelector } from "./useAppSelector" + +export function useEstimatedBTCBalance() { + return useAppSelector(selectEstimatedBtcBalance) +} diff --git a/dapp/src/hooks/store/useSharesBalance.ts b/dapp/src/hooks/store/useSharesBalance.ts new file mode 100644 index 000000000..067e32e93 --- /dev/null +++ b/dapp/src/hooks/store/useSharesBalance.ts @@ -0,0 +1,6 @@ +import { selectSharesBalance } from "#/store/btc" +import { useAppSelector } from "./useAppSelector" + +export function useSharesBalance() { + return useAppSelector(selectSharesBalance) +} diff --git a/dapp/src/hooks/useActivities.ts b/dapp/src/hooks/useActivities.ts new file mode 100644 index 000000000..bf7a80a98 --- /dev/null +++ b/dapp/src/hooks/useActivities.ts @@ -0,0 +1,51 @@ +import { useCallback, useState } from "react" +import { mockedActivities } from "#/mock" +import { useParams } from "react-router-dom" +import { ActivityInfo } from "#/types" + +export function useActivities() { + // TODO: should be replaced by redux store when subgraphs are implemented + const [activities, setActivities] = useState(mockedActivities) + const params = useParams() + + const getActivity = useCallback( + (activityId?: string) => + activities.find((_activity) => _activity.txHash === activityId), + [activities], + ) + + const removeActivity = useCallback( + (activity: ActivityInfo) => { + const filteredActivities = activities.filter( + (_activity) => _activity.txHash !== activity.txHash, + ) + setActivities(filteredActivities) + }, + [activities], + ) + + const selectedActivity = useCallback( + () => getActivity(params.activityId), + [getActivity, params.activityId], + ) + + const isSelected = useCallback( + (activity: ActivityInfo): boolean => + activity.txHash === getActivity(params.activityId)?.txHash, + [getActivity, params.activityId], + ) + + const isCompleted = useCallback( + (activity: ActivityInfo): boolean => activity.status === "completed", + [], + ) + + return { + activities, + getActivity, + removeActivity, + selectedActivity, + isCompleted, + isSelected, + } +} diff --git a/dapp/src/hooks/useFetchBTCBalance.ts b/dapp/src/hooks/useFetchBTCBalance.ts new file mode 100644 index 000000000..360135312 --- /dev/null +++ b/dapp/src/hooks/useFetchBTCBalance.ts @@ -0,0 +1,28 @@ +import { useEffect } from "react" +import { EthereumAddress } from "@acre-btc/sdk" +import { useAcreContext } from "#/acre-react/hooks" +import { logPromiseFailure } from "#/utils" +import { setEstimatedBtcBalance, setSharesBalance } from "#/store/btc" +import { useWalletContext } from "./useWalletContext" +import { useAppDispatch } from "./store" + +export function useFetchBTCBalance() { + const { acre, isInitialized } = useAcreContext() + const { ethAccount } = useWalletContext() + const dispatch = useAppDispatch() + + useEffect(() => { + const getBtcBalance = async () => { + if (!isInitialized || !ethAccount || !acre) return + + const chainIdentifier = EthereumAddress.from(ethAccount.address) + const sharesBalance = await acre.staking.sharesBalance(chainIdentifier) + const estimatedBitcoinBalance = + await acre.staking.estimatedBitcoinBalance(chainIdentifier) + + dispatch(setSharesBalance(sharesBalance)) + dispatch(setEstimatedBtcBalance(estimatedBitcoinBalance)) + } + logPromiseFailure(getBtcBalance()) + }, [acre, isInitialized, ethAccount, dispatch]) +} diff --git a/dapp/src/hooks/useInitApp.ts b/dapp/src/hooks/useInitApp.ts index bfa07283e..906bcd608 100644 --- a/dapp/src/hooks/useInitApp.ts +++ b/dapp/src/hooks/useInitApp.ts @@ -1,6 +1,7 @@ import { useSentry } from "./sentry" import { useInitializeAcreSdk } from "./useInitializeAcreSdk" import { useFetchBTCPriceUSD } from "./useFetchBTCPriceUSD" +import { useFetchBTCBalance } from "./useFetchBTCBalance" export function useInitApp() { // TODO: Let's uncomment when dark mode is ready @@ -8,4 +9,5 @@ export function useInitApp() { useSentry() useInitializeAcreSdk() useFetchBTCPriceUSD() + useFetchBTCBalance() } diff --git a/dapp/src/mock/index.ts b/dapp/src/mock/index.ts new file mode 100644 index 000000000..38a72fdb4 --- /dev/null +++ b/dapp/src/mock/index.ts @@ -0,0 +1 @@ +export * from "./mock-activities" diff --git a/dapp/src/mock/mock-activities.ts b/dapp/src/mock/mock-activities.ts new file mode 100644 index 000000000..a6efd2a2a --- /dev/null +++ b/dapp/src/mock/mock-activities.ts @@ -0,0 +1,67 @@ +import { ActivityInfo } from "#/types" + +export const mockedActivities: ActivityInfo[] = [ + { + amount: 324000000, + action: "stake", + currency: "bitcoin", + txHash: "2dc2341e6c8463b8731eeb356e52acb7", + status: "syncing", + }, + { + amount: 524000000, + action: "unstake", + currency: "bitcoin", + txHash: "92eb5ffee6ae2fec3ad71c777531578f", + status: "pending", + }, + { + amount: 224000000, + action: "receive", + currency: "bitcoin", + txHash: "0cc175b9c0f1b6a831c399e269772661", + status: "completed", + }, + { + amount: 324000000, + action: "stake", + currency: "bitcoin", + txHash: "2dc2341e6c8463b8731eeb356e52aca1", + status: "syncing", + }, + { + amount: 524000000, + action: "unstake", + currency: "bitcoin", + txHash: "92eb5ffee6ae2fec3ad71c77753157a2", + status: "pending", + }, + { + amount: 224000000, + action: "receive", + currency: "bitcoin", + txHash: "0cc175b9c0f1b6a831c399e2697726a3", + status: "completed", + }, + { + amount: 324000000, + action: "stake", + currency: "bitcoin", + txHash: "2dc2341e6c8463b8731eeb356e52aca4", + status: "syncing", + }, + { + amount: 524000000, + action: "unstake", + currency: "bitcoin", + txHash: "92eb5ffee6ae2fec3ad71c77753157a5", + status: "pending", + }, + { + amount: 224000000, + action: "receive", + currency: "bitcoin", + txHash: "0cc175b9c0f1b6a831c399e2697726a6", + status: "completed", + }, +] diff --git a/dapp/src/pages/ActivityPage/ActivityBar.tsx b/dapp/src/pages/ActivityPage/ActivityBar.tsx new file mode 100644 index 000000000..c7d2ec48d --- /dev/null +++ b/dapp/src/pages/ActivityPage/ActivityBar.tsx @@ -0,0 +1,20 @@ +import React from "react" +import { VStack } from "@chakra-ui/react" +import { ActivityCard } from "#/components/shared/ActivityCard" +import { useActivities } from "#/hooks" + +export function ActivityBar() { + const { activities, removeActivity } = useActivities() + + return ( + + {activities.map((activity) => ( + + ))} + + ) +} diff --git a/dapp/src/pages/ActivityPage/ActivityDetails.tsx b/dapp/src/pages/ActivityPage/ActivityDetails.tsx index 05f82fd82..44fd84b21 100644 --- a/dapp/src/pages/ActivityPage/ActivityDetails.tsx +++ b/dapp/src/pages/ActivityPage/ActivityDetails.tsx @@ -1,5 +1,4 @@ import React from "react" -import { useLocation } from "react-router-dom" import { Card, CardBody, @@ -10,22 +9,23 @@ import { Flex, Text, } from "@chakra-ui/react" -import { capitalize } from "#/utils" import ActivityProgress from "#/assets/images/activity-progress.png" -import { LocationState } from "#/types" import StatusInfo from "#/components/shared/StatusInfo" import { TextMd, TextSm } from "#/components/shared/Typography" import Spinner from "#/components/shared/Spinner" import { CurrencyBalanceWithConversion } from "#/components/shared/CurrencyBalanceWithConversion" +import { useActivities } from "#/hooks" function ActivityDetails() { - const location = useLocation() + const { selectedActivity } = useActivities() - const { activity } = location.state as LocationState + const currentActivity = selectedActivity() + + if (!currentActivity) return null return ( - {activity.status === "pending" && ( + {currentActivity.status === "pending" && ( @@ -46,13 +46,17 @@ function ActivityDetails() { - - {capitalize(activity.action)} + + {currentActivity.action} - + diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx new file mode 100644 index 000000000..3a35b9a9e --- /dev/null +++ b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx @@ -0,0 +1,132 @@ +import React, { useRef, useCallback } from "react" +import Slider from "react-slick" +import { Box, BoxProps } from "@chakra-ui/react" +import { Carousel } from "#/components/shared/Carousel" +import { ActivityCard } from "#/components/shared/ActivityCard" +import { useActivities } from "#/hooks" +import { ActivityInfo } from "#/types" +import { NextArrowCarousel, PrevArrowCarousel } from "./ActivityCarouselArrows" + +/* * + * Settings for react-slick carousel. + * Breakpoints are calculated based on with & visibility of activity card. + * slidesToShow attr is needed to correctly display the number of cards in the carousel + * and it depends on the width of the viewport. + * */ +export const activityCarouselSettings = { + nextArrow: , + prevArrow: , + responsive: [ + { + breakpoint: 820, + settings: { + slidesToShow: 1, + }, + }, + { + breakpoint: 1080, + settings: { + slidesToShow: 2, + }, + }, + { + breakpoint: 1360, + settings: { + slidesToShow: 3, + }, + }, + { + breakpoint: 1620, + settings: { + slidesToShow: 4, + }, + }, + { + breakpoint: 1900, + settings: { + slidesToShow: 5, + }, + }, + { + breakpoint: 2160, + settings: { + slidesToShow: 6, + }, + }, + { + breakpoint: 2440, + settings: { + slidesToShow: 7, + }, + }, + { + breakpoint: 2700, + settings: { + slidesToShow: 8, + }, + }, + { + breakpoint: 2980, + settings: { + slidesToShow: 9, + }, + }, + { + breakpoint: 3240, + settings: { + slidesToShow: 10, + }, + }, + { + breakpoint: 3520, + settings: { + slidesToShow: 11, + }, + }, + ], +} + +export function ActivityCarousel({ ...props }: BoxProps) { + const carouselRef = useRef(null) + const { activities, removeActivity } = useActivities() + + const handleRemove = useCallback( + (activity: ActivityInfo) => { + carouselRef.current?.slickPrev() + carouselRef.current?.forceUpdate() + removeActivity(activity) + }, + [removeActivity], + ) + + return ( + + + {activities.map((activity) => ( + + ))} + + + ) +} diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarouselArrows.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarouselArrows.tsx new file mode 100644 index 000000000..29a5cb8c6 --- /dev/null +++ b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarouselArrows.tsx @@ -0,0 +1,48 @@ +import React from "react" +import { CustomArrowProps } from "react-slick" +import { IconButton, IconButtonProps } from "@chakra-ui/react" +import { ArrowLeft, ArrowRight } from "#/assets/icons" + +type PaginationArrowType = CustomArrowProps & IconButtonProps + +function PaginationArrow({ icon, onClick, ...props }: PaginationArrowType) { + return ( + + ) +} + +export function PrevArrowCarousel({ onClick }: CustomArrowProps) { + return ( + } + aria-label="prev" + data-id="slick-arrow-prev" + /> + ) +} +export function NextArrowCarousel({ onClick }: CustomArrowProps) { + return ( + } + aria-label="next" + data-id="slick-arrow-next" + /> + ) +} diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/index.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/index.tsx new file mode 100644 index 000000000..47ef26c1e --- /dev/null +++ b/dapp/src/pages/OverviewPage/ActivityCarousel/index.tsx @@ -0,0 +1 @@ +export * from "./ActivityCarousel" diff --git a/dapp/src/pages/OverviewPage/DocsCard.tsx b/dapp/src/pages/OverviewPage/DocsCard.tsx new file mode 100644 index 000000000..dafe74c04 --- /dev/null +++ b/dapp/src/pages/OverviewPage/DocsCard.tsx @@ -0,0 +1,32 @@ +import React from "react" +import { Card, CardProps, HStack } from "@chakra-ui/react" +import { useDocsDrawer } from "#/hooks" +import { TextSm } from "#/components/shared/Typography" +import { ArrowUpRightAnimatedIcon } from "#/assets/icons/animated" +import { motion } from "framer-motion" + +export function DocsCard(props: CardProps) { + const { onOpen } = useDocsDrawer() + + return ( + + + + Documentation + + Everything you need to know about our contracts. + + ) +} diff --git a/dapp/src/pages/OverviewPage/PositionDetails.tsx b/dapp/src/pages/OverviewPage/PositionDetails.tsx index d3441362f..db6eb4387 100644 --- a/dapp/src/pages/OverviewPage/PositionDetails.tsx +++ b/dapp/src/pages/OverviewPage/PositionDetails.tsx @@ -11,10 +11,12 @@ import { CurrencyBalanceWithConversion } from "#/components/shared/CurrencyBalan import { TextMd } from "#/components/shared/Typography" import { ACTION_FLOW_TYPES, ActionFlowType } from "#/types" import TransactionModal from "#/components/TransactionModal" +import { useEstimatedBTCBalance } from "#/hooks/store" import { LiquidStakingTokenPopover } from "#/components/LiquidStakingTokenPopover" import { useSize } from "#/hooks" export default function PositionDetails(props: CardProps) { + const estimatedBtcBalance = useEstimatedBTCBalance() const { ref, size } = useSize() const [actionFlowType, setActionFlowType] = useState< @@ -35,7 +37,7 @@ export default function PositionDetails(props: CardProps) { - + + {/* TODO: Handle click actions */} Show values in {USD.symbol} - - - - Docs - - + + + + + diff --git a/dapp/src/router/index.tsx b/dapp/src/router/index.tsx index 357a14aad..5d280ad22 100644 --- a/dapp/src/router/index.tsx +++ b/dapp/src/router/index.tsx @@ -2,14 +2,15 @@ import React from "react" import { createBrowserRouter } from "react-router-dom" import OverviewPage from "#/pages/OverviewPage" import ActivityPage from "#/pages/ActivityPage" +import { routerPath } from "./path" export const router = createBrowserRouter([ { - path: "/", + path: routerPath.home, element: , }, { - path: "activity-details", + path: `${routerPath.activity}/:activityId`, element: , }, ]) diff --git a/dapp/src/router/path.ts b/dapp/src/router/path.ts new file mode 100644 index 000000000..be0fbef7c --- /dev/null +++ b/dapp/src/router/path.ts @@ -0,0 +1,4 @@ +export const routerPath = { + home: "/", + activity: "/activity-details", +} diff --git a/dapp/src/store/btc/btcSelector.ts b/dapp/src/store/btc/btcSelector.ts index c8ae200ab..806f5b65b 100644 --- a/dapp/src/store/btc/btcSelector.ts +++ b/dapp/src/store/btc/btcSelector.ts @@ -1,3 +1,8 @@ import { RootState } from ".." -export const selectBtcUsdPrice = (state: RootState) => state.btc.usdPrice +export const selectEstimatedBtcBalance = (state: RootState): bigint => + state.btc.estimatedBtcBalance +export const selectSharesBalance = (state: RootState): bigint => + state.btc.sharesBalance +export const selectBtcUsdPrice = (state: RootState): number => + state.btc.usdPrice diff --git a/dapp/src/store/btc/btcSlice.ts b/dapp/src/store/btc/btcSlice.ts index 84468710d..a3e7ce9fc 100644 --- a/dapp/src/store/btc/btcSlice.ts +++ b/dapp/src/store/btc/btcSlice.ts @@ -2,11 +2,15 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit" import { fetchBTCPriceUSD } from "./btcThunk" type BtcState = { + estimatedBtcBalance: bigint + sharesBalance: bigint isLoadingPriceUSD: boolean usdPrice: number } const initialState: BtcState = { + estimatedBtcBalance: 0n, + sharesBalance: 0n, isLoadingPriceUSD: false, usdPrice: 0, } @@ -15,7 +19,14 @@ const initialState: BtcState = { export const btcSlice = createSlice({ name: "btc", initialState, - reducers: {}, + reducers: { + setSharesBalance(state, action: PayloadAction) { + state.sharesBalance = action.payload + }, + setEstimatedBtcBalance(state, action: PayloadAction) { + state.estimatedBtcBalance = action.payload + }, + }, extraReducers: (builder) => { builder.addCase(fetchBTCPriceUSD.pending, (state) => { state.isLoadingPriceUSD = true @@ -32,3 +43,5 @@ export const btcSlice = createSlice({ ) }, }) + +export const { setSharesBalance, setEstimatedBtcBalance } = btcSlice.actions diff --git a/dapp/src/store/devTools.ts b/dapp/src/store/devTools.ts new file mode 100644 index 000000000..1edd8dee7 --- /dev/null +++ b/dapp/src/store/devTools.ts @@ -0,0 +1,23 @@ +import { DevToolsEnhancerOptions } from "@reduxjs/toolkit" +import { encodeJSON } from "#/utils" + +function devToolsSanitizer(input: unknown): unknown { + switch (typeof input) { + // We can make use of encodeJSON instead of recursively looping through + // the input + case "bigint": + case "object": + // We only need to sanitize bigints and objects + // that may or may not contain them. + return JSON.parse(encodeJSON(input)) + default: + return input + } +} + +export const devTools = !import.meta.env.PROD + ? ({ + actionSanitizer: devToolsSanitizer, + stateSanitizer: devToolsSanitizer, + } as DevToolsEnhancerOptions) + : false diff --git a/dapp/src/store/index.ts b/dapp/src/store/index.ts index fc68092ae..3353ac94a 100644 --- a/dapp/src/store/index.ts +++ b/dapp/src/store/index.ts @@ -1,11 +1,12 @@ import { configureStore } from "@reduxjs/toolkit" import { middleware } from "./middleware" import { reducer } from "./reducer" +import { devTools } from "./devTools" export const store = configureStore({ reducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware(middleware), - devTools: !import.meta.env.PROD, + devTools, }) export type RootState = ReturnType diff --git a/dapp/src/store/middleware.ts b/dapp/src/store/middleware.ts index 451fd90f1..80413c5ce 100644 --- a/dapp/src/store/middleware.ts +++ b/dapp/src/store/middleware.ts @@ -1 +1,8 @@ -export const middleware = {} +import { isPlain } from "@reduxjs/toolkit" + +export const middleware = { + serializableCheck: { + isSerializable: (value: unknown) => + isPlain(value) || typeof value === "bigint", + }, +} diff --git a/dapp/src/theme/utils/index.ts b/dapp/src/theme/utils/index.ts index d2fad7e05..c7cef473b 100644 --- a/dapp/src/theme/utils/index.ts +++ b/dapp/src/theme/utils/index.ts @@ -2,3 +2,4 @@ export * from "./colors" export * from "./fonts" export * from "./zIndices" export * from "./semanticTokens" +export * from "./units" diff --git a/dapp/src/theme/utils/units.ts b/dapp/src/theme/utils/units.ts new file mode 100644 index 000000000..c6c7df1b8 --- /dev/null +++ b/dapp/src/theme/utils/units.ts @@ -0,0 +1,7 @@ +// The values are proportional, so 1 spacing unit is equal to 0.25rem, which translates to 4px by default in common browsers. +const chakraSpacingUnit = { + px: 4, + rem: 0.25, +} +export const chakraUnitToPx = (chakraUnit: number): number => + chakraUnit * chakraSpacingUnit.px diff --git a/dapp/src/utils/index.ts b/dapp/src/utils/index.ts index cb7034c47..36f75497e 100644 --- a/dapp/src/utils/index.ts +++ b/dapp/src/utils/index.ts @@ -8,3 +8,4 @@ export * from "./time" export * from "./promise" export * from "./exchangeApi" export * from "./verifyDepositAddress" +export * from "./json" diff --git a/dapp/src/utils/json.ts b/dapp/src/utils/json.ts new file mode 100644 index 000000000..4214e25db --- /dev/null +++ b/dapp/src/utils/json.ts @@ -0,0 +1,13 @@ +/** + * Encode an unknown input as JSON, special-casing bigints and undefined. + * + * @param input an object, array, or primitive to encode as JSON + */ +export function encodeJSON(input: unknown) { + return JSON.stringify(input, (_, value: unknown) => { + if (typeof value === "bigint") { + return { B_I_G_I_N_T: value.toString() } + } + return value + }) +} 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 cbf6ccdfc..a58ff6a75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,118 +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) - '@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': @@ -162,6 +50,9 @@ importers: '@tanstack/react-table': specifier: ^8.11.3 version: 8.11.7(react-dom@18.2.0)(react@18.2.0) + '@types/react-slick': + specifier: ^0.23.13 + version: 0.23.13 axios: specifier: ^1.6.7 version: 1.6.7(debug@4.3.4) @@ -189,6 +80,9 @@ importers: react-router-dom: specifier: ^6.22.0 version: 6.22.1(react-dom@18.2.0)(react@18.2.0) + react-slick: + specifier: ^0.30.2 + version: 0.30.2(react-dom@18.2.0)(react@18.2.0) recharts: specifier: ^2.12.0 version: 2.12.1(react-dom@18.2.0)(react@18.2.0) @@ -235,9 +129,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) @@ -276,6 +170,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': @@ -4491,7 +4500,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 @@ -4501,10 +4510,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 @@ -4626,8 +4635,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 @@ -5119,7 +5128,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 @@ -5127,24 +5136,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: @@ -5160,7 +5169,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 @@ -5181,17 +5190,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) @@ -5327,6 +5336,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: @@ -5550,7 +5560,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: @@ -5562,7 +5572,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) @@ -5573,7 +5583,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 @@ -6806,21 +6816,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 @@ -6828,8 +6838,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) @@ -7212,6 +7222,12 @@ packages: '@types/react': 18.2.38 dev: true + /@types/react-slick@0.23.13: + resolution: {integrity: sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==} + dependencies: + '@types/react': 18.2.38 + dev: false + /@types/react@18.2.38: resolution: {integrity: sha512-cBBXHzuPtQK6wNthuVMV6IjHAFkdl/FOPFIlkd81/Cd1+IqkHu/A+w4g43kaQQoYHik/ruaQBDL72HyCy1vuMw==} dependencies: @@ -8287,7 +8303,7 @@ 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) + follow-redirects: 1.15.5(debug@4.3.4) transitivePeerDependencies: - debug @@ -9372,6 +9388,10 @@ packages: node-gyp-build: 4.7.0 dev: true + /classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + dev: false + /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -10832,6 +10852,10 @@ packages: graceful-fs: 4.2.11 tapable: 2.2.1 + /enquire.js@2.1.6: + resolution: {integrity: sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==} + dev: false + /enquirer@2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} engines: {node: '>=8.6'} @@ -11815,23 +11839,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==} @@ -12280,17 +12287,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(debug@4.3.4): resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} engines: {node: '>=4.0'} @@ -15249,6 +15245,12 @@ packages: delimit-stream: 0.1.0 dev: false + /json2mq@0.2.0: + resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==} + dependencies: + string-convert: 0.2.1 + dev: false + /json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -18032,6 +18034,21 @@ packages: react: 18.2.0 dev: false + /react-slick@0.30.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-XvQJi7mRHuiU3b9irsqS9SGIgftIfdV5/tNcURTb5LdIokRA5kIIx3l4rlq2XYHfxcSntXapoRg/GxaVOM1yfg==} + peerDependencies: + react: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + classnames: 2.5.1 + enquire.js: 2.1.6 + json2mq: 0.2.0 + lodash.debounce: 4.0.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + resize-observer-polyfill: 1.5.1 + dev: false + /react-smooth@4.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2NMXOBY1uVUQx1jBeENGA497HK20y6CPGYL1ZnJLeoQ8rrc3UfmOM82sRxtzpcoCkUMy4CS0RGylfuVhuFjBgg==} peerDependencies: @@ -18367,6 +18384,10 @@ packages: resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==} dev: false + /resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + dev: false + /resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -18963,7 +18984,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 @@ -19203,6 +19224,10 @@ packages: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} + /string-convert@0.2.1: + resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} + dev: false + /string-format@2.0.0: resolution: {integrity: sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==} dev: true @@ -22031,6 +22056,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: @@ -22154,6 +22180,14 @@ packages: '@openzeppelin/contracts': 4.9.5 dev: false + github.com/thesis/solidity-contracts/c315b9d: + resolution: {tarball: https://codeload.github.com/thesis/solidity-contracts/tar.gz/c315b9d} + name: '@thesis-co/solidity-contracts' + version: 0.0.1-pre + dependencies: + '@openzeppelin/contracts': 4.9.5 + dev: false + github.com/umpirsky/country-list/05fda51: resolution: {tarball: https://codeload.github.com/umpirsky/country-list/tar.gz/05fda51} name: '@umpirsky/country-list' 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 960bc4e1f..f221f39ce 100644 --- a/sdk/src/lib/contracts/bitcoin-depositor.ts +++ b/sdk/src/lib/contracts/bitcoin-depositor.ts @@ -5,12 +5,12 @@ import { DepositorProxy } from "./depositor-proxy" export { DepositReceipt } from "@keep-network/tbtc-v2.ts" export type DecodedExtraData = { - staker: ChainIdentifier + depositOwner: ChainIdentifier referral: number } /** - * Interface for communication with the AcreBitcoinDepositor on-chain contract. + * Interface for communication with the BitcoinDepositor on-chain contract. */ export interface BitcoinDepositor extends DepositorProxy { /** @@ -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/core/deployments/sepolia/AcreBitcoinDepositor.json b/sdk/src/lib/ethereum/artifacts/sepolia/BitcoinDepositor.json similarity index 100% rename from core/deployments/sepolia/AcreBitcoinDepositor.json rename to sdk/src/lib/ethereum/artifacts/sepolia/BitcoinDepositor.json diff --git a/sdk/src/lib/ethereum/bitcoin-depositor.ts b/sdk/src/lib/ethereum/bitcoin-depositor.ts index e5b33ca80..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 { AcreBitcoinDepositor as AcreBitcoinDepositorTypechain } from "@acre-btc/core/typechain/contracts/AcreBitcoinDepositor" +import { BitcoinDepositor as BitcoinDepositorTypechain } from "@acre-btc/contracts/typechain/contracts/BitcoinDepositor" import { ZeroAddress, dataSlice, @@ -24,7 +24,7 @@ import { import { Hex } from "../utils" import { EthereumNetwork } from "./network" -import SepoliaBitcoinDepositor from "./artifacts/sepolia/AcreBitcoinDepositor.json" +import SepoliaBitcoinDepositor from "./artifacts/sepolia/BitcoinDepositor.json" /** * Ethereum implementation of the BitcoinDepositor. @@ -32,8 +32,8 @@ import SepoliaBitcoinDepositor from "./artifacts/sepolia/AcreBitcoinDepositor.js class EthereumBitcoinDepositor // @ts-expect-error TODO: Figure out why type generated by typechain does not // satisfy the constraint `Contract`. Error: `Property '[internal]' is missing - // in type 'AcreBitcoinDepositor' but required in type 'Contract'`. - extends EthersContractWrapper + // in type 'BitcoinDepositor' but required in type 'Contract'`. + extends EthersContractWrapper implements BitcoinDepositor { constructor(config: EthersContractConfig, network: EthereumNetwork) { @@ -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 4d66a111c..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. @@ -120,7 +121,7 @@ class StakeInitialization { */ #getStakeMessageTypedData() { const domain: Domain = { - name: "AcreBitcoinDepositor", + name: "BitcoinDepositor", version: "1", verifyingContract: this.#contracts.bitcoinDepositor.getChainIdentifier(), } 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/eip712.test.ts b/sdk/test/lib/ethereum/eip712.test.ts index 5e342e0ac..7395d8497 100644 --- a/sdk/test/lib/ethereum/eip712.test.ts +++ b/sdk/test/lib/ethereum/eip712.test.ts @@ -7,7 +7,7 @@ import { const signMessageData = { domain: { - name: "AcreBitcoinDepositor", + name: "BitcoinDepositor", version: "1", verifyingContract: EthereumAddress.from( ethers.Wallet.createRandom().address, 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/sdk/test/modules/staking.test.ts b/sdk/test/modules/staking.test.ts index 664e54767..6eaca2ead 100644 --- a/sdk/test/modules/staking.test.ts +++ b/sdk/test/modules/staking.test.ts @@ -171,7 +171,7 @@ describe("Staking", () => { it("should sign message", () => { expect(messageSigner.sign).toHaveBeenCalledWith( { - name: "AcreBitcoinDepositor", + name: "BitcoinDepositor", version: "1", verifyingContract: contracts.bitcoinDepositor.getChainIdentifier(), 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/AcreBitcoinDepositor.sol b/solidity/contracts/BitcoinDepositor.sol similarity index 65% rename from core/contracts/AcreBitcoinDepositor.sol rename to solidity/contracts/BitcoinDepositor.sol index 0c1ade254..2251dbd57 100644 --- a/core/contracts/AcreBitcoinDepositor.sol +++ b/solidity/contracts/BitcoinDepositor.sol @@ -11,14 +11,14 @@ import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol"; import {stBTC} from "./stBTC.sol"; -/// @title Acre Bitcoin Depositor contract. -/// @notice The contract integrates Acre staking with tBTC minting. -/// User who wants to stake BTC in Acre should submit a Bitcoin transaction +/// @title Bitcoin Depositor contract. +/// @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,25 +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. -contract AcreBitcoinDepositor is - AbstractTBTCDepositor, - Ownable2StepUpgradeable -{ +/// 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; @@ -58,13 +62,13 @@ contract AcreBitcoinDepositor is /// @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, @@ -72,29 +76,29 @@ contract AcreBitcoinDepositor is /// `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, @@ -103,10 +107,10 @@ contract AcreBitcoinDepositor is 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. @@ -118,14 +122,13 @@ contract AcreBitcoinDepositor is /// 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. @@ -134,10 +137,10 @@ contract AcreBitcoinDepositor is 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 ); @@ -146,7 +149,7 @@ contract AcreBitcoinDepositor is _disableInitializers(); } - /// @notice Acre Bitcoin Depositor contract initializer. + /// @notice Bitcoin Depositor contract initializer. /// @param bridge tBTC Bridge contract instance. /// @param tbtcVault tBTC Vault contract instance. /// @param _tbtcToken tBTC token contract instance. @@ -172,18 +175,18 @@ contract AcreBitcoinDepositor is 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. @@ -191,15 +194,15 @@ contract AcreBitcoinDepositor is /// 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 @@ -207,47 +210,47 @@ contract AcreBitcoinDepositor is (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, @@ -272,9 +275,9 @@ contract AcreBitcoinDepositor is 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, @@ -283,33 +286,33 @@ contract AcreBitcoinDepositor is 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. @@ -317,45 +320,35 @@ contract AcreBitcoinDepositor is 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/BitcoinRedeemer.sol b/solidity/contracts/BitcoinRedeemer.sol new file mode 100644 index 000000000..159a81dfd --- /dev/null +++ b/solidity/contracts/BitcoinRedeemer.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; + +import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol"; + +import "./stBTC.sol"; +import "./bridge/ITBTCToken.sol"; +import {ZeroAddress} from "./utils/Errors.sol"; + +/// @title Bitcoin Redeemer +/// @notice This contract facilitates redemption of stBTC tokens to Bitcoin through +/// tBTC redemption process. +contract BitcoinRedeemer is Ownable2StepUpgradeable, IReceiveApproval { + /// Interface for tBTC token contract. + ITBTCToken public tbtcToken; + + /// stBTC token contract. + stBTC public stbtc; + + /// Address of the TBTCVault contract. + address public tbtcVault; + + /// Emitted when the TBTCVault contract address is updated. + /// @param oldTbtcVault Address of the old TBTCVault contract. + /// @param newTbtcVault Address of the new TBTCVault contract. + event TbtcVaultUpdated(address oldTbtcVault, address newTbtcVault); + + /// Emitted when redemption is requested. + /// @param owner Owner of stBTC tokens. + /// @param shares Number of stBTC tokens. + /// @param tbtcAmount Number of tBTC tokens. + event RedemptionRequested( + address indexed owner, + uint256 shares, + uint256 tbtcAmount + ); + + /// Reverts if the tBTC Token address is zero. + error TbtcTokenZeroAddress(); + + /// Reverts if the stBTC address is zero. + error StbtcZeroAddress(); + + /// Reverts if the TBTCVault address is zero. + error TbtcVaultZeroAddress(); + + /// Attempted to call receiveApproval for not supported token. + error UnsupportedToken(address token); + + /// Attempted to call receiveApproval by supported token. + error CallerNotAllowed(address caller); + + /// Attempted to call receiveApproval with empty data. + error EmptyExtraData(); + + /// Attempted to call redeemSharesAndUnmint with unexpected tBTC token owner. + error UnexpectedTbtcTokenOwner(); + + /// Reverts when approveAndCall to tBTC contract fails. + error ApproveAndCallFailed(); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the contract with tBTC token and stBTC token addresses. + /// @param _tbtcToken The address of the tBTC token contract. + /// @param _stbtc The address of the stBTC token contract. + /// @param _tbtcVault The address of the TBTCVault contract. + function initialize( + address _tbtcToken, + address _stbtc, + address _tbtcVault + ) public initializer { + __Ownable2Step_init(); + __Ownable_init(msg.sender); + + if (address(_tbtcToken) == address(0)) { + revert TbtcTokenZeroAddress(); + } + if (address(_stbtc) == address(0)) { + revert StbtcZeroAddress(); + } + if (address(_tbtcVault) == address(0)) { + revert TbtcVaultZeroAddress(); + } + + tbtcToken = ITBTCToken(_tbtcToken); + stbtc = stBTC(_stbtc); + tbtcVault = _tbtcVault; + } + + /// @notice Redeems shares for tBTC and requests bridging to Bitcoin. + /// @param from Shares token holder executing redemption. + /// @param amount Amount of shares to redeem. + /// @param token stBTC token address. + /// @param extraData Redemption data in a format expected from + /// `redemptionData` parameter of Bridge's `receiveBalanceApproval` + /// function. + function receiveApproval( + address from, + uint256 amount, + address token, + bytes calldata extraData + ) external { + if (token != address(stbtc)) revert UnsupportedToken(token); + if (msg.sender != token) revert CallerNotAllowed(msg.sender); + if (extraData.length == 0) revert EmptyExtraData(); + + redeemSharesAndUnmint(from, amount, extraData); + } + + /// @notice Updates TBTCVault contract address. + /// @param newTbtcVault New TBTCVault contract address. + function updateTbtcVault(address newTbtcVault) external onlyOwner { + if (newTbtcVault == address(0)) { + revert ZeroAddress(); + } + + emit TbtcVaultUpdated(tbtcVault, newTbtcVault); + + tbtcVault = newTbtcVault; + } + + /// @notice Initiates the redemption process by exchanging stBTC tokens for + /// tBTC tokens and requesting bridging to Bitcoin. + /// @dev Redeems stBTC shares to receive tBTC and requests redemption of tBTC + /// to Bitcoin via tBTC Bridge. + /// Redemption data in a format expected from `redemptionData` parameter + /// of Bridge's `receiveBalanceApproval`. + /// It uses tBTC token owner which is the TBTCVault contract as spender + /// of tBTC requested for redemption. + /// @dev tBTC Bridge redemption process has a path where request can timeout. + /// It is a scenario that is unlikely to happen with the current Bridge + /// 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 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. + /// @param shares The number of stBTC tokens to redeem. + /// @param tbtcRedemptionData Additional data required for the tBTC redemption. + /// See `redemptionData` parameter description of `Bridge.requestRedemption` + /// function. + function redeemSharesAndUnmint( + address owner, + uint256 shares, + bytes calldata tbtcRedemptionData + ) internal { + // TBTC Token contract owner resolves to the TBTCVault contract. + if (tbtcToken.owner() != tbtcVault) revert UnexpectedTbtcTokenOwner(); + + uint256 tbtcAmount = stbtc.redeem(shares, address(this), owner); + + // slither-disable-next-line reentrancy-events + emit RedemptionRequested(owner, shares, tbtcAmount); + + if ( + !tbtcToken.approveAndCall(tbtcVault, tbtcAmount, tbtcRedemptionData) + ) { + revert ApproveAndCallFailed(); + } + } +} 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/solidity/contracts/bridge/ITBTCToken.sol b/solidity/contracts/bridge/ITBTCToken.sol new file mode 100644 index 000000000..7b660ec06 --- /dev/null +++ b/solidity/contracts/bridge/ITBTCToken.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +/// @title Interface of TBTC token contract. +/// @notice This interface defines functions of TBTC token contract used by Acre +/// contracts. +interface ITBTCToken { + /// @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 + /// overwrites the current allowance with `amount`. Reverts if the + /// approval reverted or if `receiveApproval` call on the spender + /// reverted. + /// @return True if both approval and `receiveApproval` calls succeeded. + /// @dev If the `amount` is set to `type(uint256).max` then + /// `transferFrom` and `burnFrom` will not reduce an allowance. + function approveAndCall( + address spender, + uint256 amount, + bytes memory extraData + ) external returns (bool); + + /// @dev Returns the address of the contract owner. + function owner() external view returns (address); +} 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 73% rename from core/contracts/stBTC.sol rename to solidity/contracts/stBTC.sol index 37f86b49e..613c8c301 100644 --- a/core/contracts/stBTC.sol +++ b/solidity/contracts/stBTC.sol @@ -3,27 +3,30 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "./Dispatcher.sol"; +import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.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 /// tokens, providing a seamless exchange with tBTC tokens. +// slither-disable-next-line missing-inheritance 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; @@ -42,8 +45,9 @@ contract stBTC is ERC4626Fees, PausableOwnable { uint256 public exitFeeBasisPoints; /// Emitted when the treasury wallet address is updated. - /// @param treasury New treasury wallet address. - event TreasuryUpdated(address treasury); + /// @param oldTreasury Address of the old treasury wallet. + /// @param newTreasury Address of the new treasury wallet. + event TreasuryUpdated(address oldTreasury, address newTreasury); /// Emitted when deposit parameters are updated. /// @param minimumDepositAmount New value of the minimum deposit amount. @@ -94,16 +98,16 @@ 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(); } if (newTreasury == address(this)) { revert DisallowedAddress(); } - treasury = newTreasury; - emit TreasuryUpdated(newTreasury); + emit TreasuryUpdated(treasury, newTreasury); + + treasury = newTreasury; } /// @notice Updates minimum deposit amount. @@ -112,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(); } @@ -133,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); @@ -146,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( @@ -158,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( @@ -170,6 +163,39 @@ 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 + /// overwrites the current allowance with `amount`. Reverts if the + /// approval reverted or if `receiveApproval` call on the spender + /// reverted. + /// @return True if both approval and `receiveApproval` calls succeeded. + /// @dev If the `amount` is set to `type(uint256).max` then + /// `transferFrom` and `burnFrom` will not reduce an allowance. + function approveAndCall( + address spender, + uint256 value, + bytes memory extraData + ) external returns (bool) { + if (approve(spender, value)) { + IReceiveApproval(spender).receiveApproval( + msg.sender, + value, + address(this), + extraData + ); + return true; + } + return false; + } + /// @notice Mints shares to receiver by depositing exactly amount of /// tBTC tokens. /// @dev Takes into account a deposit parameter, minimum deposit amount, @@ -214,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/AcreBitcoinDepositorHarness.sol b/solidity/contracts/test/BitcoinDepositorHarness.sol similarity index 96% rename from core/contracts/test/AcreBitcoinDepositorHarness.sol rename to solidity/contracts/test/BitcoinDepositorHarness.sol index 47f61d9d3..41e8791fc 100644 --- a/core/contracts/test/AcreBitcoinDepositorHarness.sol +++ b/solidity/contracts/test/BitcoinDepositorHarness.sol @@ -2,7 +2,7 @@ /* solhint-disable func-name-mixedcase */ pragma solidity ^0.8.21; -import {AcreBitcoinDepositor} from "../AcreBitcoinDepositor.sol"; +import {BitcoinDepositor} from "../BitcoinDepositor.sol"; import {MockBridge, MockTBTCVault} from "@keep-network/tbtc-v2/contracts/test/TestTBTCDepositor.sol"; import {IBridge} from "@keep-network/tbtc-v2/contracts/integrator/IBridge.sol"; import {IBridgeTypes} from "@keep-network/tbtc-v2/contracts/integrator/IBridge.sol"; 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/solidity/contracts/test/TestTBTC.sol b/solidity/contracts/test/TestTBTC.sol new file mode 100644 index 000000000..e6ed5152a --- /dev/null +++ b/solidity/contracts/test/TestTBTC.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "../bridge/ITBTCToken.sol"; + +contract TestTBTC is ITBTCToken, ERC20 { + event ApproveAndCallCalled( + address spender, + uint256 amount, + bytes extraData + ); + + bool public approveAndCallResult = true; + + address public owner; + + constructor(string memory name, string memory symbol) ERC20(name, symbol) { + owner = address(1); + } + + function mint(address account, uint256 value) external { + _mint(account, value); + } + + function approveAndCall( + address spender, + uint256 amount, + bytes memory extraData + ) external returns (bool) { + emit ApproveAndCallCalled(spender, amount, extraData); + + return approveAndCallResult; + } + + function setApproveAndCallResult(bool value) public { + approveAndCallResult = value; + } + + function setOwner(address newOwner) public { + owner = newOwner; + } +} diff --git a/core/contracts/test/upgrades/AcreBitcoinDepositorV2.sol b/solidity/contracts/test/upgrades/BitcoinDepositorV2.sol similarity index 62% rename from core/contracts/test/upgrades/AcreBitcoinDepositorV2.sol rename to solidity/contracts/test/upgrades/BitcoinDepositorV2.sol index ad82e9f5b..85f02f3c8 100644 --- a/core/contracts/test/upgrades/AcreBitcoinDepositorV2.sol +++ b/solidity/contracts/test/upgrades/BitcoinDepositorV2.sol @@ -11,26 +11,30 @@ import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol"; import {stBTC} from "../../stBTC.sol"; -/// @title AcreBitcoinDepositorV2 -/// @dev This is a contract used to test Acre Bitcoin Depositor upgradeability. -/// It is a copy of AcreBitcoinDepositor contract with some differences +/// @title BitcoinDepositorV2 +/// @dev This is a contract used to test Bitcoin Depositor upgradeability. +/// It is a copy of BitcoinDepositor contract with some differences /// marked with `TEST:` comments. -contract AcreBitcoinDepositorV2 is - AbstractTBTCDepositor, - Ownable2StepUpgradeable -{ +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; @@ -38,13 +42,13 @@ contract AcreBitcoinDepositorV2 is /// @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, @@ -55,29 +59,29 @@ contract AcreBitcoinDepositorV2 is // 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, @@ -86,10 +90,10 @@ contract AcreBitcoinDepositorV2 is 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. @@ -104,14 +108,13 @@ contract AcreBitcoinDepositorV2 is /// 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. @@ -120,10 +123,10 @@ contract AcreBitcoinDepositorV2 is 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 ); @@ -142,14 +145,14 @@ contract AcreBitcoinDepositorV2 is 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. @@ -157,15 +160,15 @@ contract AcreBitcoinDepositorV2 is /// 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 @@ -173,47 +176,47 @@ contract AcreBitcoinDepositorV2 is (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, @@ -238,9 +241,9 @@ contract AcreBitcoinDepositorV2 is 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, @@ -249,33 +252,33 @@ contract AcreBitcoinDepositorV2 is 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(); @@ -286,45 +289,35 @@ contract AcreBitcoinDepositorV2 is 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 84% rename from core/contracts/test/upgrades/stBTCV2.sol rename to solidity/contracts/test/upgrades/stBTCV2.sol index 3a83a172f..69cf8e8c6 100644 --- a/core/contracts/test/upgrades/stBTCV2.sol +++ b/solidity/contracts/test/upgrades/stBTCV2.sol @@ -3,9 +3,11 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "../../Dispatcher.sol"; +import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol"; + import "../../PausableOwnable.sol"; import "../../lib/ERC4626Fees.sol"; +import "../../interfaces/IDispatcher.sol"; import {ZeroAddress} from "../../utils/Errors.sol"; /// @title stBTCV2 @@ -15,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; @@ -37,8 +39,9 @@ contract stBTCV2 is ERC4626Fees, PausableOwnable { uint256 public newVariable; /// Emitted when the treasury wallet address is updated. - /// @param treasury New treasury wallet address. - event TreasuryUpdated(address treasury); + /// @param oldTreasury Address of the old treasury wallet. + /// @param newTreasury Address of the new treasury wallet. + event TreasuryUpdated(address oldTreasury, address newTreasury); /// Emitted when deposit parameters are updated. /// @param minimumDepositAmount New value of the minimum deposit amount. @@ -93,9 +96,10 @@ contract stBTCV2 is ERC4626Fees, PausableOwnable { if (newTreasury == address(this)) { revert DisallowedAddress(); } - treasury = newTreasury; - emit TreasuryUpdated(newTreasury); + emit TreasuryUpdated(treasury, newTreasury); + + treasury = newTreasury; } /// @notice Updates deposit parameters. @@ -113,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(); } @@ -162,6 +166,32 @@ contract stBTCV2 is ERC4626Fees, PausableOwnable { emit ExitFeeBasisPointsUpdated(newExitFeeBasisPoints); } + /// @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 + /// overwrites the current allowance with `amount`. Reverts if the + /// approval reverted or if `receiveApproval` call on the spender + /// reverted. + /// @return True if both approval and `receiveApproval` calls succeeded. + /// @dev If the `amount` is set to `type(uint256).max` then + /// `transferFrom` and `burnFrom` will not reduce an allowance. + function approveAndCall( + address spender, + uint256 value, + bytes memory extraData + ) external returns (bool) { + if (approve(spender, value)) { + IReceiveApproval(spender).receiveApproval( + _msgSender(), + value, + address(this), + extraData + ); + return true; + } + return false; + } + // TEST: Modified function. function deposit( uint256 assets, 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 97% rename from core/deploy/00_resolve_tbtc_token.ts rename to solidity/deploy/00_resolve_tbtc_token.ts index ef00bcdef..d1ddf731e 100644 --- a/core/deploy/00_resolve_tbtc_token.ts +++ b/solidity/deploy/00_resolve_tbtc_token.ts @@ -24,7 +24,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { log("deploying TBTC contract stub") await deployments.deploy("TBTC", { - contract: "TestERC20", + contract: "TestTBTC", args: ["Test tBTC", "TestTBTC"], from: deployer, log: true, diff --git a/core/deploy/00_resolve_tbtc_vault.ts b/solidity/deploy/00_resolve_tbtc_vault.ts similarity index 86% rename from core/deploy/00_resolve_tbtc_vault.ts rename to solidity/deploy/00_resolve_tbtc_vault.ts index 46edbd919..27034347f 100644 --- a/core/deploy/00_resolve_tbtc_vault.ts +++ b/solidity/deploy/00_resolve_tbtc_vault.ts @@ -26,13 +26,20 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const tbtc = await deployments.get("TBTC") const bridge = await deployments.get("Bridge") - await deployments.deploy("TBTCVault", { + const deployment = await deployments.deploy("TBTCVault", { contract: "TBTCVaultStub", args: [tbtc.address, bridge.address], from: deployer, log: true, waitConfirmations: waitConfirmationsNumber(hre), }) + + await deployments.execute( + "TBTC", + { from: deployer, log: true }, + "setOwner", + deployment.address, + ) } } 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_acre_bitcoin_depositor.ts b/solidity/deploy/03_deploy_bitcoin_depositor.ts similarity index 71% rename from core/deploy/03_deploy_acre_bitcoin_depositor.ts rename to solidity/deploy/03_deploy_bitcoin_depositor.ts index f2063d5c1..7de26abcb 100644 --- a/core/deploy/03_deploy_acre_bitcoin_depositor.ts +++ b/solidity/deploy/03_deploy_bitcoin_depositor.ts @@ -12,8 +12,8 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const tbtc = await deployments.get("TBTC") const stbtc = await deployments.get("stBTC") - const [, acreBitcoinDepositorDeployment] = await helpers.upgrades.deployProxy( - "AcreBitcoinDepositor", + const [, deployment] = await helpers.upgrades.deployProxy( + "BitcoinDepositor", { factoryOpts: { signer: deployer, @@ -31,15 +31,9 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { }, ) - if ( - acreBitcoinDepositorDeployment.transactionHash && - hre.network.tags.etherscan - ) { - await waitForTransaction( - hre, - acreBitcoinDepositorDeployment.transactionHash, - ) - await helpers.etherscan.verify(acreBitcoinDepositorDeployment) + if (deployment.transactionHash && hre.network.tags.etherscan) { + await waitForTransaction(hre, deployment.transactionHash) + await helpers.etherscan.verify(deployment) } // TODO: Add Tenderly verification @@ -47,5 +41,5 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func -func.tags = ["AcreBitcoinDepositor"] +func.tags = ["BitcoinDepositor"] func.dependencies = ["TBTC", "stBTC"] diff --git a/solidity/deploy/04_deploy_bitcoin_redeemer.ts b/solidity/deploy/04_deploy_bitcoin_redeemer.ts new file mode 100644 index 000000000..7dbc0d847 --- /dev/null +++ b/solidity/deploy/04_deploy_bitcoin_redeemer.ts @@ -0,0 +1,36 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import { waitForTransaction } from "../helpers/deployment" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { deployments, helpers } = hre + const { deployer } = await helpers.signers.getNamedSigners() + + const tbtc = await deployments.get("TBTC") + const stbtc = await deployments.get("stBTC") + const tbtcVault = await deployments.get("TBTCVault") + + const [_, deployment] = await helpers.upgrades.deployProxy( + "BitcoinRedeemer", + { + contractName: "BitcoinRedeemer", + initializerArgs: [tbtc.address, stbtc.address, tbtcVault.address], + factoryOpts: { signer: deployer }, + proxyOpts: { + kind: "transparent", + }, + }, + ) + + 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 = ["BitcoinRedeemer"] +func.dependencies = ["TBTC", "stBTC", "TBTCVault"] 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_acre_bitcoin_depositor.ts b/solidity/deploy/23_transfer_ownership_bitcoin_depositor.ts similarity index 73% rename from core/deploy/23_transfer_ownership_acre_bitcoin_depositor.ts rename to solidity/deploy/23_transfer_ownership_bitcoin_depositor.ts index 30095cd05..b1214346d 100644 --- a/core/deploy/23_transfer_ownership_acre_bitcoin_depositor.ts +++ b/solidity/deploy/23_transfer_ownership_bitcoin_depositor.ts @@ -6,12 +6,10 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployer, governance } = await getNamedAccounts() const { log } = deployments - log( - `transferring ownership of AcreBitcoinDepositor contract to ${governance}`, - ) + log(`transferring ownership of BitcoinDepositor contract to ${governance}`) await deployments.execute( - "AcreBitcoinDepositor", + "BitcoinDepositor", { from: deployer, log: true, waitConfirmations: 1 }, "transferOwnership", governance, @@ -19,7 +17,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { if (hre.network.name !== "mainnet") { await deployments.execute( - "AcreBitcoinDepositor", + "BitcoinDepositor", { from: governance, log: true, waitConfirmations: 1 }, "acceptOwnership", ) @@ -28,6 +26,6 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func -func.tags = ["TransferOwnershipAcreBitcoinDepositor"] -func.dependencies = ["AcreBitcoinDepositor"] +func.tags = ["TransferOwnershipBitcoinDepositor"] +func.dependencies = ["BitcoinDepositor"] func.runAtTheEnd = true diff --git a/core/deploy/22_transfer_ownership_dispatcher.ts b/solidity/deploy/24_transfer_ownership_bitcoin_redeemer.ts similarity index 76% rename from core/deploy/22_transfer_ownership_dispatcher.ts rename to solidity/deploy/24_transfer_ownership_bitcoin_redeemer.ts index 427cf388e..f3e78caee 100644 --- a/core/deploy/22_transfer_ownership_dispatcher.ts +++ b/solidity/deploy/24_transfer_ownership_bitcoin_redeemer.ts @@ -6,10 +6,10 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployer, governance } = await getNamedAccounts() const { log } = deployments - log(`transferring ownership of Dispatcher contract to ${governance}`) + log(`transferring ownership of BitcoinRedeemer contract to ${governance}`) await deployments.execute( - "Dispatcher", + "BitcoinRedeemer", { from: deployer, log: true, waitConfirmations: 1 }, "transferOwnership", governance, @@ -17,7 +17,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { if (hre.network.name !== "mainnet") { await deployments.execute( - "Dispatcher", + "BitcoinRedeemer", { from: governance, log: true, waitConfirmations: 1 }, "acceptOwnership", ) @@ -26,6 +26,6 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func -func.tags = ["TransferOwnershipDispatcher"] -func.dependencies = ["Dispatcher"] +func.tags = ["TransferOwnershipBitcoinRedeemer"] +func.dependencies = ["BitcoinRedeemer"] func.runAtTheEnd = true diff --git a/solidity/deploy/24_transfer_ownership_mezo_allocator.ts b/solidity/deploy/24_transfer_ownership_mezo_allocator.ts new file mode 100644 index 000000000..f6add8a43 --- /dev/null +++ b/solidity/deploy/24_transfer_ownership_mezo_allocator.ts @@ -0,0 +1,39 @@ +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 MezoAllocator contract to ${governance}`) + + await deployments.execute( + "MezoAllocator", + { + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }, + "transferOwnership", + governance, + ) + + if (hre.network.name !== "mainnet") { + await deployments.execute( + "MezoAllocator", + { + from: governance, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }, + "acceptOwnership", + ) + } +} + +export default func + +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/sdk/src/lib/ethereum/artifacts/sepolia/AcreBitcoinDepositor.json b/solidity/deployments/sepolia/BitcoinDepositor.json similarity index 100% rename from sdk/src/lib/ethereum/artifacts/sepolia/AcreBitcoinDepositor.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 95% rename from core/package.json rename to solidity/package.json index 26ec0fa79..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", @@ -65,6 +65,7 @@ "@keep-network/tbtc-v2": "development", "@openzeppelin/contracts": "^5.0.0", "@openzeppelin/contracts-upgradeable": "^5.0.2", + "@thesis-co/solidity-contracts": "github:thesis/solidity-contracts#c315b9d", "@types/chai-as-promised": "^7.1.8", "chai-as-promised": "^7.1.1" } 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/AcreBitcoinDepositor.test.ts b/solidity/test/BitcoinDepositor.test.ts similarity index 74% rename from core/test/AcreBitcoinDepositor.test.ts rename to solidity/test/BitcoinDepositor.test.ts index a106d3a92..79b7c82aa 100644 --- a/core/test/AcreBitcoinDepositor.test.ts +++ b/solidity/test/BitcoinDepositor.test.ts @@ -6,13 +6,13 @@ 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, BridgeStub, TBTCVaultStub, - AcreBitcoinDepositor, + BitcoinDepositor, TestERC20, } from "../typechain" import { deployment } from "./helpers" @@ -30,7 +30,7 @@ async function fixture() { const { lastBlockTime } = helpers.time const { getNamedSigners, getUnnamedSigners } = helpers.signers -describe("AcreBitcoinDepositor", () => { +describe("BitcoinDepositor", () => { const defaultDepositDustThreshold = 1000000 // 1000000 satoshi = 0.01 BTC const defaultDepositTreasuryFeeDivisor = 2000 // 1/2000 = 0.05% = 0.0005 const defaultDepositTxMaxFee = 1000 // 1000 satoshi = 0.00001 BTC @@ -46,9 +46,9 @@ describe("AcreBitcoinDepositor", () => { 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: AcreBitcoinDepositor + let bitcoinDepositor: BitcoinDepositor let tbtcBridge: BridgeStub let tbtcVault: TBTCVaultStub let stbtc: StBTC @@ -87,24 +87,27 @@ describe("AcreBitcoinDepositor", () => { .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("AcreBitcoinDepositor", () => { 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("AcreBitcoinDepositor", () => { 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("AcreBitcoinDepositor", () => { 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("AcreBitcoinDepositor", () => { }) }) - 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("AcreBitcoinDepositor", () => { }) }) - 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("AcreBitcoinDepositor", () => { 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("AcreBitcoinDepositor", () => { await expect( bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey), + .finalizeDeposit(tbtcDepositData.depositKey), ) .to.be.revertedWithCustomError( stbtc, @@ -309,7 +312,7 @@ describe("AcreBitcoinDepositor", () => { .withArgs( await bitcoinDepositor.getAddress(), mintedAmount - depositorFee, - amountToStake, + amountToDeposit, ) }) }) @@ -325,15 +328,15 @@ describe("AcreBitcoinDepositor", () => { 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("AcreBitcoinDepositor", () => { ) }) - 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("AcreBitcoinDepositor", () => { .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("AcreBitcoinDepositor", () => { 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("AcreBitcoinDepositor", () => { 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("AcreBitcoinDepositor", () => { .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("AcreBitcoinDepositor", () => { await expect( bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey), + .finalizeDeposit(tbtcDepositData.depositKey), ) .to.be.revertedWithCustomError( bitcoinDepositor, @@ -489,40 +493,37 @@ describe("AcreBitcoinDepositor", () => { }) }) - 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("AcreBitcoinDepositor", () => { 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("AcreBitcoinDepositor", () => { }) describe("when caller is governance", () => { - const testUpdateMinStakeAmount = (newValue: bigint) => + const testupdateMinDepositAmount = (newValue: bigint) => function () { beforeAfterSnapshotWrapper() @@ -550,17 +551,17 @@ describe("AcreBitcoinDepositor", () => { 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("AcreBitcoinDepositor", () => { // 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("AcreBitcoinDepositor", () => { 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("AcreBitcoinDepositor", () => { [ "referral is zero", { - staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + depositOwner: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", referral: 0, extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e000000000000000000000000", @@ -704,7 +705,7 @@ describe("AcreBitcoinDepositor", () => { [ "referral has leading zeros", { - staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + depositOwner: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", referral: 31, // hex: 0x001f extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e001f00000000000000000000", @@ -713,7 +714,7 @@ describe("AcreBitcoinDepositor", () => { [ "referral has trailing zeros", { - staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + depositOwner: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", referral: 19712, // hex: 0x4d00 extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e4d0000000000000000000000", @@ -722,7 +723,7 @@ describe("AcreBitcoinDepositor", () => { [ "referral is maximum value", { - staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + depositOwner: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", referral: 65535, // max uint16 extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89effff00000000000000000000", @@ -733,10 +734,10 @@ describe("AcreBitcoinDepositor", () => { 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("AcreBitcoinDepositor", () => { 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("AcreBitcoinDepositor", () => { // 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/AcreBitcoinDepositor.upgrade.test.ts b/solidity/test/BitcoinDepositor.upgrade.test.ts similarity index 79% rename from core/test/AcreBitcoinDepositor.upgrade.test.ts rename to solidity/test/BitcoinDepositor.upgrade.test.ts index c447c74ce..6194712d5 100644 --- a/core/test/AcreBitcoinDepositor.upgrade.test.ts +++ b/solidity/test/BitcoinDepositor.upgrade.test.ts @@ -8,10 +8,10 @@ import { beforeAfterSnapshotWrapper, deployment } from "./helpers" import { TestERC20, StBTC, - AcreBitcoinDepositor, + BitcoinDepositor, BridgeStub, TBTCVaultStub, - AcreBitcoinDepositorV2, + BitcoinDepositorV2, } from "../typechain" import { to1e18 } from "./utils" @@ -24,12 +24,12 @@ async function fixture() { return { tbtc, stbtc, bitcoinDepositor, tbtcBridge, tbtcVault } } -describe("AcreBitcoinDepositor contract upgrade", () => { +describe("BitcoinDepositor contract upgrade", () => { let tbtc: TestERC20 let tbtcBridge: BridgeStub let tbtcVault: TBTCVaultStub let stbtc: StBTC - let bitcoinDepositor: AcreBitcoinDepositor + let bitcoinDepositor: BitcoinDepositor let governance: HardhatEthersSigner before(async () => { @@ -40,27 +40,27 @@ describe("AcreBitcoinDepositor contract upgrade", () => { context("when upgrading to a valid contract", () => { const newVariable = 1n - let bitcoinDepositorV2: AcreBitcoinDepositorV2 + 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, } const [upgradedDepositor] = await helpers.upgrades.upgradeProxy( - "AcreBitcoinDepositor", - "AcreBitcoinDepositorV2", + "BitcoinDepositor", + "BitcoinDepositorV2", { factoryOpts: { signer: governance }, proxyOpts: { @@ -72,8 +72,7 @@ describe("AcreBitcoinDepositor contract upgrade", () => { }, ) - bitcoinDepositorV2 = - upgradedDepositor as unknown as AcreBitcoinDepositorV2 + bitcoinDepositorV2 = upgradedDepositor as unknown as BitcoinDepositorV2 }) it("new instance should have the same address as the old one", async () => { @@ -99,8 +98,8 @@ describe("AcreBitcoinDepositor 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, @@ -108,14 +107,14 @@ describe("AcreBitcoinDepositor 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/solidity/test/BitcoinRedeemer.test.ts b/solidity/test/BitcoinRedeemer.test.ts new file mode 100644 index 000000000..396391945 --- /dev/null +++ b/solidity/test/BitcoinRedeemer.test.ts @@ -0,0 +1,319 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" +import { expect } from "chai" +import { + ContractTransactionResponse, + encodeBytes32String, + ZeroAddress, +} from "ethers" +import { ethers, helpers } from "hardhat" + +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" +import { beforeAfterSnapshotWrapper, deployment } from "./helpers" + +import { to1e18 } from "./utils" + +import type { StBTC as stBTC, BitcoinRedeemer, TestTBTC } from "../typechain" +import { tbtcRedemptionData } from "./data/tbtc" + +const { getNamedSigners, getUnnamedSigners } = helpers.signers + +async function fixture() { + const { tbtc, stbtc, bitcoinRedeemer } = await deployment() + + const { governance } = await getNamedSigners() + + const [depositor, thirdParty] = await getUnnamedSigners() + + const amountToMint = to1e18(100000) + await tbtc.mint(depositor, amountToMint) + + return { + stbtc, + tbtc, + bitcoinRedeemer, + governance, + depositor, + thirdParty, + } +} + +describe("BitcoinRedeemer", () => { + let stbtc: stBTC + let tbtc: TestTBTC + let bitcoinRedeemer: BitcoinRedeemer + + let governance: HardhatEthersSigner + let depositor: HardhatEthersSigner + let thirdParty: HardhatEthersSigner + + before(async () => { + ;({ stbtc, tbtc, bitcoinRedeemer, governance, depositor, thirdParty } = + await loadFixture(fixture)) + }) + + describe("receiveApproval", () => { + context("when called not for stBTC token", () => { + it("should revert", async () => { + await expect( + bitcoinRedeemer + .connect(depositor) + .receiveApproval( + depositor.address, + to1e18(1), + depositor.address, + encodeBytes32String(""), + ), + ).to.be.revertedWithCustomError(bitcoinRedeemer, "UnsupportedToken") + }) + }) + + context("when called directly", () => { + it("should revert", async () => { + await expect( + bitcoinRedeemer + .connect(depositor) + .receiveApproval( + depositor.address, + to1e18(1), + await stbtc.getAddress(), + encodeBytes32String(""), + ), + ).to.be.revertedWithCustomError(bitcoinRedeemer, "CallerNotAllowed") + }) + }) + + context("when called via approveAndCall", () => { + context("when called with empty extraData", () => { + it("should revert", async () => { + await expect( + stbtc + .connect(depositor) + .approveAndCall( + await bitcoinRedeemer.getAddress(), + to1e18(1), + "0x", + ), + ).to.be.revertedWithCustomError(bitcoinRedeemer, "EmptyExtraData") + }) + }) + + context("when called with non-empty extraData", () => { + context("when TBTC token owner doesn't match TBTCVault", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + const newOwner = await ethers.Wallet.createRandom().getAddress() + await tbtc.setOwner(newOwner) + }) + + it("should revert", async () => { + await expect( + stbtc + .connect(depositor) + .approveAndCall( + await bitcoinRedeemer.getAddress(), + to1e18(1), + tbtcRedemptionData.redemptionData, + ), + ).to.be.revertedWithCustomError( + bitcoinRedeemer, + "UnexpectedTbtcTokenOwner", + ) + }) + }) + + context("when TBTC token owner matches TBTCVault", () => { + context("when caller has no deposit", () => { + it("should revert", async () => { + await expect( + stbtc + .connect(depositor) + .approveAndCall( + await bitcoinRedeemer.getAddress(), + to1e18(1), + tbtcRedemptionData.redemptionData, + ), + ).to.be.revertedWithCustomError(stbtc, "ERC20InsufficientBalance") + }) + }) + + context("when caller has deposit", () => { + beforeAfterSnapshotWrapper() + + const depositAmount = to1e18(10) + const earnedYield = to1e18(8) + + before(async () => { + await tbtc + .connect(depositor) + .approve(await stbtc.getAddress(), depositAmount) + await stbtc + .connect(depositor) + .deposit(depositAmount, depositor.address) + + await tbtc.mint(await stbtc.getAddress(), earnedYield) + }) + + context("when redeeming too many tokens", () => { + const amountToRedeem = depositAmount + 1n + it("should revert", async () => { + await expect( + stbtc + .connect(depositor) + .approveAndCall( + await bitcoinRedeemer.getAddress(), + amountToRedeem, + tbtcRedemptionData.redemptionData, + ), + ).to.be.revertedWithCustomError( + stbtc, + "ERC20InsufficientBalance", + ) + }) + }) + + context("when redeeming deposit partially", () => { + const stBtcAmountToRedeem = to1e18(6) + // 6 / 10 * (10 + 8) = 10.8 + // 10.7(9) to match stBTC calculations rounding. + const tbtcAmountToRedeem = 10799999999999999999n + + context("when tBTC.approveAndCall returns true", () => { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await stbtc + .connect(depositor) + .approveAndCall( + await bitcoinRedeemer.getAddress(), + stBtcAmountToRedeem, + tbtcRedemptionData.redemptionData, + ) + }) + + it("should emit RedemptionRequested event", async () => { + await expect(tx) + .to.emit(bitcoinRedeemer, "RedemptionRequested") + .withArgs( + depositor.address, + stBtcAmountToRedeem, + tbtcAmountToRedeem, + ) + }) + + it("should change stBTC tokens balance", async () => { + await expect(tx).to.changeTokenBalances( + stbtc, + [depositor], + [-stBtcAmountToRedeem], + ) + }) + + it("should burn stBTC tokens", async () => { + expect(await stbtc.totalSupply()).to.be.equal( + depositAmount - stBtcAmountToRedeem, + ) + }) + + it("should transfer tBTC tokens", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [stbtc, bitcoinRedeemer], + [-tbtcAmountToRedeem, tbtcAmountToRedeem], + ) + }) + + it("should call approveAndCall in tBTC contract", async () => { + await expect(tx) + .to.emit(tbtc, "ApproveAndCallCalled") + .withArgs( + await tbtc.owner(), + tbtcAmountToRedeem, + tbtcRedemptionData.redemptionData, + ) + }) + }) + + context("when tBTC.approveAndCall returns false", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await tbtc.setApproveAndCallResult(false) + }) + + it("should revert", async () => { + await expect( + stbtc + .connect(depositor) + .approveAndCall( + await bitcoinRedeemer.getAddress(), + stBtcAmountToRedeem, + tbtcRedemptionData.redemptionData, + ), + ).to.be.revertedWithCustomError( + bitcoinRedeemer, + "ApproveAndCallFailed", + ) + }) + }) + }) + }) + }) + }) + }) + }) + + describe("updateTbtcVault", () => { + beforeAfterSnapshotWrapper() + + context("when caller is not governance", () => { + it("should revert", async () => { + await expect( + bitcoinRedeemer.connect(thirdParty).updateTbtcVault(ZeroAddress), + ) + .to.be.revertedWithCustomError( + bitcoinRedeemer, + "OwnableUnauthorizedAccount", + ) + .withArgs(thirdParty.address) + }) + }) + + context("when caller is governance", () => { + context("when a new tbtc vault is zero address", () => { + it("should revert", async () => { + await expect( + bitcoinRedeemer.connect(governance).updateTbtcVault(ZeroAddress), + ).to.be.revertedWithCustomError(bitcoinRedeemer, "ZeroAddress") + }) + }) + + context("when a new treasury is an allowed address", () => { + let oldTbtcVault: string + let newTbtcVault: string + let tx: ContractTransactionResponse + + before(async () => { + oldTbtcVault = await bitcoinRedeemer.tbtcVault() + newTbtcVault = await ethers.Wallet.createRandom().getAddress() + + tx = await bitcoinRedeemer + .connect(governance) + .updateTbtcVault(newTbtcVault) + }) + + it("should update the tbtc vault", async () => { + expect(await bitcoinRedeemer.tbtcVault()).to.be.equal(newTbtcVault) + }) + + it("should emit TbtcVaultUpdated event", async () => { + await expect(tx) + .to.emit(bitcoinRedeemer, "TbtcVaultUpdated") + .withArgs(oldTbtcVault, newTbtcVault) + }) + }) + }) + }) +}) 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 72% rename from core/test/data/tbtc.ts rename to solidity/test/data/tbtc.ts index fd491081f..923635e4f 100644 --- a/core/test/data/tbtc.ts +++ b/solidity/test/data/tbtc.ts @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - import { ethers } from "hardhat" // TODO: Revisit the data once full integration is tested on testnet with valid @@ -37,7 +35,7 @@ export const tbtcDepositData = { vault: "0x594cfd89700040163727828AE20B52099C58F02C", }, // 20-bytes of extraData - staker: "0xa9B38eA6435c8941d6eDa6a46b68E3e211719699", + depositOwner: "0xa9B38eA6435c8941d6eDa6a46b68E3e211719699", // 2-bytes of extraData referral: "0x5bd1", extraData: @@ -47,3 +45,10 @@ export const tbtcDepositData = { "0x8dde6118338ae2a046eb77a4acceb0521699275f9cc8e9b50057b29d9de1e844", ), } + +// Fixture used for tBTC redemptions. +// Source: // https://etherscan.io/tx/0xac0ae065b093d53b4af2749fa974d2a2cea21e1d9e1a872d4717e440c521265a +export const tbtcRedemptionData = { + redemptionData: + "0x0000000000000000000000004b9826faf6c88d5d979fd1dd66564525f44c876e1a7e037e81765655a195e64daeea03dde3cae199000000000000000000000000adc7b255022f5c9eff0425363cb157fa827a836a890410b6841f072740123d320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000059120ebde00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000001a1976a91424228d558e3e9c439698841dd10e8f9b53045d9688ac000000000000", +} 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 52% rename from core/test/helpers/context.ts rename to solidity/test/helpers/context.ts index 3933baeb1..b4d864fcb 100644 --- a/core/test/helpers/context.ts +++ b/solidity/test/helpers/context.ts @@ -3,12 +3,13 @@ import { getDeployedContract } from "./contract" import type { StBTC as stBTC, - Dispatcher, - TestERC20, BridgeStub, - TestERC4626, TBTCVaultStub, - AcreBitcoinDepositor, + MezoAllocator, + MezoPortalStub, + BitcoinDepositor, + BitcoinRedeemer, + TestTBTC, } from "../../typechain" // eslint-disable-next-line import/prefer-default-export @@ -16,25 +17,27 @@ export async function deployment() { await deployments.fixture() const stbtc: stBTC = await getDeployedContract("stBTC") - const bitcoinDepositor: AcreBitcoinDepositor = await getDeployedContract( - "AcreBitcoinDepositor", - ) + const bitcoinDepositor: BitcoinDepositor = + await getDeployedContract("BitcoinDepositor") + const bitcoinRedeemer: BitcoinRedeemer = + await getDeployedContract("BitcoinRedeemer") - const tbtc: TestERC20 = await getDeployedContract("TBTC") + const tbtc: TestTBTC = await getDeployedContract("TBTC") const tbtcBridge: BridgeStub = await getDeployedContract("Bridge") const tbtcVault: TBTCVaultStub = await getDeployedContract("TBTCVault") - const dispatcher: Dispatcher = await getDeployedContract("Dispatcher") - - const vault: TestERC4626 = await getDeployedContract("Vault") + const mezoAllocator: MezoAllocator = + await getDeployedContract("MezoAllocator") + const mezoPortal: MezoPortalStub = await getDeployedContract("MezoPortal") return { tbtc, stbtc, bitcoinDepositor, + 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 949f76f6b..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], @@ -1070,15 +1070,7 @@ describe("stBTC", () => { }) describe("updateDispatcher", () => { - let snapshot: SnapshotRestorer - - before(async () => { - snapshot = await takeSnapshot() - }) - - after(async () => { - await snapshot.restore() - }) + beforeAfterSnapshotWrapper() context("when caller is not governance", () => { it("should revert", async () => { @@ -1106,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() @@ -1168,19 +1160,28 @@ describe("stBTC", () => { }) context("when a new treasury is an allowed address", () => { + let oldTreasury: string let newTreasury: string + let tx: ContractTransactionResponse before(async () => { // Treasury is set by the deployment scripts. See deployment tests // where initial parameters are checked. + oldTreasury = await stbtc.treasury() newTreasury = await ethers.Wallet.createRandom().getAddress() - await stbtc.connect(governance).updateTreasury(newTreasury) + tx = await stbtc.connect(governance).updateTreasury(newTreasury) }) it("should update the treasury", async () => { expect(await stbtc.treasury()).to.be.equal(newTreasury) }) + + it("should emit TreasuryUpdated event", async () => { + await expect(tx) + .to.emit(stbtc, "TreasuryUpdated") + .withArgs(oldTreasury, newTreasury) + }) }) }) }) 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,