From 389b9e2f56cbeca07177508d9f7aa201cdf53518 Mon Sep 17 00:00:00 2001 From: ioay Date: Sun, 3 Mar 2024 18:33:12 +0100 Subject: [PATCH 001/123] Activities bar - added carousel --- dapp/package.json | 3 +- dapp/src/components/GlobalStyles/index.tsx | 9 ++ .../index.tsx => Activities/ActivityBar.tsx} | 24 ++--- .../Activities/ActivityCard/ActivityCard.tsx | 92 ++++++++++++++++ .../ActivityCard/ActivityCardLinkWrapper.tsx | 28 +++++ .../ActivityCard/ActivityCardWrapper.tsx} | 8 +- .../shared/Activities/ActivityCard/index.tsx | 1 + .../ActivityCarousel/ActivityCarousel.tsx | 63 +++++++++++ .../Activities/ActivityCarousel/index.tsx | 1 + .../ActivityCarousel/utils/carouselArrows.tsx | 48 +++++++++ .../utils/carouselSettings.tsx | 88 +++++++++++++++ .../ActivityCarousel/utils/index.ts | 2 + .../components/shared/Activities/index.tsx | 2 + .../shared/Activities/mock-activities.ts | 67 ++++++++++++ .../shared/ActivityBar/ActivityCard.tsx | 87 --------------- .../shared/ActivityBar/mock-activities.ts | 25 ----- dapp/src/pages/ActivityPage/index.tsx | 4 +- dapp/src/pages/OverviewPage/DocsCard.tsx | 33 ++++++ dapp/src/pages/OverviewPage/index.tsx | 29 +++-- pnpm-lock.yaml | 102 ++++++++++++++++-- 20 files changed, 557 insertions(+), 159 deletions(-) rename dapp/src/components/shared/{ActivityBar/index.tsx => Activities/ActivityBar.tsx} (52%) create mode 100644 dapp/src/components/shared/Activities/ActivityCard/ActivityCard.tsx create mode 100644 dapp/src/components/shared/Activities/ActivityCard/ActivityCardLinkWrapper.tsx rename dapp/src/components/shared/{ActivityBar/ActivityCardContainer.tsx => Activities/ActivityCard/ActivityCardWrapper.tsx} (86%) create mode 100644 dapp/src/components/shared/Activities/ActivityCard/index.tsx create mode 100644 dapp/src/components/shared/Activities/ActivityCarousel/ActivityCarousel.tsx create mode 100644 dapp/src/components/shared/Activities/ActivityCarousel/index.tsx create mode 100644 dapp/src/components/shared/Activities/ActivityCarousel/utils/carouselArrows.tsx create mode 100644 dapp/src/components/shared/Activities/ActivityCarousel/utils/carouselSettings.tsx create mode 100644 dapp/src/components/shared/Activities/ActivityCarousel/utils/index.ts create mode 100644 dapp/src/components/shared/Activities/index.tsx create mode 100644 dapp/src/components/shared/Activities/mock-activities.ts delete mode 100644 dapp/src/components/shared/ActivityBar/ActivityCard.tsx delete mode 100644 dapp/src/components/shared/ActivityBar/mock-activities.ts create mode 100644 dapp/src/pages/OverviewPage/DocsCard.tsx diff --git a/dapp/package.json b/dapp/package.json index e23e66b5f..429603c06 100644 --- a/dapp/package.json +++ b/dapp/package.json @@ -24,15 +24,16 @@ "@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", - "axios": "^1.6.7", "formik": "^2.4.5", "framer-motion": "^10.16.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-number-format": "^5.3.1", "react-router-dom": "^6.22.0", + "react-slick": "^0.30.2", "recharts": "^2.12.0" }, "devDependencies": { diff --git a/dapp/src/components/GlobalStyles/index.tsx b/dapp/src/components/GlobalStyles/index.tsx index e8b11945a..920139ca2 100644 --- a/dapp/src/components/GlobalStyles/index.tsx +++ b/dapp/src/components/GlobalStyles/index.tsx @@ -41,6 +41,15 @@ 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. + .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/shared/ActivityBar/index.tsx b/dapp/src/components/shared/Activities/ActivityBar.tsx similarity index 52% rename from dapp/src/components/shared/ActivityBar/index.tsx rename to dapp/src/components/shared/Activities/ActivityBar.tsx index 4eb65f505..e3b35fcc8 100644 --- a/dapp/src/components/shared/ActivityBar/index.tsx +++ b/dapp/src/components/shared/Activities/ActivityBar.tsx @@ -1,10 +1,10 @@ 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 { Flex } from "@chakra-ui/react" +import { ActivityCard } from "./ActivityCard" import { mockedActivities } from "./mock-activities" -function ActivityBar(props: FlexboxProps) { +export function ActivityBar() { + // TODO: Lines 8-18 should be replaced by redux store when subgraphs are implemented const [activities, setActivities] = useState(mockedActivities) const onRemove = useCallback( @@ -16,20 +16,16 @@ function ActivityBar(props: FlexboxProps) { }, [activities], ) + return ( - + {activities.map((activity) => ( - - - + activity={activity} + onRemove={onRemove} + /> ))} ) } - -export default ActivityBar diff --git a/dapp/src/components/shared/Activities/ActivityCard/ActivityCard.tsx b/dapp/src/components/shared/Activities/ActivityCard/ActivityCard.tsx new file mode 100644 index 000000000..f7c2fcb1a --- /dev/null +++ b/dapp/src/components/shared/Activities/ActivityCard/ActivityCard.tsx @@ -0,0 +1,92 @@ +import React, { useCallback } from "react" +import { + CardBody, + CardFooter, + CardHeader, + CardProps, + HStack, + Icon, + Tooltip, + CloseButton, +} from "@chakra-ui/react" +import { useLocation } from "react-router-dom" +import { ActivityInfo, LocationState } from "#/types" +import { capitalize } from "#/utils" +import { ChevronRightIcon } from "#/assets/icons" +import { CurrencyBalance } from "#/components/shared/CurrencyBalance" +import StatusInfo from "#/components/shared/StatusInfo" +import { TextSm } from "#/components/shared/Typography" +import { ActivityCardWrapper } from "./ActivityCardWrapper" +import { ActivityCardLinkWrapper } from "./ActivityCardLinkWrapper" + +type ActivityCardType = CardProps & { + activity: ActivityInfo + onRemove: (txHash: string) => void +} + +export function ActivityCard({ + activity, + onRemove, + ...props +}: ActivityCardType) { + const state = useLocation().state as LocationState | null + const isActive = state ? activity.txHash === state.activity.txHash : false + const isCompleted = activity.status === "completed" + + const onClose = useCallback( + (event: React.MouseEvent) => { + event.preventDefault() + if (activity.txHash) { + onRemove(activity.txHash) + } + }, + [onRemove, activity.txHash], + ) + + return ( + + + + + + {isCompleted ? ( + + + + ) : ( + + )} + + + + + {capitalize(activity.action)} + + + + + + + + ) +} diff --git a/dapp/src/components/shared/Activities/ActivityCard/ActivityCardLinkWrapper.tsx b/dapp/src/components/shared/Activities/ActivityCard/ActivityCardLinkWrapper.tsx new file mode 100644 index 000000000..9453c0b36 --- /dev/null +++ b/dapp/src/components/shared/Activities/ActivityCard/ActivityCardLinkWrapper.tsx @@ -0,0 +1,28 @@ +import React from "react" + +import { Link as ReactRouterLink } from "react-router-dom" +import { Link as ChakraLink } from "@chakra-ui/react" +import { ActivityInfo } from "#/types" + +type ActivityCardLinkWrapperProps = { + activity: ActivityInfo + children: React.ReactNode +} + +export function ActivityCardLinkWrapper({ + activity, + children, + ...props +}: ActivityCardLinkWrapperProps) { + return ( + + {children} + + ) +} diff --git a/dapp/src/components/shared/ActivityBar/ActivityCardContainer.tsx b/dapp/src/components/shared/Activities/ActivityCard/ActivityCardWrapper.tsx similarity index 86% rename from dapp/src/components/shared/ActivityBar/ActivityCardContainer.tsx rename to dapp/src/components/shared/Activities/ActivityCard/ActivityCardWrapper.tsx index 2c6256e38..2a85c122d 100644 --- a/dapp/src/components/shared/ActivityBar/ActivityCardContainer.tsx +++ b/dapp/src/components/shared/Activities/ActivityCard/ActivityCardWrapper.tsx @@ -1,7 +1,7 @@ import React from "react" import { CardProps, Card } from "@chakra-ui/react" -type ActivityCardContainerProps = CardProps & { +type ActivityCardWrapperProps = CardProps & { isCompleted: boolean isActive: boolean children: React.ReactNode @@ -34,12 +34,12 @@ const activeStyles = { }, } -function ActivityCardContainer({ +export function ActivityCardWrapper({ isActive, isCompleted, children, ...props -}: ActivityCardContainerProps) { +}: ActivityCardWrapperProps) { return ( ) } - -export default ActivityCardContainer diff --git a/dapp/src/components/shared/Activities/ActivityCard/index.tsx b/dapp/src/components/shared/Activities/ActivityCard/index.tsx new file mode 100644 index 000000000..30379b1d9 --- /dev/null +++ b/dapp/src/components/shared/Activities/ActivityCard/index.tsx @@ -0,0 +1 @@ +export * from "./ActivityCard" diff --git a/dapp/src/components/shared/Activities/ActivityCarousel/ActivityCarousel.tsx b/dapp/src/components/shared/Activities/ActivityCarousel/ActivityCarousel.tsx new file mode 100644 index 000000000..edbf6ff82 --- /dev/null +++ b/dapp/src/components/shared/Activities/ActivityCarousel/ActivityCarousel.tsx @@ -0,0 +1,63 @@ +import React, { useCallback, useRef, useState } from "react" +import Slider from "react-slick" +import { Box, HStack, BoxProps } from "@chakra-ui/react" +import { ActivityCard } from "../ActivityCard" +import { activityCarouselSettings } from "./utils" +import { mockedActivities } from "../mock-activities" + +export function ActivityCarousel({ ...props }: BoxProps) { + const sliderRef = useRef(null) + + // TODO: Lines 12-30 should be replaced by redux store when subgraphs are implemented + const [activities, setActivities] = useState(mockedActivities) + + const onRemove = useCallback( + (activityHash: string) => { + const removedIndex = activities.findIndex( + (activity) => activity.txHash === activityHash, + ) + const filteredActivities = activities.filter( + (activity) => activity.txHash !== activityHash, + ) + const isLastCard = removedIndex === activities.length - 1 + if (isLastCard) { + sliderRef.current?.slickPrev() + } + sliderRef.current?.forceUpdate() + setActivities(filteredActivities) + }, + [activities], + ) + + return ( + + + {activities.map((activity) => ( + + ))} + + + ) +} diff --git a/dapp/src/components/shared/Activities/ActivityCarousel/index.tsx b/dapp/src/components/shared/Activities/ActivityCarousel/index.tsx new file mode 100644 index 000000000..47ef26c1e --- /dev/null +++ b/dapp/src/components/shared/Activities/ActivityCarousel/index.tsx @@ -0,0 +1 @@ +export * from "./ActivityCarousel" diff --git a/dapp/src/components/shared/Activities/ActivityCarousel/utils/carouselArrows.tsx b/dapp/src/components/shared/Activities/ActivityCarousel/utils/carouselArrows.tsx new file mode 100644 index 000000000..29a5cb8c6 --- /dev/null +++ b/dapp/src/components/shared/Activities/ActivityCarousel/utils/carouselArrows.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/components/shared/Activities/ActivityCarousel/utils/carouselSettings.tsx b/dapp/src/components/shared/Activities/ActivityCarousel/utils/carouselSettings.tsx new file mode 100644 index 000000000..701330e08 --- /dev/null +++ b/dapp/src/components/shared/Activities/ActivityCarousel/utils/carouselSettings.tsx @@ -0,0 +1,88 @@ +import React from "react" +import { NextArrowCarousel, PrevArrowCarousel } from "./carouselArrows" + +/* * + * 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 = { + dots: false, + infinite: false, + draggable: false, + variableWidth: true, + speed: 500, + slidesToShow: 12, + slidesToScroll: 1, + 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, + }, + }, + ], +} diff --git a/dapp/src/components/shared/Activities/ActivityCarousel/utils/index.ts b/dapp/src/components/shared/Activities/ActivityCarousel/utils/index.ts new file mode 100644 index 000000000..15749ecf0 --- /dev/null +++ b/dapp/src/components/shared/Activities/ActivityCarousel/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./carouselArrows" +export * from "./carouselSettings" diff --git a/dapp/src/components/shared/Activities/index.tsx b/dapp/src/components/shared/Activities/index.tsx new file mode 100644 index 000000000..f1f7edeea --- /dev/null +++ b/dapp/src/components/shared/Activities/index.tsx @@ -0,0 +1,2 @@ +export * from "./ActivityBar" +export * from "./ActivityCarousel" diff --git a/dapp/src/components/shared/Activities/mock-activities.ts b/dapp/src/components/shared/Activities/mock-activities.ts new file mode 100644 index 000000000..a6efd2a2a --- /dev/null +++ b/dapp/src/components/shared/Activities/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/components/shared/ActivityBar/ActivityCard.tsx b/dapp/src/components/shared/ActivityBar/ActivityCard.tsx deleted file mode 100644 index 96e8cc8ba..000000000 --- a/dapp/src/components/shared/ActivityBar/ActivityCard.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useCallback } from "react" -import { - CardBody, - CardFooter, - CardHeader, - CardProps, - HStack, - Icon, - Tooltip, - CloseButton, -} from "@chakra-ui/react" -import { useLocation } from "react-router-dom" -import { ActivityInfo, LocationState } from "#/types" -import { capitalize } from "#/utils" -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" - -type ActivityCardType = CardProps & { - activity: ActivityInfo - onRemove: (txHash: string) => 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" - - const onClose = useCallback( - (event: React.MouseEvent) => { - event.preventDefault() - if (activity.txHash) { - onRemove(activity.txHash) - } - }, - [onRemove, activity.txHash], - ) - - return ( - - - - - {isCompleted ? ( - - - - ) : ( - - )} - - - - - {capitalize(activity.action)} - - - - - - - ) -} - -export default ActivityCard 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/pages/ActivityPage/index.tsx b/dapp/src/pages/ActivityPage/index.tsx index 8295d008f..77afcc62a 100644 --- a/dapp/src/pages/ActivityPage/index.tsx +++ b/dapp/src/pages/ActivityPage/index.tsx @@ -4,7 +4,7 @@ import { Flex, Link as ChakraLink, Icon } from "@chakra-ui/react" import { Link as ReactRouterLink } from "react-router-dom" import { useSidebar } from "#/hooks" import { ArrowLeft } from "#/assets/icons" -import ActivityBar from "#/components/shared/ActivityBar" +import { ActivityBar } from "#/components/shared/Activities" import ActivityDetails from "./ActivityDetails" export default function ActivityPage() { @@ -29,7 +29,7 @@ export default function ActivityPage() { /> - + diff --git a/dapp/src/pages/OverviewPage/DocsCard.tsx b/dapp/src/pages/OverviewPage/DocsCard.tsx new file mode 100644 index 000000000..2844e6a75 --- /dev/null +++ b/dapp/src/pages/OverviewPage/DocsCard.tsx @@ -0,0 +1,33 @@ +import React from "react" +import { Card } from "@chakra-ui/react" +import { useDocsDrawer } from "#/hooks" +import { TextSm } from "#/components/shared/Typography" +import ButtonLink from "#/components/shared/ButtonLink" + +export function DocsCard() { + const { onOpen } = useDocsDrawer() + + return ( + + + Documentation + + + Everything you need to know about our contracts. + + ) +} diff --git a/dapp/src/pages/OverviewPage/index.tsx b/dapp/src/pages/OverviewPage/index.tsx index 54bb52d8b..3ebbf743d 100644 --- a/dapp/src/pages/OverviewPage/index.tsx +++ b/dapp/src/pages/OverviewPage/index.tsx @@ -1,37 +1,36 @@ import React from "react" -import { Flex, Grid, HStack, Switch } from "@chakra-ui/react" -import { useDocsDrawer } from "#/hooks" +import { Flex, Grid, HStack, SimpleGrid, Switch } from "@chakra-ui/react" import { TextSm } from "#/components/shared/Typography" import { USD } from "#/constants" -import ButtonLink from "#/components/shared/ButtonLink" +import { ActivityCarousel } from "#/components/shared/Activities/" import PositionDetails from "./PositionDetails" import Statistics from "./Statistics" import TransactionHistory from "./TransactionHistory" -import ActivityBar from "../../components/shared/ActivityBar" +import { DocsCard } from "./DocsCard" export default function OverviewPage() { - const { onOpen } = useDocsDrawer() - return ( - - + + {/* TODO: Handle click actions */} Show values in {USD.symbol} - - - - Docs - - + + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbc589b20..df065c1f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,9 +150,12 @@ 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) + version: 1.6.7 ethers: specifier: ^6.10.0 version: 6.10.0 @@ -174,6 +177,9 @@ importers: react-router-dom: specifier: ^6.22.0 version: 6.22.0(react-dom@18.2.0)(react@18.2.0) + react-slick: + specifier: ^0.30.2 + version: 0.30.2(react-dom@18.2.0)(react@18.2.0) recharts: specifier: ^2.12.0 version: 2.12.0(react-dom@18.2.0)(react@18.2.0) @@ -425,7 +431,7 @@ packages: '@babel/traverse': 7.23.4 '@babel/types': 7.23.4 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1724,7 +1730,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.4 '@babel/types': 7.23.4 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3449,7 +3455,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 espree: 9.6.1 globals: 13.23.0 ignore: 5.3.0 @@ -4038,7 +4044,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -6837,6 +6843,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: @@ -6941,7 +6953,7 @@ packages: '@typescript-eslint/type-utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) '@typescript-eslint/utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 eslint: 8.54.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -6986,7 +6998,7 @@ packages: '@typescript-eslint/types': 6.12.0 '@typescript-eslint/typescript-estree': 6.12.0(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 eslint: 8.54.0 typescript: 5.3.2 transitivePeerDependencies: @@ -7037,7 +7049,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.12.0(typescript@5.3.2) '@typescript-eslint/utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 eslint: 8.54.0 ts-api-utils: 1.0.3(typescript@5.3.2) typescript: 5.3.2 @@ -7084,7 +7096,7 @@ packages: dependencies: '@typescript-eslint/types': 6.12.0 '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -7804,6 +7816,16 @@ packages: transitivePeerDependencies: - debug + /axios@1.6.7: + resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} + dependencies: + follow-redirects: 1.15.5 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axios@1.6.7(debug@4.3.4): resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} dependencies: @@ -7812,6 +7834,7 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug + dev: true /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} @@ -8832,6 +8855,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'} @@ -9662,6 +9689,17 @@ packages: dependencies: ms: 2.1.3 + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -10187,6 +10225,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.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -10819,7 +10861,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -11592,6 +11634,16 @@ packages: dependencies: debug: 4.3.4(supports-color@8.1.1) + /follow-redirects@1.15.5: + resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /follow-redirects@1.15.5(debug@4.3.4): resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} engines: {node: '>=4.0'} @@ -11602,6 +11654,7 @@ packages: optional: true dependencies: debug: 4.3.4(supports-color@8.1.1) + dev: true /follow-redirects@1.5.10: resolution: {integrity: sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==} @@ -14238,6 +14291,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 @@ -16739,6 +16798,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: @@ -17037,6 +17111,10 @@ packages: /require-package-name@2.0.1: resolution: {integrity: sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==} + /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==} @@ -17843,6 +17921,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 From 34ed19c68512f661df4b02b201c3d1da9aa38418 Mon Sep 17 00:00:00 2001 From: ioay Date: Tue, 5 Mar 2024 19:37:01 +0100 Subject: [PATCH 002/123] Show current position in Ledger Live dApp --- dapp/src/hooks/useBtcBalance.ts | 33 +++++++++++++++++++ dapp/src/hooks/useInitApp.ts | 2 ++ .../pages/OverviewPage/PositionDetails.tsx | 5 ++- dapp/src/store/btc/btcSelector.ts | 3 ++ dapp/src/store/btc/btcSlice.ts | 13 +++++++- dapp/src/store/middleware.ts | 4 ++- 6 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 dapp/src/hooks/useBtcBalance.ts diff --git a/dapp/src/hooks/useBtcBalance.ts b/dapp/src/hooks/useBtcBalance.ts new file mode 100644 index 000000000..d08e40dbe --- /dev/null +++ b/dapp/src/hooks/useBtcBalance.ts @@ -0,0 +1,33 @@ +import { useCallback, useEffect } from "react" +import { EthereumAddress } from "@acre-btc/sdk" +import { useAcreContext } from "#/acre-react/hooks" +import { logPromiseFailure } from "#/utils" +import { btcSlice } from "#/store/btc" +import { useWalletContext } from "./useWalletContext" +import { useAppDispatch } from "./store" + +export function useBtcBalance() { + const { acre, isInitialized } = useAcreContext() + const { ethAccount } = useWalletContext() + const dispatch = useAppDispatch() + + const getBtcBalance = useCallback(async () => { + if (isInitialized && ethAccount && acre) { + const chainIdentifier = EthereumAddress.from(ethAccount.address) + const sharesBalance = await acre.staking.sharesBalance(chainIdentifier) + const estimatedBitcoinBalance = + await acre.staking.estimatedBitcoinBalance(chainIdentifier) + + if (typeof sharesBalance === "bigint") + dispatch(btcSlice.actions.setSharesBalance(sharesBalance)) + if (typeof estimatedBitcoinBalance === "bigint") + dispatch( + btcSlice.actions.setEstimatedBtcBalance(estimatedBitcoinBalance), + ) + } + }, [acre, isInitialized, ethAccount, dispatch]) + + useEffect(() => { + logPromiseFailure(getBtcBalance()) + }, [getBtcBalance]) +} diff --git a/dapp/src/hooks/useInitApp.ts b/dapp/src/hooks/useInitApp.ts index bfa07283e..b31b09c14 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 { useBtcBalance } from "./useBtcBalance" export function useInitApp() { // TODO: Let's uncomment when dark mode is ready @@ -8,4 +9,5 @@ export function useInitApp() { useSentry() useInitializeAcreSdk() useFetchBTCPriceUSD() + useBtcBalance() } diff --git a/dapp/src/pages/OverviewPage/PositionDetails.tsx b/dapp/src/pages/OverviewPage/PositionDetails.tsx index afa2b3727..7ac6def38 100644 --- a/dapp/src/pages/OverviewPage/PositionDetails.tsx +++ b/dapp/src/pages/OverviewPage/PositionDetails.tsx @@ -14,8 +14,11 @@ import { TextMd } from "#/components/shared/Typography" import { Info } from "#/assets/icons" import { ACTION_FLOW_TYPES, ActionFlowType } from "#/types" import TransactionModal from "#/components/TransactionModal" +import { useAppSelector } from "#/hooks" +import { selectEstimatedBtcBalance } from "#/store/btc" export default function PositionDetails(props: CardProps) { + const estimatedBtcBalance = useAppSelector(selectEstimatedBtcBalance) const [actionFlowType, setActionFlowType] = useState< ActionFlowType | undefined >(undefined) @@ -37,7 +40,7 @@ export default function PositionDetails(props: CardProps) { + state.btc.estimatedBtcBalance +export const selectSharesBalance = (state: RootState) => state.btc.sharesBalance export const selectBtcUsdPrice = (state: RootState) => state.btc.usdPrice diff --git a/dapp/src/store/btc/btcSlice.ts b/dapp/src/store/btc/btcSlice.ts index 84468710d..49096b7af 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 diff --git a/dapp/src/store/middleware.ts b/dapp/src/store/middleware.ts index 451fd90f1..8950058df 100644 --- a/dapp/src/store/middleware.ts +++ b/dapp/src/store/middleware.ts @@ -1 +1,3 @@ -export const middleware = {} +export const middleware = { + serializableCheck: false, +} From 401f25345dfe89193b28e1a6c828dfc78ed6712f Mon Sep 17 00:00:00 2001 From: ioay Date: Wed, 13 Mar 2024 12:55:02 +0100 Subject: [PATCH 003/123] Added ArrowUpRightAnimatedIcon component --- .../animated/ArrowUpRightAnimatedIcon.tsx | 69 +++++++++++++++++++ dapp/src/assets/icons/animated/index.ts | 1 + dapp/src/pages/OverviewPage/DocsCard.tsx | 29 ++++---- dapp/src/theme/utils/index.ts | 1 + dapp/src/theme/utils/units.ts | 2 + 5 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx create mode 100644 dapp/src/assets/icons/animated/index.ts create mode 100644 dapp/src/theme/utils/units.ts diff --git a/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx b/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx new file mode 100644 index 000000000..fad0bcd91 --- /dev/null +++ b/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx @@ -0,0 +1,69 @@ +import React from "react" +import { ArrowUpRight } from "#/assets/icons" +import { Box, Flex, Icon } from "@chakra-ui/react" +import { Variants, motion } from "framer-motion" +import { chakraUnitToPx } from "#/theme/utils" + +const arrowUpVariants: Variants = { + initial: { + x: 0, + y: 0, + }, + animate: (boxSizePx: number) => ({ + x: [0, boxSizePx], + y: [0, -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, 0], + 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 ( + + + + + + + + + ) +} 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/pages/OverviewPage/DocsCard.tsx b/dapp/src/pages/OverviewPage/DocsCard.tsx index 2844e6a75..0d7a5bcbe 100644 --- a/dapp/src/pages/OverviewPage/DocsCard.tsx +++ b/dapp/src/pages/OverviewPage/DocsCard.tsx @@ -1,8 +1,9 @@ import React from "react" -import { Card } from "@chakra-ui/react" +import { Card, HStack } from "@chakra-ui/react" import { useDocsDrawer } from "#/hooks" import { TextSm } from "#/components/shared/Typography" -import ButtonLink from "#/components/shared/ButtonLink" +import { ArrowUpRightAnimatedIcon } from "#/assets/icons/animated" +import { motion } from "framer-motion" export function DocsCard() { const { onOpen } = useDocsDrawer() @@ -11,22 +12,20 @@ export function DocsCard() { - - Documentation - - + + + Documentation + Everything you need to know about our contracts. ) 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..d4dc9b844 --- /dev/null +++ b/dapp/src/theme/utils/units.ts @@ -0,0 +1,2 @@ +// The values are proportional, so 1 spacing unit is equal to 0.25rem, which translates to 4px by default in common browsers. +export const chakraUnitToPx = (chakraUnit: number): number => chakraUnit * 4 From f1218a705aa0364c753f29d726d22233fe6e4bd1 Mon Sep 17 00:00:00 2001 From: ioay Date: Wed, 13 Mar 2024 13:23:10 +0100 Subject: [PATCH 004/123] Added chakraSpacingUnit --- dapp/src/theme/utils/units.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dapp/src/theme/utils/units.ts b/dapp/src/theme/utils/units.ts index d4dc9b844..c6c7df1b8 100644 --- a/dapp/src/theme/utils/units.ts +++ b/dapp/src/theme/utils/units.ts @@ -1,2 +1,7 @@ // The values are proportional, so 1 spacing unit is equal to 0.25rem, which translates to 4px by default in common browsers. -export const chakraUnitToPx = (chakraUnit: number): number => chakraUnit * 4 +const chakraSpacingUnit = { + px: 4, + rem: 0.25, +} +export const chakraUnitToPx = (chakraUnit: number): number => + chakraUnit * chakraSpacingUnit.px From 7344c8b508446e571cbd2a7716e281637d59c3c0 Mon Sep 17 00:00:00 2001 From: ioay Date: Wed, 13 Mar 2024 17:12:50 +0100 Subject: [PATCH 005/123] Styles improvements --- dapp/src/components/GlobalStyles/index.tsx | 6 ++++-- dapp/src/components/shared/Activities/ActivityBar.tsx | 6 +++--- .../shared/Activities/ActivityCard/ActivityCard.tsx | 7 +++---- dapp/src/pages/ActivityPage/ActivityDetails.tsx | 9 ++++++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/dapp/src/components/GlobalStyles/index.tsx b/dapp/src/components/GlobalStyles/index.tsx index 920139ca2..5fd79a4e5 100644 --- a/dapp/src/components/GlobalStyles/index.tsx +++ b/dapp/src/components/GlobalStyles/index.tsx @@ -41,11 +41,13 @@ 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. + // React-slick package: Chakra-ui with react-slick package doesn't + // generate flex style for auto-generated slick-track wrapper. .slick-track { display: flex; } - // React-slick package: Hiding arrows instead of disabling them in case when carousel is not fully completed by slides. + // 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/shared/Activities/ActivityBar.tsx b/dapp/src/components/shared/Activities/ActivityBar.tsx index e3b35fcc8..4868b2244 100644 --- a/dapp/src/components/shared/Activities/ActivityBar.tsx +++ b/dapp/src/components/shared/Activities/ActivityBar.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useState } from "react" -import { Flex } from "@chakra-ui/react" +import { VStack } from "@chakra-ui/react" import { ActivityCard } from "./ActivityCard" import { mockedActivities } from "./mock-activities" @@ -18,7 +18,7 @@ export function ActivityBar() { ) return ( - + {activities.map((activity) => ( ))} - + ) } diff --git a/dapp/src/components/shared/Activities/ActivityCard/ActivityCard.tsx b/dapp/src/components/shared/Activities/ActivityCard/ActivityCard.tsx index f7c2fcb1a..16f5524a0 100644 --- a/dapp/src/components/shared/Activities/ActivityCard/ActivityCard.tsx +++ b/dapp/src/components/shared/Activities/ActivityCard/ActivityCard.tsx @@ -11,7 +11,6 @@ import { } from "@chakra-ui/react" import { useLocation } from "react-router-dom" import { ActivityInfo, LocationState } from "#/types" -import { capitalize } from "#/utils" import { ChevronRightIcon } from "#/assets/icons" import { CurrencyBalance } from "#/components/shared/CurrencyBalance" import StatusInfo from "#/components/shared/StatusInfo" @@ -73,9 +72,9 @@ export function ActivityCard({ )} - - - {capitalize(activity.action)} + + + {activity.action} diff --git a/dapp/src/pages/ActivityPage/ActivityDetails.tsx b/dapp/src/pages/ActivityPage/ActivityDetails.tsx index 05f82fd82..53d56b9c0 100644 --- a/dapp/src/pages/ActivityPage/ActivityDetails.tsx +++ b/dapp/src/pages/ActivityPage/ActivityDetails.tsx @@ -10,7 +10,6 @@ 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" @@ -46,8 +45,12 @@ function ActivityDetails() { - - {capitalize(activity.action)} + + {activity.action} Date: Wed, 13 Mar 2024 19:09:06 +0100 Subject: [PATCH 006/123] Hooks namning change & minor improvements --- dapp/src/hooks/store/index.ts | 1 + .../src/hooks/store/useEstimatedBtcBalance.ts | 6 +++++ ...useBtcBalance.ts => useFetchBtcBalance.ts} | 23 ++++++++----------- dapp/src/hooks/useInitApp.ts | 4 ++-- .../pages/OverviewPage/PositionDetails.tsx | 5 ++-- dapp/src/store/btc/btcSelector.ts | 8 ++++--- 6 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 dapp/src/hooks/store/useEstimatedBtcBalance.ts rename dapp/src/hooks/{useBtcBalance.ts => useFetchBtcBalance.ts} (62%) diff --git a/dapp/src/hooks/store/index.ts b/dapp/src/hooks/store/index.ts index 20f33eb3c..9afcc7285 100644 --- a/dapp/src/hooks/store/index.ts +++ b/dapp/src/hooks/store/index.ts @@ -1,2 +1,3 @@ export * from "./useAppDispatch" export * from "./useAppSelector" +export * from "./useEstimatedBtcBalance" diff --git a/dapp/src/hooks/store/useEstimatedBtcBalance.ts b/dapp/src/hooks/store/useEstimatedBtcBalance.ts new file mode 100644 index 000000000..9820c6c4b --- /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/useBtcBalance.ts b/dapp/src/hooks/useFetchBtcBalance.ts similarity index 62% rename from dapp/src/hooks/useBtcBalance.ts rename to dapp/src/hooks/useFetchBtcBalance.ts index d08e40dbe..dfafbdfc9 100644 --- a/dapp/src/hooks/useBtcBalance.ts +++ b/dapp/src/hooks/useFetchBtcBalance.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from "react" +import { useEffect } from "react" import { EthereumAddress } from "@acre-btc/sdk" import { useAcreContext } from "#/acre-react/hooks" import { logPromiseFailure } from "#/utils" @@ -6,28 +6,23 @@ import { btcSlice } from "#/store/btc" import { useWalletContext } from "./useWalletContext" import { useAppDispatch } from "./store" -export function useBtcBalance() { +export function useFetchBtcBalance() { const { acre, isInitialized } = useAcreContext() const { ethAccount } = useWalletContext() const dispatch = useAppDispatch() - const getBtcBalance = useCallback(async () => { - if (isInitialized && ethAccount && acre) { + 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) - if (typeof sharesBalance === "bigint") - dispatch(btcSlice.actions.setSharesBalance(sharesBalance)) - if (typeof estimatedBitcoinBalance === "bigint") - dispatch( - btcSlice.actions.setEstimatedBtcBalance(estimatedBitcoinBalance), - ) + dispatch(btcSlice.actions.setSharesBalance(sharesBalance)) + dispatch(btcSlice.actions.setEstimatedBtcBalance(estimatedBitcoinBalance)) } - }, [acre, isInitialized, ethAccount, dispatch]) - - useEffect(() => { logPromiseFailure(getBtcBalance()) - }, [getBtcBalance]) + }, [acre, isInitialized, ethAccount, dispatch]) } diff --git a/dapp/src/hooks/useInitApp.ts b/dapp/src/hooks/useInitApp.ts index b31b09c14..570651bf1 100644 --- a/dapp/src/hooks/useInitApp.ts +++ b/dapp/src/hooks/useInitApp.ts @@ -1,7 +1,7 @@ import { useSentry } from "./sentry" import { useInitializeAcreSdk } from "./useInitializeAcreSdk" import { useFetchBTCPriceUSD } from "./useFetchBTCPriceUSD" -import { useBtcBalance } from "./useBtcBalance" +import { useFetchBtcBalance } from "./useFetchBtcBalance" export function useInitApp() { // TODO: Let's uncomment when dark mode is ready @@ -9,5 +9,5 @@ export function useInitApp() { useSentry() useInitializeAcreSdk() useFetchBTCPriceUSD() - useBtcBalance() + useFetchBtcBalance() } diff --git a/dapp/src/pages/OverviewPage/PositionDetails.tsx b/dapp/src/pages/OverviewPage/PositionDetails.tsx index 7ac6def38..f6d9d42b5 100644 --- a/dapp/src/pages/OverviewPage/PositionDetails.tsx +++ b/dapp/src/pages/OverviewPage/PositionDetails.tsx @@ -14,11 +14,10 @@ import { TextMd } from "#/components/shared/Typography" import { Info } from "#/assets/icons" import { ACTION_FLOW_TYPES, ActionFlowType } from "#/types" import TransactionModal from "#/components/TransactionModal" -import { useAppSelector } from "#/hooks" -import { selectEstimatedBtcBalance } from "#/store/btc" +import { useEstimatedBtcBalance } from "#/hooks/store" export default function PositionDetails(props: CardProps) { - const estimatedBtcBalance = useAppSelector(selectEstimatedBtcBalance) + const estimatedBtcBalance = useEstimatedBtcBalance() const [actionFlowType, setActionFlowType] = useState< ActionFlowType | undefined >(undefined) diff --git a/dapp/src/store/btc/btcSelector.ts b/dapp/src/store/btc/btcSelector.ts index 677188654..806f5b65b 100644 --- a/dapp/src/store/btc/btcSelector.ts +++ b/dapp/src/store/btc/btcSelector.ts @@ -1,6 +1,8 @@ import { RootState } from ".." -export const selectEstimatedBtcBalance = (state: RootState) => +export const selectEstimatedBtcBalance = (state: RootState): bigint => state.btc.estimatedBtcBalance -export const selectSharesBalance = (state: RootState) => state.btc.sharesBalance -export const selectBtcUsdPrice = (state: RootState) => state.btc.usdPrice +export const selectSharesBalance = (state: RootState): bigint => + state.btc.sharesBalance +export const selectBtcUsdPrice = (state: RootState): number => + state.btc.usdPrice From 19d36934f8467976677d60e5db8bac688095bdaa Mon Sep 17 00:00:00 2001 From: ioay Date: Thu, 14 Mar 2024 00:32:28 +0100 Subject: [PATCH 007/123] Activities carousel structure refactoring --- dapp/src/components/shared/Activities/index.tsx | 2 -- .../shared/{Activities => }/ActivityCard/ActivityCard.tsx | 0 .../ActivityCard/ActivityCardLinkWrapper.tsx | 0 .../{Activities => }/ActivityCard/ActivityCardWrapper.tsx | 0 .../shared/{Activities => }/ActivityCard/index.tsx | 0 .../shared/{Activities => ActivityCard}/mock-activities.ts | 0 .../Activities => pages/ActivityPage}/ActivityBar.tsx | 4 ++-- dapp/src/pages/ActivityPage/index.tsx | 2 +- .../OverviewPage}/ActivityCarousel/ActivityCarousel.tsx | 4 ++-- .../OverviewPage}/ActivityCarousel/index.tsx | 0 .../OverviewPage}/ActivityCarousel/utils/carouselArrows.tsx | 0 .../ActivityCarousel/utils/carouselSettings.tsx | 0 .../OverviewPage}/ActivityCarousel/utils/index.ts | 0 dapp/src/pages/OverviewPage/DocsCard.tsx | 5 +++-- dapp/src/pages/OverviewPage/index.tsx | 6 +++--- 15 files changed, 11 insertions(+), 12 deletions(-) delete mode 100644 dapp/src/components/shared/Activities/index.tsx rename dapp/src/components/shared/{Activities => }/ActivityCard/ActivityCard.tsx (100%) rename dapp/src/components/shared/{Activities => }/ActivityCard/ActivityCardLinkWrapper.tsx (100%) rename dapp/src/components/shared/{Activities => }/ActivityCard/ActivityCardWrapper.tsx (100%) rename dapp/src/components/shared/{Activities => }/ActivityCard/index.tsx (100%) rename dapp/src/components/shared/{Activities => ActivityCard}/mock-activities.ts (100%) rename dapp/src/{components/shared/Activities => pages/ActivityPage}/ActivityBar.tsx (82%) rename dapp/src/{components/shared/Activities => pages/OverviewPage}/ActivityCarousel/ActivityCarousel.tsx (91%) rename dapp/src/{components/shared/Activities => pages/OverviewPage}/ActivityCarousel/index.tsx (100%) rename dapp/src/{components/shared/Activities => pages/OverviewPage}/ActivityCarousel/utils/carouselArrows.tsx (100%) rename dapp/src/{components/shared/Activities => pages/OverviewPage}/ActivityCarousel/utils/carouselSettings.tsx (100%) rename dapp/src/{components/shared/Activities => pages/OverviewPage}/ActivityCarousel/utils/index.ts (100%) diff --git a/dapp/src/components/shared/Activities/index.tsx b/dapp/src/components/shared/Activities/index.tsx deleted file mode 100644 index f1f7edeea..000000000 --- a/dapp/src/components/shared/Activities/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./ActivityBar" -export * from "./ActivityCarousel" diff --git a/dapp/src/components/shared/Activities/ActivityCard/ActivityCard.tsx b/dapp/src/components/shared/ActivityCard/ActivityCard.tsx similarity index 100% rename from dapp/src/components/shared/Activities/ActivityCard/ActivityCard.tsx rename to dapp/src/components/shared/ActivityCard/ActivityCard.tsx diff --git a/dapp/src/components/shared/Activities/ActivityCard/ActivityCardLinkWrapper.tsx b/dapp/src/components/shared/ActivityCard/ActivityCardLinkWrapper.tsx similarity index 100% rename from dapp/src/components/shared/Activities/ActivityCard/ActivityCardLinkWrapper.tsx rename to dapp/src/components/shared/ActivityCard/ActivityCardLinkWrapper.tsx diff --git a/dapp/src/components/shared/Activities/ActivityCard/ActivityCardWrapper.tsx b/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx similarity index 100% rename from dapp/src/components/shared/Activities/ActivityCard/ActivityCardWrapper.tsx rename to dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx diff --git a/dapp/src/components/shared/Activities/ActivityCard/index.tsx b/dapp/src/components/shared/ActivityCard/index.tsx similarity index 100% rename from dapp/src/components/shared/Activities/ActivityCard/index.tsx rename to dapp/src/components/shared/ActivityCard/index.tsx diff --git a/dapp/src/components/shared/Activities/mock-activities.ts b/dapp/src/components/shared/ActivityCard/mock-activities.ts similarity index 100% rename from dapp/src/components/shared/Activities/mock-activities.ts rename to dapp/src/components/shared/ActivityCard/mock-activities.ts diff --git a/dapp/src/components/shared/Activities/ActivityBar.tsx b/dapp/src/pages/ActivityPage/ActivityBar.tsx similarity index 82% rename from dapp/src/components/shared/Activities/ActivityBar.tsx rename to dapp/src/pages/ActivityPage/ActivityBar.tsx index 4868b2244..f837eb0e4 100644 --- a/dapp/src/components/shared/Activities/ActivityBar.tsx +++ b/dapp/src/pages/ActivityPage/ActivityBar.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useState } from "react" import { VStack } from "@chakra-ui/react" -import { ActivityCard } from "./ActivityCard" -import { mockedActivities } from "./mock-activities" +import { ActivityCard } from "../../components/shared/ActivityCard" +import { mockedActivities } from "../../components/shared/ActivityCard/mock-activities" export function ActivityBar() { // TODO: Lines 8-18 should be replaced by redux store when subgraphs are implemented diff --git a/dapp/src/pages/ActivityPage/index.tsx b/dapp/src/pages/ActivityPage/index.tsx index 77afcc62a..8d61143c2 100644 --- a/dapp/src/pages/ActivityPage/index.tsx +++ b/dapp/src/pages/ActivityPage/index.tsx @@ -4,8 +4,8 @@ import { Flex, Link as ChakraLink, Icon } from "@chakra-ui/react" import { Link as ReactRouterLink } from "react-router-dom" import { useSidebar } from "#/hooks" import { ArrowLeft } from "#/assets/icons" -import { ActivityBar } from "#/components/shared/Activities" import ActivityDetails from "./ActivityDetails" +import { ActivityBar } from "./ActivityBar" export default function ActivityPage() { const { onOpen: openSideBar, onClose: closeSidebar } = useSidebar() diff --git a/dapp/src/components/shared/Activities/ActivityCarousel/ActivityCarousel.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx similarity index 91% rename from dapp/src/components/shared/Activities/ActivityCarousel/ActivityCarousel.tsx rename to dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx index edbf6ff82..8b136d0f5 100644 --- a/dapp/src/components/shared/Activities/ActivityCarousel/ActivityCarousel.tsx +++ b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useRef, useState } from "react" import Slider from "react-slick" import { Box, HStack, BoxProps } from "@chakra-ui/react" -import { ActivityCard } from "../ActivityCard" +import { ActivityCard } from "../../../components/shared/ActivityCard" import { activityCarouselSettings } from "./utils" -import { mockedActivities } from "../mock-activities" +import { mockedActivities } from "../../../components/shared/ActivityCard/mock-activities" export function ActivityCarousel({ ...props }: BoxProps) { const sliderRef = useRef(null) diff --git a/dapp/src/components/shared/Activities/ActivityCarousel/index.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/index.tsx similarity index 100% rename from dapp/src/components/shared/Activities/ActivityCarousel/index.tsx rename to dapp/src/pages/OverviewPage/ActivityCarousel/index.tsx diff --git a/dapp/src/components/shared/Activities/ActivityCarousel/utils/carouselArrows.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselArrows.tsx similarity index 100% rename from dapp/src/components/shared/Activities/ActivityCarousel/utils/carouselArrows.tsx rename to dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselArrows.tsx diff --git a/dapp/src/components/shared/Activities/ActivityCarousel/utils/carouselSettings.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselSettings.tsx similarity index 100% rename from dapp/src/components/shared/Activities/ActivityCarousel/utils/carouselSettings.tsx rename to dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselSettings.tsx diff --git a/dapp/src/components/shared/Activities/ActivityCarousel/utils/index.ts b/dapp/src/pages/OverviewPage/ActivityCarousel/utils/index.ts similarity index 100% rename from dapp/src/components/shared/Activities/ActivityCarousel/utils/index.ts rename to dapp/src/pages/OverviewPage/ActivityCarousel/utils/index.ts diff --git a/dapp/src/pages/OverviewPage/DocsCard.tsx b/dapp/src/pages/OverviewPage/DocsCard.tsx index 0d7a5bcbe..0c124ffff 100644 --- a/dapp/src/pages/OverviewPage/DocsCard.tsx +++ b/dapp/src/pages/OverviewPage/DocsCard.tsx @@ -1,11 +1,11 @@ import React from "react" -import { Card, HStack } from "@chakra-ui/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() { +export function DocsCard({ ...props }: CardProps) { const { onOpen } = useDocsDrawer() return ( @@ -21,6 +21,7 @@ export function DocsCard() { initial="initial" whileHover="animate" cursor="pointer" + {...props} > diff --git a/dapp/src/pages/OverviewPage/index.tsx b/dapp/src/pages/OverviewPage/index.tsx index 3ebbf743d..b3eb4ca82 100644 --- a/dapp/src/pages/OverviewPage/index.tsx +++ b/dapp/src/pages/OverviewPage/index.tsx @@ -2,11 +2,11 @@ import React from "react" import { Flex, Grid, HStack, SimpleGrid, Switch } from "@chakra-ui/react" import { TextSm } from "#/components/shared/Typography" import { USD } from "#/constants" -import { ActivityCarousel } from "#/components/shared/Activities/" import PositionDetails from "./PositionDetails" import Statistics from "./Statistics" import TransactionHistory from "./TransactionHistory" import { DocsCard } from "./DocsCard" +import { ActivityCarousel } from "./ActivityCarousel" export default function OverviewPage() { return ( @@ -19,10 +19,10 @@ export default function OverviewPage() { - + Date: Thu, 14 Mar 2024 11:11:12 +0100 Subject: [PATCH 008/123] Update react-router: based on id params instead of passing state --- .../shared/ActivityCard/ActivityCard.tsx | 9 ++++----- .../ActivityCard/ActivityCardLinkWrapper.tsx | 11 ++++------- .../ActivityCard/ActivityCardWrapper.tsx | 2 +- dapp/src/pages/ActivityPage/ActivityBar.tsx | 16 +++++++++------- .../pages/ActivityPage/ActivityDetails.tsx | 9 ++------- dapp/src/pages/ActivityPage/index.tsx | 19 ++++++++++++++----- dapp/src/router/index.tsx | 2 +- 7 files changed, 35 insertions(+), 33 deletions(-) diff --git a/dapp/src/components/shared/ActivityCard/ActivityCard.tsx b/dapp/src/components/shared/ActivityCard/ActivityCard.tsx index 16f5524a0..264974f6f 100644 --- a/dapp/src/components/shared/ActivityCard/ActivityCard.tsx +++ b/dapp/src/components/shared/ActivityCard/ActivityCard.tsx @@ -9,8 +9,7 @@ import { Tooltip, CloseButton, } from "@chakra-ui/react" -import { useLocation } from "react-router-dom" -import { ActivityInfo, LocationState } from "#/types" +import { ActivityInfo } from "#/types" import { ChevronRightIcon } from "#/assets/icons" import { CurrencyBalance } from "#/components/shared/CurrencyBalance" import StatusInfo from "#/components/shared/StatusInfo" @@ -21,15 +20,15 @@ import { ActivityCardLinkWrapper } from "./ActivityCardLinkWrapper" type ActivityCardType = CardProps & { activity: ActivityInfo onRemove: (txHash: string) => void + isActive?: boolean } export function ActivityCard({ activity, onRemove, + isActive, ...props }: ActivityCardType) { - const state = useLocation().state as LocationState | null - const isActive = state ? activity.txHash === state.activity.txHash : false const isCompleted = activity.status === "completed" const onClose = useCallback( @@ -43,7 +42,7 @@ export function ActivityCard({ ) return ( - + diff --git a/dapp/src/components/shared/ActivityCard/ActivityCardLinkWrapper.tsx b/dapp/src/components/shared/ActivityCard/ActivityCardLinkWrapper.tsx index 9453c0b36..527412023 100644 --- a/dapp/src/components/shared/ActivityCard/ActivityCardLinkWrapper.tsx +++ b/dapp/src/components/shared/ActivityCard/ActivityCardLinkWrapper.tsx @@ -1,25 +1,22 @@ import React from "react" - import { Link as ReactRouterLink } from "react-router-dom" import { Link as ChakraLink } from "@chakra-ui/react" -import { ActivityInfo } from "#/types" type ActivityCardLinkWrapperProps = { - activity: ActivityInfo + activityId: string children: React.ReactNode } export function ActivityCardLinkWrapper({ - activity, + activityId, children, ...props }: ActivityCardLinkWrapperProps) { return ( {children} diff --git a/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx b/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx index 2a85c122d..816761c55 100644 --- a/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx +++ b/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx @@ -3,8 +3,8 @@ import { CardProps, Card } from "@chakra-ui/react" type ActivityCardWrapperProps = CardProps & { isCompleted: boolean - isActive: boolean children: React.ReactNode + isActive?: boolean } const completedStyles = { diff --git a/dapp/src/pages/ActivityPage/ActivityBar.tsx b/dapp/src/pages/ActivityPage/ActivityBar.tsx index f837eb0e4..64cb4d90d 100644 --- a/dapp/src/pages/ActivityPage/ActivityBar.tsx +++ b/dapp/src/pages/ActivityPage/ActivityBar.tsx @@ -1,16 +1,17 @@ import React, { useCallback, useState } from "react" import { VStack } from "@chakra-ui/react" -import { ActivityCard } from "../../components/shared/ActivityCard" -import { mockedActivities } from "../../components/shared/ActivityCard/mock-activities" +import { mockedActivities } from "#/components/shared/ActivityCard/mock-activities" +import { ActivityCard } from "#/components/shared/ActivityCard" +import { ActivityInfo } from "#/types" -export function ActivityBar() { +export function ActivityBar({ activity }: { activity: ActivityInfo }) { // TODO: Lines 8-18 should be replaced by redux store when subgraphs are implemented const [activities, setActivities] = useState(mockedActivities) const onRemove = useCallback( (activityHash: string) => { const filteredActivities = activities.filter( - (activity) => activity.txHash !== activityHash, + (_activity) => _activity.txHash !== activityHash, ) setActivities(filteredActivities) }, @@ -19,11 +20,12 @@ export function ActivityBar() { return ( - {activities.map((activity) => ( + {activities.map((_activity) => ( ))} diff --git a/dapp/src/pages/ActivityPage/ActivityDetails.tsx b/dapp/src/pages/ActivityPage/ActivityDetails.tsx index 53d56b9c0..2aa4ad2d3 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, @@ -11,17 +10,13 @@ import { Text, } from "@chakra-ui/react" 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 { ActivityInfo } from "#/types" -function ActivityDetails() { - const location = useLocation() - - const { activity } = location.state as LocationState - +function ActivityDetails({ activity }: { activity: ActivityInfo }) { return ( {activity.status === "pending" && ( diff --git a/dapp/src/pages/ActivityPage/index.tsx b/dapp/src/pages/ActivityPage/index.tsx index 8d61143c2..6cb80f425 100644 --- a/dapp/src/pages/ActivityPage/index.tsx +++ b/dapp/src/pages/ActivityPage/index.tsx @@ -1,14 +1,20 @@ import React, { useEffect } from "react" import { Flex, Link as ChakraLink, Icon } from "@chakra-ui/react" -import { Link as ReactRouterLink } from "react-router-dom" +import { Link as ReactRouterLink, useParams } from "react-router-dom" import { useSidebar } from "#/hooks" import { ArrowLeft } from "#/assets/icons" +import { mockedActivities } from "#/components/shared/ActivityCard/mock-activities" import ActivityDetails from "./ActivityDetails" import { ActivityBar } from "./ActivityBar" export default function ActivityPage() { const { onOpen: openSideBar, onClose: closeSidebar } = useSidebar() + const params = useParams() + + const selectedActivity = mockedActivities.find( + (_activity) => _activity.txHash === params.activityId, + ) useEffect(() => { openSideBar() @@ -28,10 +34,13 @@ export default function ActivityPage() { _hover={{ color: "white", bg: "brand.400" }} /> - - - - + {selectedActivity && ( + + {} + + + + )} ) } diff --git a/dapp/src/router/index.tsx b/dapp/src/router/index.tsx index 357a14aad..4b1a73388 100644 --- a/dapp/src/router/index.tsx +++ b/dapp/src/router/index.tsx @@ -9,7 +9,7 @@ export const router = createBrowserRouter([ element: , }, { - path: "activity-details", + path: "activity-details/:activityId", element: , }, ]) From ba6d8ee4ec1b0e3754f4015a1f4660e87423f922 Mon Sep 17 00:00:00 2001 From: ioay Date: Thu, 14 Mar 2024 15:36:38 +0100 Subject: [PATCH 009/123] ActivityCard linking, overviewPage improvements --- .../shared/ActivityCard/ActivityCard.tsx | 97 ++++++++++--------- .../ActivityCard/ActivityCardLinkWrapper.tsx | 25 ----- .../ActivityCard/ActivityCardWrapper.tsx | 10 +- dapp/src/pages/OverviewPage/index.tsx | 8 +- dapp/src/router/index.tsx | 5 +- dapp/src/router/path.ts | 4 + 6 files changed, 69 insertions(+), 80 deletions(-) delete mode 100644 dapp/src/components/shared/ActivityCard/ActivityCardLinkWrapper.tsx create mode 100644 dapp/src/router/path.ts diff --git a/dapp/src/components/shared/ActivityCard/ActivityCard.tsx b/dapp/src/components/shared/ActivityCard/ActivityCard.tsx index 264974f6f..6cfe75833 100644 --- a/dapp/src/components/shared/ActivityCard/ActivityCard.tsx +++ b/dapp/src/components/shared/ActivityCard/ActivityCard.tsx @@ -9,13 +9,14 @@ import { Tooltip, CloseButton, } from "@chakra-ui/react" +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 { routerPath } from "#/router/path" import { ActivityCardWrapper } from "./ActivityCardWrapper" -import { ActivityCardLinkWrapper } from "./ActivityCardLinkWrapper" type ActivityCardType = CardProps & { activity: ActivityInfo @@ -27,10 +28,14 @@ export function ActivityCard({ activity, onRemove, isActive, - ...props }: ActivityCardType) { + const navigate = useNavigate() const isCompleted = activity.status === "completed" + const onClick = () => { + navigate(`${routerPath.activity}/${activity.txHash}`) + } + const onClose = useCallback( (event: React.MouseEvent) => { event.preventDefault() @@ -42,49 +47,51 @@ export function ActivityCard({ ) return ( - - - - - - {isCompleted ? ( - - - - ) : ( - - )} - - - - - {activity.action} - - - - + + + - - - + {isCompleted ? ( + + + + ) : ( + + )} + + + + + {activity.action} + + + + + + ) } diff --git a/dapp/src/components/shared/ActivityCard/ActivityCardLinkWrapper.tsx b/dapp/src/components/shared/ActivityCard/ActivityCardLinkWrapper.tsx deleted file mode 100644 index 527412023..000000000 --- a/dapp/src/components/shared/ActivityCard/ActivityCardLinkWrapper.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react" -import { Link as ReactRouterLink } from "react-router-dom" -import { Link as ChakraLink } from "@chakra-ui/react" - -type ActivityCardLinkWrapperProps = { - activityId: string - children: React.ReactNode -} - -export function ActivityCardLinkWrapper({ - activityId, - children, - ...props -}: ActivityCardLinkWrapperProps) { - return ( - - {children} - - ) -} diff --git a/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx b/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx index 816761c55..7ec00cbb3 100644 --- a/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx +++ b/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx @@ -42,10 +42,11 @@ export function ActivityCardWrapper({ }: ActivityCardWrapperProps) { return ( {children} diff --git a/dapp/src/pages/OverviewPage/index.tsx b/dapp/src/pages/OverviewPage/index.tsx index b3eb4ca82..4504b8108 100644 --- a/dapp/src/pages/OverviewPage/index.tsx +++ b/dapp/src/pages/OverviewPage/index.tsx @@ -1,5 +1,5 @@ import React from "react" -import { Flex, Grid, HStack, SimpleGrid, Switch } from "@chakra-ui/react" +import { Flex, Grid, HStack, Switch } from "@chakra-ui/react" import { TextSm } from "#/components/shared/Typography" import { USD } from "#/constants" import PositionDetails from "./PositionDetails" @@ -17,13 +17,13 @@ export default function OverviewPage() { Show values in {USD.symbol} - - + , }, { - path: "activity-details/:activityId", + 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", +} From c78989f4f1428a676ac89766a0922f563993a5fe Mon Sep 17 00:00:00 2001 From: ioay Date: Thu, 14 Mar 2024 17:46:03 +0100 Subject: [PATCH 010/123] Moved carousel to shared components --- .../shared/ActivityCard/ActivityCard.tsx | 6 ++-- .../components/shared/Carousel/Carousel.tsx | 31 +++++++++++++++++++ dapp/src/mock/index.ts | 1 + .../ActivityCard => mock}/mock-activities.ts | 0 dapp/src/pages/ActivityPage/ActivityBar.tsx | 2 +- dapp/src/pages/ActivityPage/index.tsx | 2 +- .../ActivityCarousel/ActivityCarousel.tsx | 20 ++++++------ .../utils/carouselSettings.tsx | 7 ----- 8 files changed, 47 insertions(+), 22 deletions(-) create mode 100644 dapp/src/components/shared/Carousel/Carousel.tsx create mode 100644 dapp/src/mock/index.ts rename dapp/src/{components/shared/ActivityCard => mock}/mock-activities.ts (100%) diff --git a/dapp/src/components/shared/ActivityCard/ActivityCard.tsx b/dapp/src/components/shared/ActivityCard/ActivityCard.tsx index 6cfe75833..118eb2a8f 100644 --- a/dapp/src/components/shared/ActivityCard/ActivityCard.tsx +++ b/dapp/src/components/shared/ActivityCard/ActivityCard.tsx @@ -32,13 +32,13 @@ export function ActivityCard({ const navigate = useNavigate() const isCompleted = activity.status === "completed" - const onClick = () => { + const onClick = useCallback(() => { navigate(`${routerPath.activity}/${activity.txHash}`) - } + }, [activity.txHash, navigate]) const onClose = useCallback( (event: React.MouseEvent) => { - event.preventDefault() + event.stopPropagation() if (activity.txHash) { onRemove(activity.txHash) } diff --git a/dapp/src/components/shared/Carousel/Carousel.tsx b/dapp/src/components/shared/Carousel/Carousel.tsx new file mode 100644 index 000000000..dfa20252a --- /dev/null +++ b/dapp/src/components/shared/Carousel/Carousel.tsx @@ -0,0 +1,31 @@ +import React, { forwardRef } from "react" +import { BoxProps, Flex } from "@chakra-ui/react" +import Slider from "react-slick" + +const carouselSettings = { + dots: false, + infinite: false, + draggable: false, + variableWidth: true, + speed: 500, + slidesToShow: 12, + slidesToScroll: 1, +} + +type CarouselProps = BoxProps & { + children: React.ReactNode +} + +export const Carousel = forwardRef( + (props, ref) => ( + + {props.children} + + ), +) 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/components/shared/ActivityCard/mock-activities.ts b/dapp/src/mock/mock-activities.ts similarity index 100% rename from dapp/src/components/shared/ActivityCard/mock-activities.ts rename to dapp/src/mock/mock-activities.ts diff --git a/dapp/src/pages/ActivityPage/ActivityBar.tsx b/dapp/src/pages/ActivityPage/ActivityBar.tsx index 64cb4d90d..8fe91bbd8 100644 --- a/dapp/src/pages/ActivityPage/ActivityBar.tsx +++ b/dapp/src/pages/ActivityPage/ActivityBar.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from "react" import { VStack } from "@chakra-ui/react" -import { mockedActivities } from "#/components/shared/ActivityCard/mock-activities" +import { mockedActivities } from "#/mock/mock-activities" import { ActivityCard } from "#/components/shared/ActivityCard" import { ActivityInfo } from "#/types" diff --git a/dapp/src/pages/ActivityPage/index.tsx b/dapp/src/pages/ActivityPage/index.tsx index 6cb80f425..4f445213d 100644 --- a/dapp/src/pages/ActivityPage/index.tsx +++ b/dapp/src/pages/ActivityPage/index.tsx @@ -4,7 +4,7 @@ import { Flex, Link as ChakraLink, Icon } from "@chakra-ui/react" import { Link as ReactRouterLink, useParams } from "react-router-dom" import { useSidebar } from "#/hooks" import { ArrowLeft } from "#/assets/icons" -import { mockedActivities } from "#/components/shared/ActivityCard/mock-activities" +import { mockedActivities } from "#/mock/mock-activities" import ActivityDetails from "./ActivityDetails" import { ActivityBar } from "./ActivityBar" diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx index 8b136d0f5..2f6824a8f 100644 --- a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx +++ b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx @@ -1,12 +1,13 @@ import React, { useCallback, useRef, useState } from "react" import Slider from "react-slick" -import { Box, HStack, BoxProps } from "@chakra-ui/react" -import { ActivityCard } from "../../../components/shared/ActivityCard" +import { Box, BoxProps } from "@chakra-ui/react" +import { Carousel } from "#/components/shared/Carousel/Carousel" +import { mockedActivities } from "#/mock" +import { ActivityCard } from "#/components/shared/ActivityCard" import { activityCarouselSettings } from "./utils" -import { mockedActivities } from "../../../components/shared/ActivityCard/mock-activities" export function ActivityCarousel({ ...props }: BoxProps) { - const sliderRef = useRef(null) + const carouselRef = useRef(null) // TODO: Lines 12-30 should be replaced by redux store when subgraphs are implemented const [activities, setActivities] = useState(mockedActivities) @@ -21,9 +22,9 @@ export function ActivityCarousel({ ...props }: BoxProps) { ) const isLastCard = removedIndex === activities.length - 1 if (isLastCard) { - sliderRef.current?.slickPrev() + carouselRef.current?.slickPrev() } - sliderRef.current?.forceUpdate() + carouselRef.current?.forceUpdate() setActivities(filteredActivities) }, [activities], @@ -31,10 +32,9 @@ export function ActivityCarousel({ ...props }: BoxProps) { return ( - ))} - + ) } diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselSettings.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselSettings.tsx index 701330e08..8ff29435f 100644 --- a/dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselSettings.tsx +++ b/dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselSettings.tsx @@ -8,13 +8,6 @@ import { NextArrowCarousel, PrevArrowCarousel } from "./carouselArrows" * and it depends on the width of the viewport. * */ export const activityCarouselSettings = { - dots: false, - infinite: false, - draggable: false, - variableWidth: true, - speed: 500, - slidesToShow: 12, - slidesToScroll: 1, nextArrow: , prevArrow: , responsive: [ From a2c5017ac7a5bd0d36dfe119d1e165acf9083443 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 20 Mar 2024 22:02:12 +0100 Subject: [PATCH 011/123] Drafting Mezo Allocator Mezo Allocator will route tBTC tokens between Acre x Mezo. Mezo Allocator is the first pluggable contract to Acre system that will handle staking tBTC on various L2s/DeFi. Withdrawal part will be added in other commits. --- core/contracts/MezoAllocator.sol | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 core/contracts/MezoAllocator.sol diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol new file mode 100644 index 000000000..f96248b08 --- /dev/null +++ b/core/contracts/MezoAllocator.sol @@ -0,0 +1,74 @@ +// 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/access/Ownable2Step.sol"; + +interface IMezoPortal { + function deposit(address token, uint96 amount, uint32 lockPeriod) external; + + function withdraw(address token, uint256 depositId, uint96 amount) external; + + function depositCount() external view returns (uint256); +} + +/// @notice MezoAllocator is a contract that routes tBTC to/from MezoPortal. +contract MezoAllocator is Ownable2Step { + using SafeERC20 for IERC20; + + address public mezoPortal; + /// tBTC token contract. + IERC20 public immutable tbtc; + /// Contract holding tBTC deposited by stakers. + address public tbtcStorage; + + // Deposit ID -> Deposit Amount + mapping(uint256 => uint256) public depositsById; + // Deposit IDs + uint256[] public deposits; + + event DepositAllocated(uint256 depositId, uint256 amount); + + constructor(address _mezoPortal, IERC20 _tbtc) Ownable(msg.sender) { + mezoPortal = _mezoPortal; + tbtc = _tbtc; + } + + // TODO: replace onlyOwner with onlyMaintaier or onlyOwnerAndMaintainer. + /// @notice Deposits tBTC to MezoPortal. + /// @dev This function will be called by the bot at some interval. + function deposit(uint96 amount) external onlyOwner { + IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), amount); + // 0 means no lock. + IMezoPortal(mezoPortal).deposit(address(tbtc), amount, 0); + // MezoPortal doesn't return depositId, so we have to read depositCounter + // which assignes depositId to the current deposit. + uint256 depositId = IMezoPortal(mezoPortal).depositCount(); + depositsById[depositId] = amount; + deposits.push(depositId); + + emit DepositAllocated(depositId, amount); + } + + /// @notice Updates the tBTC storage address. + /// @dev At first this is going to be the stBTC contract. Once Acre + /// works with more destinations for tBTC, this will be updated to + /// the new storage contract like AcreDispatcher. + /// @param _tbtcStorage Address of the new tBTC storage. + function updateTbtcStorage(address _tbtcStorage) external onlyOwner { + tbtcStorage = _tbtcStorage; + } + + // TODO: add updatable withdrawer and onlyWithdrawer modifier (stBTC or AcreDispatcher). + /// @notice Withdraws tBTC from MezoPortal and transfers it to stBTC. + function withdraw(uint96 amount) external { + // TODO: Take the latest deposit and pull funds from it. + // If not enough funds, take everything from that deposit and + // take the rest from the next deposit id until the amount is + // reached. + // IMezoPortal(mezoPortal).withdraw(address(tbtc), depositId, amount); + // TODO: update depositsById and deposits data structures. + // IERC20(tbtc).safeTransfer(address(tbtcStorage), amount); + } +} From 7580b95e0f0461e7c7b163b12160df34b55737bb Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 21 Mar 2024 10:15:55 +0100 Subject: [PATCH 012/123] Adding checks, events and docs to MezoAllocator contract --- core/contracts/MezoAllocator.sol | 64 +++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index f96248b08..914520fc9 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -13,41 +13,77 @@ interface IMezoPortal { function depositCount() external view returns (uint256); } -/// @notice MezoAllocator is a contract that routes tBTC to/from MezoPortal. +/// @notice MezoAllocator routes tBTC to/from MezoPortal. contract MezoAllocator is Ownable2Step { using SafeERC20 for IERC20; - address public mezoPortal; + /// Address of the MezoPortal contract. + address public immutable mezoPortal; /// tBTC token contract. IERC20 public immutable tbtc; /// Contract holding tBTC deposited by stakers. address public tbtcStorage; + /// @notice Maintainer address which can trigger deposit flow. + address public maintainer; + // Deposit ID -> Deposit Amount mapping(uint256 => uint256) public depositsById; // Deposit IDs uint256[] public deposits; + /// Emitted when tBTC is deposited to MezoPortal. event DepositAllocated(uint256 depositId, uint256 amount); + /// @notice Emitted when the tBTC storage address is updated. + event TbtcStorageUpdated(address indexed tbtcStorage); + + /// @notice Emitted when the maintainer address is updated. + event MaintainerUpdated(address indexed maintainer); + + /// @notice Reverts if the caller is not an authorized account. + error NotAuthorized(); + + /// @notice Reverts if the address is 0. + error ZeroAddress(); + + modifier onlyMaintainerAndOwner() { + if (msg.sender != maintainer && owner() != msg.sender) { + revert NotAuthorized(); + } + _; + } + + /// @notice Initializes the MezoAllocator contract. + /// @param _mezoPortal Address of the MezoPortal contract. + /// @param _tbtc Address of the tBTC token contract. constructor(address _mezoPortal, IERC20 _tbtc) Ownable(msg.sender) { + if (_mezoPortal == address(0)) { + revert ZeroAddress(); + } + if (address(_tbtc) == address(0)) { + revert ZeroAddress(); + } mezoPortal = _mezoPortal; tbtc = _tbtc; } - // TODO: replace onlyOwner with onlyMaintaier or onlyOwnerAndMaintainer. /// @notice Deposits tBTC to MezoPortal. - /// @dev This function will be called by the bot at some interval. - function deposit(uint96 amount) external onlyOwner { + /// @dev This function can be invoked periodically by a bot. + /// @param amount Amount of tBTC to deposit to Mezo Portal. + function deposit(uint96 amount) external onlyMaintainerAndOwner { + // slither-disable-next-line arbitrary-send-erc20 IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), amount); - // 0 means no lock. + // 0 denotes no lock period for this deposit. IMezoPortal(mezoPortal).deposit(address(tbtc), amount, 0); // MezoPortal doesn't return depositId, so we have to read depositCounter // which assignes depositId to the current deposit. uint256 depositId = IMezoPortal(mezoPortal).depositCount(); + // slither-disable-next-line reentrancy-benign depositsById[depositId] = amount; deposits.push(depositId); + // slither-disable-next-line reentrancy-events emit DepositAllocated(depositId, amount); } @@ -57,7 +93,23 @@ contract MezoAllocator is Ownable2Step { /// the new storage contract like AcreDispatcher. /// @param _tbtcStorage Address of the new tBTC storage. function updateTbtcStorage(address _tbtcStorage) external onlyOwner { + if (_tbtcStorage == address(0)) { + revert ZeroAddress(); + } tbtcStorage = _tbtcStorage; + + emit TbtcStorageUpdated(_tbtcStorage); + } + + /// @notice Updates the maintainer address. + /// @param _maintainer Address of the new maintainer. + function updateMaintainer(address _maintainer) external onlyOwner { + if (_maintainer == address(0)) { + revert ZeroAddress(); + } + maintainer = _maintainer; + + emit MaintainerUpdated(_maintainer); } // TODO: add updatable withdrawer and onlyWithdrawer modifier (stBTC or AcreDispatcher). From 61b654b20970acf1cbf2447935704f56926e7f12 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 21 Mar 2024 11:18:11 +0100 Subject: [PATCH 013/123] Adding deployment scripts for Mezo Allocator - Added mainnet and sepolia addresses in Jsons for Mezo Portal - Added deployment script for Mezo Allocator contract. A fake random address is used for Mezo Portal when deploying on hardhat network. This is for testing purposes only. --- core/deploy/00_resolve_mezo_portal.ts | 23 +++++++++++++++ core/deploy/02_deploy_mezo_allocator.ts | 38 +++++++++++++++++++++++++ core/external/mainnet/MezoPortal.json | 3 ++ core/external/sepolia/MezoPortal.json | 3 ++ 4 files changed, 67 insertions(+) create mode 100644 core/deploy/00_resolve_mezo_portal.ts create mode 100644 core/deploy/02_deploy_mezo_allocator.ts create mode 100644 core/external/mainnet/MezoPortal.json create mode 100644 core/external/sepolia/MezoPortal.json diff --git a/core/deploy/00_resolve_mezo_portal.ts b/core/deploy/00_resolve_mezo_portal.ts new file mode 100644 index 000000000..02cf262b2 --- /dev/null +++ b/core/deploy/00_resolve_mezo_portal.ts @@ -0,0 +1,23 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { + HardhatNetworkConfig, + HardhatRuntimeEnvironment, +} from "hardhat/types" +import { isNonZeroAddress } from "../helpers/address" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { deployments } = hre + const { log } = deployments + + 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) { + throw new Error("deployed MezoPortal contract not found") + } +} + +export default func + +func.tags = ["MezoPortal"] diff --git a/core/deploy/02_deploy_mezo_allocator.ts b/core/deploy/02_deploy_mezo_allocator.ts new file mode 100644 index 000000000..c07250005 --- /dev/null +++ b/core/deploy/02_deploy_mezo_allocator.ts @@ -0,0 +1,38 @@ +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 { log } = deployments + + const tbtc = await deployments.get("TBTC") + // Fake random address for local development purposes only. + const fakeMezoPortal = "0x0af5DC16568EFF2d480a43A77E6C409e497FcFb9" + const mezoPortal = await deployments.getOrNull("MezoPortal") + + let mezoPortalAddress = mezoPortal?.address + if (!mezoPortalAddress && hre.network.name === "hardhat") { + mezoPortalAddress = fakeMezoPortal + log(`using fake Mezo Portal address ${mezoPortalAddress}`) + } + + const mezoAllocator = await deployments.deploy("MezoAllocator", { + from: deployer, + args: [mezoPortalAddress, tbtc.address], + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }) + + if (hre.network.tags.etherscan) { + await helpers.etherscan.verify(mezoAllocator) + } + + // TODO: Add Tenderly verification +} + +export default func + +func.tags = ["MezoAllocator"] +func.dependencies = ["TBTC"] diff --git a/core/external/mainnet/MezoPortal.json b/core/external/mainnet/MezoPortal.json new file mode 100644 index 000000000..858fc0b32 --- /dev/null +++ b/core/external/mainnet/MezoPortal.json @@ -0,0 +1,3 @@ +{ + "address": "0xAB13B8eecf5AA2460841d75da5d5D861fD5B8A39" +} \ No newline at end of file diff --git a/core/external/sepolia/MezoPortal.json b/core/external/sepolia/MezoPortal.json new file mode 100644 index 000000000..7c6c1309f --- /dev/null +++ b/core/external/sepolia/MezoPortal.json @@ -0,0 +1,3 @@ +{ + "address": "0x6978E3e11b8Bc34ea836C1706fC742aC4Cb6b0Db" +} \ No newline at end of file From 6dc9c8fa90d251d8072d25121ba34edb2814c663 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 21 Mar 2024 16:32:16 +0100 Subject: [PATCH 014/123] Adding a stub for MezoPortal for testing purposes --- core/contracts/MezoAllocator.sol | 6 ++++++ core/contracts/stBTC.sol | 3 ++- core/contracts/test/MezoPortalStub.sol | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 core/contracts/test/MezoPortalStub.sol diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 914520fc9..294aaa3ca 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -74,6 +74,7 @@ contract MezoAllocator is Ownable2Step { function deposit(uint96 amount) external onlyMaintainerAndOwner { // slither-disable-next-line arbitrary-send-erc20 IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), amount); + IERC20(tbtc).forceApprove(mezoPortal, amount); // 0 denotes no lock period for this deposit. IMezoPortal(mezoPortal).deposit(address(tbtc), amount, 0); // MezoPortal doesn't return depositId, so we have to read depositCounter @@ -112,6 +113,11 @@ contract MezoAllocator is Ownable2Step { emit MaintainerUpdated(_maintainer); } + /// @notice Returns the deposit IDs. + function getDeposits() external view returns (uint256[] memory) { + return deposits; + } + // TODO: add updatable withdrawer and onlyWithdrawer modifier (stBTC or AcreDispatcher). /// @notice Withdraws tBTC from MezoPortal and transfers it to stBTC. function withdraw(uint96 amount) external { diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index c287b322b..1fabf3b16 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -21,7 +21,8 @@ import "./lib/ERC4626Fees.sol"; contract stBTC is ERC4626Fees, Ownable2StepUpgradeable { using SafeERC20 for IERC20; - /// Dispatcher contract that routes tBTC from stBTC to a given vault and back. + /// Dispatcher contract that routes tBTC from stBTC to a given destination + /// and back. Dispatcher public dispatcher; /// Address of the treasury wallet, where fees should be transferred to. diff --git a/core/contracts/test/MezoPortalStub.sol b/core/contracts/test/MezoPortalStub.sol new file mode 100644 index 000000000..496f05394 --- /dev/null +++ b/core/contracts/test/MezoPortalStub.sol @@ -0,0 +1,22 @@ +// 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 {} + + function deposit(address token, uint96 amount, uint32 lockPeriod) external { + depositCount++; + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + } +} From 79aff1a7a8306c170507e61442c52068a1c8e182 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 21 Mar 2024 16:32:48 +0100 Subject: [PATCH 015/123] Adding deployment scripts --- core/deploy/00_resolve_mezo_portal.ts | 14 ++++++- core/deploy/02_deploy_mezo_allocator.ts | 13 +------ core/deploy/11_acre_update_dispatcher.ts | 21 ---------- .../13_mezo_allocator_update_storage.ts | 26 +++++++++++++ .../24_transfer_ownership_mezo_allocator.ts | 39 +++++++++++++++++++ 5 files changed, 80 insertions(+), 33 deletions(-) delete mode 100644 core/deploy/11_acre_update_dispatcher.ts create mode 100644 core/deploy/13_mezo_allocator_update_storage.ts create mode 100644 core/deploy/24_transfer_ownership_mezo_allocator.ts diff --git a/core/deploy/00_resolve_mezo_portal.ts b/core/deploy/00_resolve_mezo_portal.ts index 02cf262b2..312b8ab95 100644 --- a/core/deploy/00_resolve_mezo_portal.ts +++ b/core/deploy/00_resolve_mezo_portal.ts @@ -4,10 +4,12 @@ import type { HardhatRuntimeEnvironment, } from "hardhat/types" import { isNonZeroAddress } from "../helpers/address" +import { waitConfirmationsNumber } from "../helpers/deployment" const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { deployments } = hre + const { getNamedAccounts, deployments } = hre const { log } = deployments + const { deployer } = await getNamedAccounts() const mezoPortal = await deployments.getOrNull("MezoPortal") @@ -15,6 +17,16 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { log(`using MezoPortal contract at ${mezoPortal.address}`) } else if ((hre.network.config as HardhatNetworkConfig)?.forking?.enabled) { 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), + }) } } diff --git a/core/deploy/02_deploy_mezo_allocator.ts b/core/deploy/02_deploy_mezo_allocator.ts index c07250005..bcada08dd 100644 --- a/core/deploy/02_deploy_mezo_allocator.ts +++ b/core/deploy/02_deploy_mezo_allocator.ts @@ -5,22 +5,13 @@ import { waitConfirmationsNumber } from "../helpers/deployment" const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, deployments, helpers } = hre const { deployer } = await getNamedAccounts() - const { log } = deployments const tbtc = await deployments.get("TBTC") - // Fake random address for local development purposes only. - const fakeMezoPortal = "0x0af5DC16568EFF2d480a43A77E6C409e497FcFb9" - const mezoPortal = await deployments.getOrNull("MezoPortal") - - let mezoPortalAddress = mezoPortal?.address - if (!mezoPortalAddress && hre.network.name === "hardhat") { - mezoPortalAddress = fakeMezoPortal - log(`using fake Mezo Portal address ${mezoPortalAddress}`) - } + const mezoPortal = await deployments.get("MezoPortal") const mezoAllocator = await deployments.deploy("MezoAllocator", { from: deployer, - args: [mezoPortalAddress, tbtc.address], + args: [mezoPortal.address, tbtc.address], log: true, waitConfirmations: waitConfirmationsNumber(hre), }) diff --git a/core/deploy/11_acre_update_dispatcher.ts b/core/deploy/11_acre_update_dispatcher.ts deleted file mode 100644 index 6ddb718d8..000000000 --- a/core/deploy/11_acre_update_dispatcher.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { HardhatRuntimeEnvironment } from "hardhat/types" -import type { DeployFunction } from "hardhat-deploy/types" - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { getNamedAccounts, deployments } = hre - const { deployer } = await getNamedAccounts() - - const dispatcher = await deployments.get("Dispatcher") - - await deployments.execute( - "stBTC", - { from: deployer, log: true, waitConfirmations: 1 }, - "updateDispatcher", - dispatcher.address, - ) -} - -export default func - -func.tags = ["AcreUpdateDispatcher"] -func.dependencies = ["stBTC", "Dispatcher"] diff --git a/core/deploy/13_mezo_allocator_update_storage.ts b/core/deploy/13_mezo_allocator_update_storage.ts new file mode 100644 index 000000000..8f6785756 --- /dev/null +++ b/core/deploy/13_mezo_allocator_update_storage.ts @@ -0,0 +1,26 @@ +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 } = await getNamedAccounts() + + const stbtc = await deployments.get("stBTC") + + await deployments.execute( + "MezoAllocator", + { + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }, + "updateTbtcStorage", + stbtc.address, + ) +} + +export default func + +func.tags = ["MezoAllocatorUpdateStorage"] +func.dependencies = ["stBTC"] diff --git a/core/deploy/24_transfer_ownership_mezo_allocator.ts b/core/deploy/24_transfer_ownership_mezo_allocator.ts new file mode 100644 index 000000000..f6add8a43 --- /dev/null +++ b/core/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"] From 0476309be59eb26479f7359f87ca9cf0086334bd Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 21 Mar 2024 16:33:06 +0100 Subject: [PATCH 016/123] Replacing Dispatcher with MezoAllocator At first Mezo Allocator will be Acre's dispatcher. Later on a proper dispatcher will be implemented and switched with the Mezo Allocator. --- core/deploy/11_stbtc_update_dispatcher.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 core/deploy/11_stbtc_update_dispatcher.ts diff --git a/core/deploy/11_stbtc_update_dispatcher.ts b/core/deploy/11_stbtc_update_dispatcher.ts new file mode 100644 index 000000000..280950a1d --- /dev/null +++ b/core/deploy/11_stbtc_update_dispatcher.ts @@ -0,0 +1,21 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { deployer } = await getNamedAccounts() + + const dispatcher = await deployments.get("MezoAllocator") + + await deployments.execute( + "stBTC", + { from: deployer, log: true, waitConfirmations: 1 }, + "updateDispatcher", + dispatcher.address, + ) +} + +export default func + +func.tags = ["stBTCUpdateDispatcher"] +func.dependencies = ["stBTC", "Dispatcher"] From bf84e76e576d4c5261dd5a110e074e16b3984cdf Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 21 Mar 2024 16:34:32 +0100 Subject: [PATCH 017/123] Drafting first tests for MezoAllocator --- core/.solhintignore | 1 + core/test/Dispatcher.test.ts | 2 +- core/test/MezoAllocator.test.ts | 139 ++++++++++++++++++++++++++++++++ core/test/helpers/context.ts | 7 ++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 core/test/MezoAllocator.test.ts diff --git a/core/.solhintignore b/core/.solhintignore index c2658d7d1..a908c5b25 100644 --- a/core/.solhintignore +++ b/core/.solhintignore @@ -1 +1,2 @@ node_modules/ +MezoPortalStub.sol diff --git a/core/test/Dispatcher.test.ts b/core/test/Dispatcher.test.ts index 4e1e8a987..b860a70ad 100644 --- a/core/test/Dispatcher.test.ts +++ b/core/test/Dispatcher.test.ts @@ -29,7 +29,7 @@ async function fixture() { return { dispatcher, governance, thirdParty, maintainer, vault, tbtc, stbtc } } -describe("Dispatcher", () => { +describe.skip("Dispatcher", () => { let dispatcher: Dispatcher let vault: TestERC4626 let tbtc: TestERC20 diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts new file mode 100644 index 000000000..be495e121 --- /dev/null +++ b/core/test/MezoAllocator.test.ts @@ -0,0 +1,139 @@ +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 } 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, dispatcher, mezoAllocator, mezoPortal } = + await deployment() + const { governance, maintainer } = await getNamedSigners() + const [thirdParty] = await getUnnamedSigners() + + return { + dispatcher, + governance, + thirdParty, + maintainer, + tbtc, + stbtc, + mezoAllocator, + mezoPortal, + } +} + +describe("MezoAllocator", () => { + let tbtc: TestERC20 + let stbtc: stBTC + let mezoAllocator: MezoAllocator + let mezoPortal: IMezoPortal + + let governance: HardhatEthersSigner + let thirdParty: HardhatEthersSigner + let maintainer: HardhatEthersSigner + + before(async () => { + ;({ + governance, + thirdParty, + maintainer, + tbtc, + stbtc, + mezoAllocator, + mezoPortal, + } = await loadFixture(fixture)) + }) + + describe("deposit", () => { + beforeAfterSnapshotWrapper() + + before(async () => {}) + + context("when the caller is not an owner", () => { + it("should revert", async () => { + await expect( + mezoAllocator.connect(thirdParty).deposit(to1e18(1)), + ).to.be.revertedWithCustomError(mezoAllocator, "NotAuthorized") + }) + }) + + context("when the caller is an owner", () => { + it("should not revert", async () => { + await expect( + mezoAllocator.connect(governance).deposit(to1e18(1)), + ).to.not.be.revertedWithCustomError(mezoAllocator, "NotAuthorized") + }) + }) + + context("when the caller is maintainer", () => { + context("when first deposit is made", () => { + let tx: ContractTransactionResponse + + before(async () => { + await tbtc.mint(await stbtc.getAddress(), to1e18(1)) + await mezoAllocator + .connect(governance) + .updateMaintainer(maintainer.address) + + tx = await mezoAllocator.connect(maintainer).deposit(to1e18(1)) + }) + + it("should deposit and transfer tBTC to Mezo Portal", async () => { + expect( + await tbtc.balanceOf(await mezoAllocator.getAddress()), + ).to.equal(0) + expect(await tbtc.balanceOf(await mezoPortal.getAddress())).to.equal( + to1e18(1), + ) + }) + + it("should populate deposits array", async () => { + expect(await mezoAllocator.deposits(0)).to.equal(1) + }) + + it("should populate deposits mapping", async () => { + expect(await mezoAllocator.depositsById(1)).to.equal(to1e18(1)) + }) + + it("should emit Deposit event", async () => { + const latestDepositId = await mezoAllocator.deposits(0) + await expect(tx) + .to.emit(mezoAllocator, "DepositAllocated") + .withArgs(latestDepositId, to1e18(1)) + }) + }) + + context("when second deposit is made", () => { + before(async () => { + await tbtc.mint(await stbtc.getAddress(), to1e18(5)) + await mezoAllocator + .connect(governance) + .updateMaintainer(maintainer.address) + + await mezoAllocator.connect(maintainer).deposit(to1e18(5)) + }) + + it("should increment the deposits array", async () => { + expect(await mezoAllocator.deposits(1)).to.equal(2) + }) + + it("should populate deposits mapping", async () => { + expect(await mezoAllocator.depositsById(2)).to.equal(to1e18(5)) + }) + }) + }) + }) +}) diff --git a/core/test/helpers/context.ts b/core/test/helpers/context.ts index 239721235..fe7842dd4 100644 --- a/core/test/helpers/context.ts +++ b/core/test/helpers/context.ts @@ -9,6 +9,8 @@ import type { TestERC4626, TBTCVaultStub, AcreBitcoinDepositorHarness, + MezoAllocator, + MezoPortalStub, } from "../../typechain" // eslint-disable-next-line import/prefer-default-export @@ -26,6 +28,9 @@ export async function deployment() { 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, @@ -35,5 +40,7 @@ export async function deployment() { tbtcVault, dispatcher, vault, + mezoAllocator, + mezoPortal, } } From 3c72154a4f363b5d758218b4cfd55b0499d89689 Mon Sep 17 00:00:00 2001 From: ioay Date: Thu, 21 Mar 2024 22:18:11 +0100 Subject: [PATCH 018/123] Added bigint sanitizer to redux enhancers & updated serializable check --- dapp/package.json | 1 + dapp/src/store/enhancers.ts | 30 ++++++++++++++++++++++++++++++ dapp/src/store/index.ts | 2 ++ dapp/src/store/middleware.ts | 7 ++++++- dapp/src/utils/index.ts | 1 + dapp/src/utils/json.ts | 13 +++++++++++++ pnpm-lock.yaml | 14 +++++++++++++- 7 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 dapp/src/store/enhancers.ts create mode 100644 dapp/src/utils/json.ts diff --git a/dapp/package.json b/dapp/package.json index f93626084..968b3350f 100644 --- a/dapp/package.json +++ b/dapp/package.json @@ -21,6 +21,7 @@ "@emotion/styled": "^11.11.0", "@ledgerhq/wallet-api-client": "^1.5.0", "@ledgerhq/wallet-api-client-react": "^1.3.0", + "@redux-devtools/extension": "^3.3.0", "@reduxjs/toolkit": "^2.2.0", "@sentry/react": "^7.98.0", "@sentry/types": "^7.102.0", diff --git a/dapp/src/store/enhancers.ts b/dapp/src/store/enhancers.ts new file mode 100644 index 000000000..ef089a73d --- /dev/null +++ b/dapp/src/store/enhancers.ts @@ -0,0 +1,30 @@ +import { encodeJSON } from "#/utils" +import { devToolsEnhancer } from "@redux-devtools/extension" + +// This sanitizer runs on store and action data before serializing for remote +// redux devtools. The goal is to end up with an object that is directly +// JSON-serializable and deserializable; the remote end will display the +// resulting objects without additional processing or decoding logic. +function devToolsSanitizer(input: T) { + 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)) as T + default: + return input + } +} + +export const enhancers = import.meta.env.DEV + ? { + autoBatch: undefined, + devToolsEnhancer: devToolsEnhancer({ + actionSanitizer: devToolsSanitizer, + stateSanitizer: devToolsSanitizer, + }), + } + : {} diff --git a/dapp/src/store/index.ts b/dapp/src/store/index.ts index fc68092ae..b099ca384 100644 --- a/dapp/src/store/index.ts +++ b/dapp/src/store/index.ts @@ -1,10 +1,12 @@ import { configureStore } from "@reduxjs/toolkit" import { middleware } from "./middleware" import { reducer } from "./reducer" +import { enhancers } from "./enhancers" export const store = configureStore({ reducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware(middleware), + enhancers: (getDefaultEnhancers) => getDefaultEnhancers(enhancers), devTools: !import.meta.env.PROD, }) diff --git a/dapp/src/store/middleware.ts b/dapp/src/store/middleware.ts index 8950058df..80413c5ce 100644 --- a/dapp/src/store/middleware.ts +++ b/dapp/src/store/middleware.ts @@ -1,3 +1,8 @@ +import { isPlain } from "@reduxjs/toolkit" + export const middleware = { - serializableCheck: false, + serializableCheck: { + isSerializable: (value: unknown) => + isPlain(value) || typeof value === "bigint", + }, } 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..ed50aef94 --- /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: bigint | object | null): string { + return JSON.stringify(input, (_, value): object | null => { + if (typeof value === "bigint") { + return { B_I_G_I_N_T: value.toString() } + } + return value as object | null + }) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5efefe084..b7b3b6b09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,6 +141,9 @@ importers: '@ledgerhq/wallet-api-client-react': specifier: ^1.3.0 version: 1.3.0(react@18.2.0) + '@redux-devtools/extension': + specifier: ^3.3.0 + version: 3.3.0(redux@5.0.1) '@reduxjs/toolkit': specifier: ^2.2.0 version: 2.2.1(react-redux@9.1.0)(react@18.2.0) @@ -5864,6 +5867,16 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false + /@redux-devtools/extension@3.3.0(redux@5.0.1): + resolution: {integrity: sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==} + peerDependencies: + redux: ^3.1.0 || ^4.0.0 || ^5.0.0 + dependencies: + '@babel/runtime': 7.23.4 + immutable: 4.3.4 + redux: 5.0.1 + dev: false + /@reduxjs/toolkit@2.2.1(react-redux@9.1.0)(react@18.2.0): resolution: {integrity: sha512-8CREoqJovQW/5I4yvvijm/emUiCCmcs4Ev4XPWd4mizSO+dD3g5G6w34QK5AGeNrSH7qM8Fl66j4vuV7dpOdkw==} peerDependencies: @@ -13186,7 +13199,6 @@ packages: /immutable@4.3.4: resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} - dev: true /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} From 1c5585af57a5909212958cb831119c18fc91064d Mon Sep 17 00:00:00 2001 From: ioay Date: Tue, 19 Mar 2024 17:11:39 +0100 Subject: [PATCH 019/123] Structure update, moved mocked activities to standalone hooks, minor improvements --- .../animated/ArrowUpRightAnimatedIcon.tsx | 16 ++++----- .../shared/{Carousel => }/Carousel.tsx | 13 +++---- dapp/src/hooks/index.ts | 1 + dapp/src/hooks/useActivities.ts | 25 +++++++++++++ dapp/src/pages/ActivityPage/index.tsx | 16 ++++----- .../ActivityCarousel/ActivityCarousel.tsx | 36 ++++++------------- ...lArrows.tsx => ActivityCarouselArrows.tsx} | 0 ...tings.tsx => ActivityCarouselSettings.tsx} | 2 +- .../ActivityCarousel/utils/index.ts | 2 -- dapp/src/pages/OverviewPage/DocsCard.tsx | 2 +- dapp/src/pages/OverviewPage/index.tsx | 4 +-- 11 files changed, 62 insertions(+), 55 deletions(-) rename dapp/src/components/shared/{Carousel => }/Carousel.tsx (64%) create mode 100644 dapp/src/hooks/useActivities.ts rename dapp/src/pages/OverviewPage/ActivityCarousel/{utils/carouselArrows.tsx => ActivityCarouselArrows.tsx} (100%) rename dapp/src/pages/OverviewPage/ActivityCarousel/{utils/carouselSettings.tsx => ActivityCarouselSettings.tsx} (94%) delete mode 100644 dapp/src/pages/OverviewPage/ActivityCarousel/utils/index.ts diff --git a/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx b/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx index fad0bcd91..49a8309ca 100644 --- a/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx +++ b/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx @@ -1,17 +1,17 @@ import React from "react" import { ArrowUpRight } from "#/assets/icons" -import { Box, Flex, Icon } from "@chakra-ui/react" +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: 0, + y: -5, }, animate: (boxSizePx: number) => ({ x: [0, boxSizePx], - y: [0, -boxSizePx], + y: [-5, -boxSizePx], transition: { duration: 0.4, ease: "easeInOut", @@ -26,7 +26,7 @@ const arrowBottomVariants: Variants = { }), animate: (boxSizePx: number) => ({ x: [-boxSizePx, 0], - y: [boxSizePx, 0], + y: [boxSizePx, -5], transition: { duration: 0.4, ease: "easeInOut", @@ -46,7 +46,7 @@ export function ArrowUpRightAnimatedIcon({ const boxSizePx = chakraUnitToPx(boxSize) return ( - - - + - + ) } diff --git a/dapp/src/components/shared/Carousel/Carousel.tsx b/dapp/src/components/shared/Carousel.tsx similarity index 64% rename from dapp/src/components/shared/Carousel/Carousel.tsx rename to dapp/src/components/shared/Carousel.tsx index dfa20252a..524a7aa2c 100644 --- a/dapp/src/components/shared/Carousel/Carousel.tsx +++ b/dapp/src/components/shared/Carousel.tsx @@ -1,8 +1,8 @@ import React, { forwardRef } from "react" -import { BoxProps, Flex } from "@chakra-ui/react" -import Slider from "react-slick" +import { FlexProps, Flex } from "@chakra-ui/react" +import Slider, { Settings as SliderProps } from "react-slick" -const carouselSettings = { +const carouselSettings: SliderProps = { dots: false, infinite: false, draggable: false, @@ -12,9 +12,10 @@ const carouselSettings = { slidesToScroll: 1, } -type CarouselProps = BoxProps & { - children: React.ReactNode -} +type CarouselProps = FlexProps & + SliderProps & { + children: React.ReactNode + } export const Carousel = forwardRef( (props, ref) => ( diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index 292568f86..4a5e2a88f 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -17,3 +17,4 @@ export * from "./useInitApp" export * from "./useCurrencyConversion" export * from "./useDepositTelemetry" export * from "./useFetchBTCPriceUSD" +export * from "./useActivities" diff --git a/dapp/src/hooks/useActivities.ts b/dapp/src/hooks/useActivities.ts new file mode 100644 index 000000000..07c8656d3 --- /dev/null +++ b/dapp/src/hooks/useActivities.ts @@ -0,0 +1,25 @@ +import { useCallback, useState } from "react" +import { mockedActivities } from "#/mock" + +export function useActivities() { + // TODO: should be replaced by redux store when subgraphs are implemented + const [activities, setActivities] = useState(mockedActivities) + + const getActivity = useCallback( + (activityId?: string) => + activities.find((_activity) => _activity.txHash === activityId), + [activities], + ) + + const onRemove = useCallback( + (activityHash: string) => { + const filteredActivities = activities.filter( + (activity) => activity.txHash !== activityHash, + ) + setActivities(filteredActivities) + }, + [activities], + ) + + return { activities, getActivity, onRemove } +} diff --git a/dapp/src/pages/ActivityPage/index.tsx b/dapp/src/pages/ActivityPage/index.tsx index 4f445213d..6f1f450f4 100644 --- a/dapp/src/pages/ActivityPage/index.tsx +++ b/dapp/src/pages/ActivityPage/index.tsx @@ -2,19 +2,16 @@ import React, { useEffect } from "react" import { Flex, Link as ChakraLink, Icon } from "@chakra-ui/react" import { Link as ReactRouterLink, useParams } from "react-router-dom" -import { useSidebar } from "#/hooks" +import { useActivities, useSidebar } from "#/hooks" import { ArrowLeft } from "#/assets/icons" -import { mockedActivities } from "#/mock/mock-activities" import ActivityDetails from "./ActivityDetails" import { ActivityBar } from "./ActivityBar" export default function ActivityPage() { const { onOpen: openSideBar, onClose: closeSidebar } = useSidebar() + const { getActivity } = useActivities() const params = useParams() - - const selectedActivity = mockedActivities.find( - (_activity) => _activity.txHash === params.activityId, - ) + const activity = getActivity(params.activityId) useEffect(() => { openSideBar() @@ -34,11 +31,10 @@ export default function ActivityPage() { _hover={{ color: "white", bg: "brand.400" }} /> - {selectedActivity && ( + {activity && ( - {} - - + + )} diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx index 2f6824a8f..0ca460043 100644 --- a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx +++ b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx @@ -1,34 +1,20 @@ -import React, { useCallback, useRef, useState } from "react" +import React, { useRef } from "react" import Slider from "react-slick" import { Box, BoxProps } from "@chakra-ui/react" -import { Carousel } from "#/components/shared/Carousel/Carousel" -import { mockedActivities } from "#/mock" +import { Carousel } from "#/components/shared/Carousel" import { ActivityCard } from "#/components/shared/ActivityCard" -import { activityCarouselSettings } from "./utils" +import { useActivities } from "#/hooks" +import { activityCarouselSettings } from "./ActivityCarouselSettings" export function ActivityCarousel({ ...props }: BoxProps) { const carouselRef = useRef(null) + const { activities, onRemove } = useActivities() - // TODO: Lines 12-30 should be replaced by redux store when subgraphs are implemented - const [activities, setActivities] = useState(mockedActivities) - - const onRemove = useCallback( - (activityHash: string) => { - const removedIndex = activities.findIndex( - (activity) => activity.txHash === activityHash, - ) - const filteredActivities = activities.filter( - (activity) => activity.txHash !== activityHash, - ) - const isLastCard = removedIndex === activities.length - 1 - if (isLastCard) { - carouselRef.current?.slickPrev() - } - carouselRef.current?.forceUpdate() - setActivities(filteredActivities) - }, - [activities], - ) + const handleRemove = (txHash: string) => { + carouselRef.current?.slickPrev() + carouselRef.current?.forceUpdate() + onRemove(txHash) + } return ( @@ -53,7 +39,7 @@ export function ActivityCarousel({ ...props }: BoxProps) { ))} diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselArrows.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarouselArrows.tsx similarity index 100% rename from dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselArrows.tsx rename to dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarouselArrows.tsx diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselSettings.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarouselSettings.tsx similarity index 94% rename from dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselSettings.tsx rename to dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarouselSettings.tsx index 8ff29435f..ec0989a56 100644 --- a/dapp/src/pages/OverviewPage/ActivityCarousel/utils/carouselSettings.tsx +++ b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarouselSettings.tsx @@ -1,5 +1,5 @@ import React from "react" -import { NextArrowCarousel, PrevArrowCarousel } from "./carouselArrows" +import { NextArrowCarousel, PrevArrowCarousel } from "./ActivityCarouselArrows" /* * * Settings for react-slick carousel. diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/utils/index.ts b/dapp/src/pages/OverviewPage/ActivityCarousel/utils/index.ts deleted file mode 100644 index 15749ecf0..000000000 --- a/dapp/src/pages/OverviewPage/ActivityCarousel/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./carouselArrows" -export * from "./carouselSettings" diff --git a/dapp/src/pages/OverviewPage/DocsCard.tsx b/dapp/src/pages/OverviewPage/DocsCard.tsx index 0c124ffff..84aeab7ec 100644 --- a/dapp/src/pages/OverviewPage/DocsCard.tsx +++ b/dapp/src/pages/OverviewPage/DocsCard.tsx @@ -24,7 +24,7 @@ export function DocsCard({ ...props }: CardProps) { {...props} > - + Documentation Everything you need to know about our contracts. diff --git a/dapp/src/pages/OverviewPage/index.tsx b/dapp/src/pages/OverviewPage/index.tsx index 4504b8108..bf36ca424 100644 --- a/dapp/src/pages/OverviewPage/index.tsx +++ b/dapp/src/pages/OverviewPage/index.tsx @@ -18,11 +18,11 @@ export default function OverviewPage() { - + Date: Fri, 22 Mar 2024 12:24:28 +0100 Subject: [PATCH 020/123] Updating minor comments for MezoAllocator --- core/contracts/MezoAllocator.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 294aaa3ca..ab29376d3 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -121,10 +121,10 @@ contract MezoAllocator is Ownable2Step { // TODO: add updatable withdrawer and onlyWithdrawer modifier (stBTC or AcreDispatcher). /// @notice Withdraws tBTC from MezoPortal and transfers it to stBTC. function withdraw(uint96 amount) external { - // TODO: Take the latest deposit and pull funds from it. + // TODO: Take the last deposit and pull the funds from it (FIFO). // If not enough funds, take everything from that deposit and // take the rest from the next deposit id until the amount is - // reached. + // reached. Delete deposit ids that are empty. // IMezoPortal(mezoPortal).withdraw(address(tbtc), depositId, amount); // TODO: update depositsById and deposits data structures. // IERC20(tbtc).safeTransfer(address(tbtcStorage), amount); From f4a9d5001d1dc3fcf3221a2c1ee8651c8cef1084 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 22 Mar 2024 12:24:36 +0100 Subject: [PATCH 021/123] Adding more tests and deployment scripts Dispatcher in the current form will be replaced in the future. For now, the role of the dispatcher contract will server the MezoAllocator contract since we do not have any other L2s/DeFi partners yet. Once we have it, it will be updated for the proper dispatcher that will include MezoAllocator. --- core/deploy/02_deploy_mezo_allocator.ts | 2 +- .../12_mezo_allocator_update_maintainer.ts | 24 +++++++++ core/test/Deployment.test.ts | 26 +++++++--- core/test/Dispatcher.test.ts | 2 +- core/test/MezoAllocator.test.ts | 50 +++++++++++++++++++ core/test/stBTC.test.ts | 12 ++--- 6 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 core/deploy/12_mezo_allocator_update_maintainer.ts diff --git a/core/deploy/02_deploy_mezo_allocator.ts b/core/deploy/02_deploy_mezo_allocator.ts index bcada08dd..33cee9344 100644 --- a/core/deploy/02_deploy_mezo_allocator.ts +++ b/core/deploy/02_deploy_mezo_allocator.ts @@ -26,4 +26,4 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func func.tags = ["MezoAllocator"] -func.dependencies = ["TBTC"] +func.dependencies = ["TBTC", "MezoPortal"] diff --git a/core/deploy/12_mezo_allocator_update_maintainer.ts b/core/deploy/12_mezo_allocator_update_maintainer.ts new file mode 100644 index 000000000..3d9be2d05 --- /dev/null +++ b/core/deploy/12_mezo_allocator_update_maintainer.ts @@ -0,0 +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( + "MezoAllocator", + { + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }, + "updateMaintainer", + maintainer, + ) +} + +export default func + +func.tags = ["DispatcherUpdateMaintainer"] +func.dependencies = ["Dispatcher"] diff --git a/core/test/Deployment.test.ts b/core/test/Deployment.test.ts index 5914e98bb..5b8d42de8 100644 --- a/core/test/Deployment.test.ts +++ b/core/test/Deployment.test.ts @@ -6,26 +6,26 @@ 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)) }) @@ -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,15 +61,25 @@ 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 actualMaintainer = await mezoAllocator.maintainer() expect(actualMaintainer).to.be.equal(await maintainer.getAddress()) }) }) }) + + describe("updateTbtcStorage", () => { + context("when a new stBTC address has been set", () => { + it("should be set to a new stBTC address by the deployment script", async () => { + const actualTbtcStorage = await mezoAllocator.tbtcStorage() + + expect(actualTbtcStorage).to.be.equal(await stbtc.getAddress()) + }) + }) + }) }) }) diff --git a/core/test/Dispatcher.test.ts b/core/test/Dispatcher.test.ts index b860a70ad..dfe7dba92 100644 --- a/core/test/Dispatcher.test.ts +++ b/core/test/Dispatcher.test.ts @@ -28,7 +28,7 @@ async function fixture() { return { dispatcher, governance, thirdParty, maintainer, vault, tbtc, stbtc } } - +// TODO: Remove these tests once Distpather contract is removed from the project. describe.skip("Dispatcher", () => { let dispatcher: Dispatcher let vault: TestERC4626 diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index be495e121..f4dd95670 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -136,4 +136,54 @@ describe("MezoAllocator", () => { }) }) }) + + describe("updateTbtcStorage", () => { + context("when the caller is not an owner", () => { + it("should revert", async () => { + await expect( + mezoAllocator + .connect(thirdParty) + .updateTbtcStorage(thirdParty.address), + ).to.be.revertedWithCustomError( + mezoAllocator, + "OwnableUnauthorizedAccount", + ) + }) + }) + + context("when the caller is an owner", () => { + it("should not revert", async () => { + await mezoAllocator + .connect(governance) + .updateTbtcStorage(thirdParty.address) + const tbtcStorageAddress = await mezoAllocator.tbtcStorage() + expect(tbtcStorageAddress).to.equal(thirdParty.address) + }) + }) + }) + + describe("updateMaintainer", () => { + context("when the caller is not an owner", () => { + it("should revert", async () => { + await expect( + mezoAllocator + .connect(thirdParty) + .updateMaintainer(thirdParty.address), + ).to.be.revertedWithCustomError( + mezoAllocator, + "OwnableUnauthorizedAccount", + ) + }) + }) + + context("when the caller is an owner", () => { + it("should not revert", async () => { + await mezoAllocator + .connect(governance) + .updateMaintainer(thirdParty.address) + const maintainerAddress = await mezoAllocator.maintainer() + expect(maintainerAddress).to.equal(thirdParty.address) + }) + }) + }) }) diff --git a/core/test/stBTC.test.ts b/core/test/stBTC.test.ts index c5ce7d207..26c50931e 100644 --- a/core/test/stBTC.test.ts +++ b/core/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 } = await getNamedSigners() const [depositor1, depositor2, thirdParty] = await getUnnamedSigners() @@ -31,10 +31,10 @@ async function fixture() { tbtc, depositor1, depositor2, - dispatcher, governance, thirdParty, treasury, + mezoAllocator, } } @@ -45,7 +45,7 @@ describe("stBTC", () => { let stbtc: stBTC let tbtc: TestERC20 - let dispatcher: Dispatcher + let mezoAllocator: MezoAllocator let governance: HardhatEthersSigner let depositor1: HardhatEthersSigner @@ -59,10 +59,10 @@ describe("stBTC", () => { tbtc, depositor1, depositor2, - dispatcher, governance, thirdParty, treasury, + mezoAllocator, } = await loadFixture(fixture)) await stbtc @@ -1327,7 +1327,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() From 99ff8d94f30caedb03a2f98bc6bcf56fe252f571 Mon Sep 17 00:00:00 2001 From: ioay Date: Fri, 22 Mar 2024 14:15:39 +0100 Subject: [PATCH 022/123] Destructuring action creators --- dapp/src/hooks/useFetchBtcBalance.ts | 6 +++--- dapp/src/store/btc/btcSlice.ts | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dapp/src/hooks/useFetchBtcBalance.ts b/dapp/src/hooks/useFetchBtcBalance.ts index dfafbdfc9..b41eb4470 100644 --- a/dapp/src/hooks/useFetchBtcBalance.ts +++ b/dapp/src/hooks/useFetchBtcBalance.ts @@ -2,7 +2,7 @@ import { useEffect } from "react" import { EthereumAddress } from "@acre-btc/sdk" import { useAcreContext } from "#/acre-react/hooks" import { logPromiseFailure } from "#/utils" -import { btcSlice } from "#/store/btc" +import { setEstimatedBtcBalance, setSharesBalance } from "#/store/btc" import { useWalletContext } from "./useWalletContext" import { useAppDispatch } from "./store" @@ -20,8 +20,8 @@ export function useFetchBtcBalance() { const estimatedBitcoinBalance = await acre.staking.estimatedBitcoinBalance(chainIdentifier) - dispatch(btcSlice.actions.setSharesBalance(sharesBalance)) - dispatch(btcSlice.actions.setEstimatedBtcBalance(estimatedBitcoinBalance)) + dispatch(setSharesBalance(sharesBalance)) + dispatch(setEstimatedBtcBalance(estimatedBitcoinBalance)) } logPromiseFailure(getBtcBalance()) }, [acre, isInitialized, ethAccount, dispatch]) diff --git a/dapp/src/store/btc/btcSlice.ts b/dapp/src/store/btc/btcSlice.ts index 49096b7af..a3e7ce9fc 100644 --- a/dapp/src/store/btc/btcSlice.ts +++ b/dapp/src/store/btc/btcSlice.ts @@ -43,3 +43,5 @@ export const btcSlice = createSlice({ ) }, }) + +export const { setSharesBalance, setEstimatedBtcBalance } = btcSlice.actions From cea8744a1e112ab12434d45dc6014b8335f5c756 Mon Sep 17 00:00:00 2001 From: ioay Date: Fri, 22 Mar 2024 20:25:49 +0100 Subject: [PATCH 023/123] Moving logic related to activities to useActivities hook --- .../shared/ActivityCard/ActivityCard.tsx | 31 +++++++++---------- .../ActivityCard/ActivityCardWrapper.tsx | 2 +- dapp/src/hooks/useActivities.ts | 31 ++++++++++++++++--- dapp/src/pages/ActivityPage/ActivityBar.tsx | 29 +++++------------ .../pages/ActivityPage/ActivityDetails.tsx | 18 ++++++----- dapp/src/pages/ActivityPage/index.tsx | 17 ++++------ .../ActivityCarousel/ActivityCarousel.tsx | 18 ++++++----- 7 files changed, 78 insertions(+), 68 deletions(-) diff --git a/dapp/src/components/shared/ActivityCard/ActivityCard.tsx b/dapp/src/components/shared/ActivityCard/ActivityCard.tsx index b54ce0e83..72d814f44 100644 --- a/dapp/src/components/shared/ActivityCard/ActivityCard.tsx +++ b/dapp/src/components/shared/ActivityCard/ActivityCard.tsx @@ -16,21 +16,20 @@ import { CurrencyBalance } from "#/components/shared/CurrencyBalance" import StatusInfo from "#/components/shared/StatusInfo" import { TextSm } from "#/components/shared/Typography" import { routerPath } from "#/router/path" +import { useActivities } from "#/hooks" import { ActivityCardWrapper } from "./ActivityCardWrapper" type ActivityCardType = CardProps & { activity: ActivityInfo - onRemove: (txHash: string) => void - isActive?: boolean + onRemove: (activity: ActivityInfo) => void } -export function ActivityCard({ - activity, - onRemove, - isActive, -}: ActivityCardType) { +export function ActivityCard({ activity, onRemove }: ActivityCardType) { const navigate = useNavigate() - const isCompleted = activity.status === "completed" + const { isCompleted, isSelected } = useActivities() + + const isActivitySelected = isSelected(activity) + const isActivityCompleted = isCompleted(activity) const onClick = useCallback(() => { navigate(`${routerPath.activity}/${activity.txHash}`) @@ -39,17 +38,17 @@ export function ActivityCard({ const onClose = useCallback( (event: React.MouseEvent) => { event.stopPropagation() - if (activity.txHash) { - onRemove(activity.txHash) + if (activity) { + onRemove(activity) } }, - [onRemove, activity.txHash], + [activity, onRemove], ) return ( @@ -61,7 +60,7 @@ export function ActivityCard({ balanceFontWeight="black" symbolFontWeight="medium" /> - {isCompleted ? ( + {isActivityCompleted ? ( )} diff --git a/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx b/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx index 7ec00cbb3..4ac9c0691 100644 --- a/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx +++ b/dapp/src/components/shared/ActivityCard/ActivityCardWrapper.tsx @@ -2,8 +2,8 @@ import React from "react" import { CardProps, Card } from "@chakra-ui/react" type ActivityCardWrapperProps = CardProps & { - isCompleted: boolean children: React.ReactNode + isCompleted: boolean isActive?: boolean } diff --git a/dapp/src/hooks/useActivities.ts b/dapp/src/hooks/useActivities.ts index 07c8656d3..d6764549b 100644 --- a/dapp/src/hooks/useActivities.ts +++ b/dapp/src/hooks/useActivities.ts @@ -1,9 +1,12 @@ 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 [activities, setActivities] = useState(mockedActivities) + const params = useParams() const getActivity = useCallback( (activityId?: string) => @@ -11,15 +14,33 @@ export function useActivities() { [activities], ) - const onRemove = useCallback( - (activityHash: string) => { + const removeActivity = useCallback( + (activity: ActivityInfo) => { const filteredActivities = activities.filter( - (activity) => activity.txHash !== activityHash, + (_activity) => _activity.txHash !== activity.txHash, ) setActivities(filteredActivities) }, [activities], ) - return { activities, getActivity, onRemove } + const selectedActivity = getActivity(params.activityId) + + const isSelected = useCallback( + (activity: ActivityInfo): boolean => + activity.txHash === getActivity(params.activityId)?.txHash, + [getActivity, params.activityId], + ) + + const isCompleted = (activity: ActivityInfo): boolean => + activity.status === "completed" + + return { + activities, + getActivity, + removeActivity, + selectedActivity, + isCompleted, + isSelected, + } } diff --git a/dapp/src/pages/ActivityPage/ActivityBar.tsx b/dapp/src/pages/ActivityPage/ActivityBar.tsx index 8fe91bbd8..c7d2ec48d 100644 --- a/dapp/src/pages/ActivityPage/ActivityBar.tsx +++ b/dapp/src/pages/ActivityPage/ActivityBar.tsx @@ -1,31 +1,18 @@ -import React, { useCallback, useState } from "react" +import React from "react" import { VStack } from "@chakra-ui/react" -import { mockedActivities } from "#/mock/mock-activities" import { ActivityCard } from "#/components/shared/ActivityCard" -import { ActivityInfo } from "#/types" +import { useActivities } from "#/hooks" -export function ActivityBar({ activity }: { activity: ActivityInfo }) { - // TODO: Lines 8-18 should be replaced by redux store when subgraphs are implemented - const [activities, setActivities] = useState(mockedActivities) - - const onRemove = useCallback( - (activityHash: string) => { - const filteredActivities = activities.filter( - (_activity) => _activity.txHash !== activityHash, - ) - setActivities(filteredActivities) - }, - [activities], - ) +export function ActivityBar() { + const { activities, removeActivity } = useActivities() return ( - {activities.map((_activity) => ( + {activities.map((activity) => ( ))} diff --git a/dapp/src/pages/ActivityPage/ActivityDetails.tsx b/dapp/src/pages/ActivityPage/ActivityDetails.tsx index 2aa4ad2d3..dc45dd7a1 100644 --- a/dapp/src/pages/ActivityPage/ActivityDetails.tsx +++ b/dapp/src/pages/ActivityPage/ActivityDetails.tsx @@ -14,12 +14,16 @@ 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 { ActivityInfo } from "#/types" +import { useActivities } from "#/hooks" + +function ActivityDetails() { + const { selectedActivity } = useActivities() + + if (!selectedActivity) return null -function ActivityDetails({ activity }: { activity: ActivityInfo }) { return ( - {activity.status === "pending" && ( + {selectedActivity.status === "pending" && ( @@ -45,12 +49,12 @@ function ActivityDetails({ activity }: { activity: ActivityInfo }) { fontWeight="semibold" textTransform="capitalize" > - {activity.action} + {selectedActivity.action} { openSideBar() @@ -31,12 +28,10 @@ export default function ActivityPage() { _hover={{ color: "white", bg: "brand.400" }} /> - {activity && ( - - - - - )} + + + + ) } diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx index 0ca460043..8987e812a 100644 --- a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx +++ b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx @@ -1,20 +1,24 @@ -import React, { useRef } from "react" +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 { activityCarouselSettings } from "./ActivityCarouselSettings" export function ActivityCarousel({ ...props }: BoxProps) { const carouselRef = useRef(null) - const { activities, onRemove } = useActivities() + const { activities, removeActivity } = useActivities() - const handleRemove = (txHash: string) => { - carouselRef.current?.slickPrev() - carouselRef.current?.forceUpdate() - onRemove(txHash) - } + const handleRemove = useCallback( + (activity: ActivityInfo) => { + carouselRef.current?.slickPrev() + carouselRef.current?.forceUpdate() + removeActivity(activity) + }, + [removeActivity], + ) return ( From ae675fda04dad72496d9570bc44f8b27322e5d0f Mon Sep 17 00:00:00 2001 From: ioay Date: Fri, 22 Mar 2024 20:31:00 +0100 Subject: [PATCH 024/123] Carousel arrow rendering (mapping) --- .../animated/ArrowUpRightAnimatedIcon.tsx | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx b/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx index 49a8309ca..ea95c22b7 100644 --- a/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx +++ b/dapp/src/assets/icons/animated/ArrowUpRightAnimatedIcon.tsx @@ -46,24 +46,21 @@ export function ArrowUpRightAnimatedIcon({ const boxSizePx = chakraUnitToPx(boxSize) return ( - - - - - - + {[ + { id: "arrow-up", variants: arrowUpVariants }, + { id: "arrow-bottom", variants: arrowBottomVariants }, + ].map(({ id, variants }) => ( + + + + ))} ) } From ebb5f11b2101a575cd50d12ce6817171f3507e77 Mon Sep 17 00:00:00 2001 From: ioay Date: Fri, 22 Mar 2024 20:34:39 +0100 Subject: [PATCH 025/123] Reduce width of gradient under Documentation Card --- .../pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx index 8987e812a..c11ac88ea 100644 --- a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx +++ b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx @@ -33,7 +33,7 @@ export function ActivityCarousel({ ...props }: BoxProps) { content: '""', pos: "absolute", right: 0, - w: 20, + w: 8, height: 40, bgGradient: "linear(to-r, transparent, gold.300)", }} From 4d73d9eeecb2fcf72534187fb7b6b4cec67c961b Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 25 Mar 2024 10:33:45 +0100 Subject: [PATCH 026/123] Adding hardhat network check for resolve_mezo_portal script --- core/deploy/00_resolve_mezo_portal.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/deploy/00_resolve_mezo_portal.ts b/core/deploy/00_resolve_mezo_portal.ts index 312b8ab95..412c468d6 100644 --- a/core/deploy/00_resolve_mezo_portal.ts +++ b/core/deploy/00_resolve_mezo_portal.ts @@ -15,7 +15,10 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { if (mezoPortal && isNonZeroAddress(mezoPortal.address)) { log(`using MezoPortal contract at ${mezoPortal.address}`) - } else if ((hre.network.config as HardhatNetworkConfig)?.forking?.enabled) { + } 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") From 94c96e7989017d5776a287d1bd13b20a563e0ee5 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 26 Mar 2024 16:34:04 +0100 Subject: [PATCH 027/123] Adding creation and unlock deposit timestamps for future use It might happen that we'll need a data structure to support information like deposit creation and unclock time in case we shift from the zero time locks. --- core/contracts/MezoAllocator.sol | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index ab29376d3..55ed4176a 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -17,6 +17,14 @@ interface IMezoPortal { contract MezoAllocator is Ownable2Step { using SafeERC20 for IERC20; + /// @notice DepositInfo keeps track of the deposit balance, creation time, + /// and unlock time. + struct DepositInfo { + uint96 balance; + uint32 createdAt; + uint32 unlockAt; + } + /// Address of the MezoPortal contract. address public immutable mezoPortal; /// tBTC token contract. @@ -27,8 +35,8 @@ contract MezoAllocator is Ownable2Step { /// @notice Maintainer address which can trigger deposit flow. address public maintainer; - // Deposit ID -> Deposit Amount - mapping(uint256 => uint256) public depositsById; + // Deposit ID -> Deposit Info + mapping(uint256 => DepositInfo) public depositsById; // Deposit IDs uint256[] public deposits; @@ -75,13 +83,18 @@ contract MezoAllocator is Ownable2Step { // slither-disable-next-line arbitrary-send-erc20 IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), amount); IERC20(tbtc).forceApprove(mezoPortal, amount); - // 0 denotes no lock period for this deposit. + // 0 denotes no lock period for this deposit. The zero lock time is + // hardcoded as of biz decision. IMezoPortal(mezoPortal).deposit(address(tbtc), amount, 0); // MezoPortal doesn't return depositId, so we have to read depositCounter // which assignes depositId to the current deposit. uint256 depositId = IMezoPortal(mezoPortal).depositCount(); // slither-disable-next-line reentrancy-benign - depositsById[depositId] = amount; + depositsById[depositId] = DepositInfo({ + balance: amount, + createdAt: uint32(block.timestamp), + unlockAt: uint32(block.timestamp) + }); deposits.push(depositId); // slither-disable-next-line reentrancy-events From 0f1ac2389c4d046a21ad319f4241292e571ea23c Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 27 Mar 2024 12:36:39 +0100 Subject: [PATCH 028/123] Cleanups and smaller refactorings around MezoAllocator - Made Slither and Solhing happier - Renamed the tag name for Mezo Allocator --- core/contracts/MezoAllocator.sol | 14 ++++++++------ core/deploy/12_mezo_allocator_update_maintainer.ts | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 55ed4176a..c4efa81f9 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -84,7 +84,7 @@ contract MezoAllocator is Ownable2Step { IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), amount); IERC20(tbtc).forceApprove(mezoPortal, amount); // 0 denotes no lock period for this deposit. The zero lock time is - // hardcoded as of biz decision. + // hardcoded as of biz decision. IMezoPortal(mezoPortal).deposit(address(tbtc), amount, 0); // MezoPortal doesn't return depositId, so we have to read depositCounter // which assignes depositId to the current deposit. @@ -92,7 +92,9 @@ contract MezoAllocator is Ownable2Step { // slither-disable-next-line reentrancy-benign depositsById[depositId] = DepositInfo({ balance: amount, + // solhint-disable-next-line not-rely-on-time createdAt: uint32(block.timestamp), + // solhint-disable-next-line not-rely-on-time unlockAt: uint32(block.timestamp) }); deposits.push(depositId); @@ -126,11 +128,6 @@ contract MezoAllocator is Ownable2Step { emit MaintainerUpdated(_maintainer); } - /// @notice Returns the deposit IDs. - function getDeposits() external view returns (uint256[] memory) { - return deposits; - } - // TODO: add updatable withdrawer and onlyWithdrawer modifier (stBTC or AcreDispatcher). /// @notice Withdraws tBTC from MezoPortal and transfers it to stBTC. function withdraw(uint96 amount) external { @@ -142,4 +139,9 @@ contract MezoAllocator is Ownable2Step { // TODO: update depositsById and deposits data structures. // IERC20(tbtc).safeTransfer(address(tbtcStorage), amount); } + + /// @notice Returns the deposit IDs. + function getDeposits() external view returns (uint256[] memory) { + return deposits; + } } diff --git a/core/deploy/12_mezo_allocator_update_maintainer.ts b/core/deploy/12_mezo_allocator_update_maintainer.ts index 3d9be2d05..4e1f5fa18 100644 --- a/core/deploy/12_mezo_allocator_update_maintainer.ts +++ b/core/deploy/12_mezo_allocator_update_maintainer.ts @@ -20,5 +20,5 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func -func.tags = ["DispatcherUpdateMaintainer"] +func.tags = ["MezoAllocatorUpdateMaintainer"] func.dependencies = ["Dispatcher"] From b24448e30169f35354f8191b8c7a505ae7f39716 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 27 Mar 2024 12:42:06 +0100 Subject: [PATCH 029/123] Adding tests supporting DepositInfo struct --- core/test/MezoAllocator.test.ts | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index f4dd95670..9dfd2b2c1 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -60,8 +60,6 @@ describe("MezoAllocator", () => { describe("deposit", () => { beforeAfterSnapshotWrapper() - before(async () => {}) - context("when the caller is not an owner", () => { it("should revert", async () => { await expect( @@ -104,8 +102,31 @@ describe("MezoAllocator", () => { expect(await mezoAllocator.deposits(0)).to.equal(1) }) - it("should populate deposits mapping", async () => { - expect(await mezoAllocator.depositsById(1)).to.equal(to1e18(1)) + it("should set deposit balance", async () => { + const deposit = await mezoAllocator.depositsById(1) + expect(deposit.balance).to.equal(to1e18(1)) + }) + + it("should set creation timestamp", async () => { + const deposit = await mezoAllocator.depositsById(1) + const dateTime = new Date() + // Check if the block timestamp is within 60 seconds of the current + // test time + expect(deposit.createdAt).to.be.closeTo( + String(dateTime.valueOf()).slice(0, -3), + 60, + ) + }) + + it("should set unlocking timestamp", async () => { + const deposit = await mezoAllocator.depositsById(1) + const dateTime = new Date() + // Check if the block timestamp is within 60 seconds of the current + // test time + expect(deposit.unlockAt).to.be.closeTo( + String(dateTime.valueOf()).slice(0, -3), + 60, + ) }) it("should emit Deposit event", async () => { @@ -131,7 +152,8 @@ describe("MezoAllocator", () => { }) it("should populate deposits mapping", async () => { - expect(await mezoAllocator.depositsById(2)).to.equal(to1e18(5)) + const deposit = await mezoAllocator.depositsById(2) + expect(deposit.balance).to.equal(to1e18(5)) }) }) }) From 40e709a1a6d137a1fa79fc9f5459e7306e1a0f9e Mon Sep 17 00:00:00 2001 From: ioay Date: Wed, 3 Apr 2024 18:40:15 +0200 Subject: [PATCH 030/123] Docs card minor change, rename settings file --- .../pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx | 2 +- .../{ActivityCarouselSettings.tsx => settings.tsx} | 0 dapp/src/pages/OverviewPage/DocsCard.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename dapp/src/pages/OverviewPage/ActivityCarousel/{ActivityCarouselSettings.tsx => settings.tsx} (100%) diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx index c11ac88ea..e0c63c936 100644 --- a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx +++ b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx @@ -5,7 +5,7 @@ import { Carousel } from "#/components/shared/Carousel" import { ActivityCard } from "#/components/shared/ActivityCard" import { useActivities } from "#/hooks" import { ActivityInfo } from "#/types" -import { activityCarouselSettings } from "./ActivityCarouselSettings" +import { activityCarouselSettings } from "./settings" export function ActivityCarousel({ ...props }: BoxProps) { const carouselRef = useRef(null) diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarouselSettings.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/settings.tsx similarity index 100% rename from dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarouselSettings.tsx rename to dapp/src/pages/OverviewPage/ActivityCarousel/settings.tsx diff --git a/dapp/src/pages/OverviewPage/DocsCard.tsx b/dapp/src/pages/OverviewPage/DocsCard.tsx index 84aeab7ec..42db18181 100644 --- a/dapp/src/pages/OverviewPage/DocsCard.tsx +++ b/dapp/src/pages/OverviewPage/DocsCard.tsx @@ -5,7 +5,7 @@ import { TextSm } from "#/components/shared/Typography" import { ArrowUpRightAnimatedIcon } from "#/assets/icons/animated" import { motion } from "framer-motion" -export function DocsCard({ ...props }: CardProps) { +export function DocsCard(props: CardProps) { const { onOpen } = useDocsDrawer() return ( From 427921d943ad49636aeea99d5b637b09a8c7a967 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 3 Apr 2024 20:49:15 +0200 Subject: [PATCH 031/123] Rename Bitcoin Depositor contract We renamed the depositor contract from AcreBitcoinDepositor to BitcoinDepositor, as there is no point in prefixing the contract with Acre. We used to do that for ethereum typed messages clear signing, but we no longer need ethereum messages signing with Bitcoin-only experience. --- ...coinDepositor.sol => BitcoinDepositor.sol} | 9 +++---- ...arness.sol => BitcoinDepositorHarness.sol} | 10 +++++--- ....sol => BitcoinDepositorMisplacedSlot.sol} | 10 ++++---- ...ot.sol => BitcoinDepositorMissingSlot.sol} | 10 ++++---- ...DepositorV2.sol => BitcoinDepositorV2.sol} | 11 +++----- ...itor.ts => 03_deploy_bitcoin_depositor.ts} | 22 ++++++---------- ...3_transfer_ownership_bitcoin_depositor.ts} | 12 ++++----- ...inDepositor.json => BitcoinDepositor.json} | 0 ...sitor.test.ts => BitcoinDepositor.test.ts} | 6 ++--- ...st.ts => BitcoinDepositor.upgrade.test.ts} | 25 +++++++++---------- core/test/helpers/context.ts | 6 ++--- sdk/src/lib/contracts/bitcoin-depositor.ts | 2 +- ...inDepositor.json => BitcoinDepositor.json} | 0 sdk/src/lib/ethereum/bitcoin-depositor.ts | 8 +++--- .../modules/staking/stake-initialization.ts | 2 +- sdk/test/lib/ethereum/eip712.test.ts | 2 +- sdk/test/modules/staking.test.ts | 2 +- 17 files changed, 62 insertions(+), 75 deletions(-) rename core/contracts/{AcreBitcoinDepositor.sol => BitcoinDepositor.sol} (99%) rename core/contracts/test/{AcreBitcoinDepositorHarness.sol => BitcoinDepositorHarness.sol} (90%) rename core/contracts/test/upgrades/{AcreBitcoinDepositorMisplacedSlot.sol => BitcoinDepositorMisplacedSlot.sol} (98%) rename core/contracts/test/upgrades/{AcreBitcoinDepositorMissingSlot.sol => BitcoinDepositorMissingSlot.sol} (98%) rename core/contracts/test/upgrades/{AcreBitcoinDepositorV2.sol => BitcoinDepositorV2.sol} (98%) rename core/deploy/{03_deploy_acre_bitcoin_depositor.ts => 03_deploy_bitcoin_depositor.ts} (68%) rename core/deploy/{23_transfer_ownership_acre_bitcoin_depositor.ts => 23_transfer_ownership_bitcoin_depositor.ts} (72%) rename core/deployments/sepolia/{AcreBitcoinDepositor.json => BitcoinDepositor.json} (100%) rename core/test/{AcreBitcoinDepositor.test.ts => BitcoinDepositor.test.ts} (99%) rename core/test/{AcreBitcoinDepositor.upgrade.test.ts => BitcoinDepositor.upgrade.test.ts} (90%) rename sdk/src/lib/ethereum/artifacts/sepolia/{AcreBitcoinDepositor.json => BitcoinDepositor.json} (100%) diff --git a/core/contracts/AcreBitcoinDepositor.sol b/core/contracts/BitcoinDepositor.sol similarity index 99% rename from core/contracts/AcreBitcoinDepositor.sol rename to core/contracts/BitcoinDepositor.sol index 6e46fbcab..f2d514ac5 100644 --- a/core/contracts/AcreBitcoinDepositor.sol +++ b/core/contracts/BitcoinDepositor.sol @@ -13,7 +13,7 @@ import {stBTC} from "./stBTC.sol"; // TODO: Make Pausable -/// @title Acre Bitcoin Depositor contract. +/// @title Bitcoin Depositor contract. /// @notice The contract integrates Acre staking with tBTC minting. /// User who wants to stake BTC in Acre should submit a Bitcoin transaction /// to the most recently created off-chain ECDSA wallets of the tBTC Bridge @@ -37,10 +37,7 @@ import {stBTC} from "./stBTC.sol"; /// 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 -{ +contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { using SafeERC20 for IERC20; /// @notice State of the stake request. @@ -270,7 +267,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. diff --git a/core/contracts/test/AcreBitcoinDepositorHarness.sol b/core/contracts/test/BitcoinDepositorHarness.sol similarity index 90% rename from core/contracts/test/AcreBitcoinDepositorHarness.sol rename to core/contracts/test/BitcoinDepositorHarness.sol index 71b0b3a33..3a87bb3af 100644 --- a/core/contracts/test/AcreBitcoinDepositorHarness.sol +++ b/core/contracts/test/BitcoinDepositorHarness.sol @@ -2,17 +2,17 @@ /* 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"; import {TestERC20} from "./TestERC20.sol"; -/// @dev A test contract to expose internal function from AcreBitcoinDepositor contract. +/// @dev A test contract to expose internal function from BitcoinDepositor contract. /// This solution follows Foundry recommendation: /// https://book.getfoundry.sh/tutorials/best-practices#internal-functions -contract AcreBitcoinDepositorHarness is AcreBitcoinDepositor { +contract BitcoinDepositorHarness is BitcoinDepositor { function exposed_finalizeBridging( uint256 depositKey ) external returns (uint256 amountToStake, address staker) { @@ -25,7 +25,9 @@ contract AcreBitcoinDepositorHarness is AcreBitcoinDepositor { } /// @dev A test contract to stub tBTC Bridge contract. -contract BridgeStub is MockBridge {} +contract BridgeStub is MockBridge { + +} /// @dev A test contract to stub tBTC Vault contract. contract TBTCVaultStub is MockTBTCVault { diff --git a/core/contracts/test/upgrades/AcreBitcoinDepositorMisplacedSlot.sol b/core/contracts/test/upgrades/BitcoinDepositorMisplacedSlot.sol similarity index 98% rename from core/contracts/test/upgrades/AcreBitcoinDepositorMisplacedSlot.sol rename to core/contracts/test/upgrades/BitcoinDepositorMisplacedSlot.sol index 744ebe5bb..848facd99 100644 --- a/core/contracts/test/upgrades/AcreBitcoinDepositorMisplacedSlot.sol +++ b/core/contracts/test/upgrades/BitcoinDepositorMisplacedSlot.sol @@ -11,11 +11,11 @@ import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol"; import {stBTC} from "../../stBTC.sol"; -/// @title AcreBitcoinDepositorMisplacedSlot -/// @dev This is a contract used to test Acre Bitcoin Depositor upgradeability. -/// It is a copy of AcreBitcoinDepositor contract with some differences +/// @title BitcoinDepositorMisplacedSlot +/// @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 AcreBitcoinDepositorMisplacedSlot is +contract BitcoinDepositorMisplacedSlot is AbstractTBTCDepositor, Ownable2StepUpgradeable { @@ -251,7 +251,7 @@ contract AcreBitcoinDepositorMisplacedSlot 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. diff --git a/core/contracts/test/upgrades/AcreBitcoinDepositorMissingSlot.sol b/core/contracts/test/upgrades/BitcoinDepositorMissingSlot.sol similarity index 98% rename from core/contracts/test/upgrades/AcreBitcoinDepositorMissingSlot.sol rename to core/contracts/test/upgrades/BitcoinDepositorMissingSlot.sol index 13213eac7..83190769c 100644 --- a/core/contracts/test/upgrades/AcreBitcoinDepositorMissingSlot.sol +++ b/core/contracts/test/upgrades/BitcoinDepositorMissingSlot.sol @@ -11,11 +11,11 @@ import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol"; import {stBTC} from "../../stBTC.sol"; -/// @title AcreBitcoinDepositorMissingSlot -/// @dev This is a contract used to test Acre Bitcoin Depositor upgradeability. -/// It is a copy of AcreBitcoinDepositor contract with some differences +/// @title BitcoinDepositorMissingSlot +/// @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 AcreBitcoinDepositorMissingSlot is +contract BitcoinDepositorMissingSlot is AbstractTBTCDepositor, Ownable2StepUpgradeable { @@ -249,7 +249,7 @@ contract AcreBitcoinDepositorMissingSlot 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. diff --git a/core/contracts/test/upgrades/AcreBitcoinDepositorV2.sol b/core/contracts/test/upgrades/BitcoinDepositorV2.sol similarity index 98% rename from core/contracts/test/upgrades/AcreBitcoinDepositorV2.sol rename to core/contracts/test/upgrades/BitcoinDepositorV2.sol index 911570891..f826bc95f 100644 --- a/core/contracts/test/upgrades/AcreBitcoinDepositorV2.sol +++ b/core/contracts/test/upgrades/BitcoinDepositorV2.sol @@ -11,14 +11,11 @@ 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. diff --git a/core/deploy/03_deploy_acre_bitcoin_depositor.ts b/core/deploy/03_deploy_bitcoin_depositor.ts similarity index 68% rename from core/deploy/03_deploy_acre_bitcoin_depositor.ts rename to core/deploy/03_deploy_bitcoin_depositor.ts index 73df43e9c..476cd4e08 100644 --- a/core/deploy/03_deploy_acre_bitcoin_depositor.ts +++ b/core/deploy/03_deploy_bitcoin_depositor.ts @@ -12,13 +12,13 @@ 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", { contractName: process.env.HARDHAT_TEST === "true" - ? "AcreBitcoinDepositorHarness" - : "AcreBitcoinDepositor", + ? "BitcoinDepositorHarness" + : "BitcoinDepositor", factoryOpts: { signer: deployer, }, @@ -35,15 +35,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 @@ -51,5 +45,5 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func -func.tags = ["AcreBitcoinDepositor"] +func.tags = ["BitcoinDepositor"] func.dependencies = ["TBTC", "stBTC"] diff --git a/core/deploy/23_transfer_ownership_acre_bitcoin_depositor.ts b/core/deploy/23_transfer_ownership_bitcoin_depositor.ts similarity index 72% rename from core/deploy/23_transfer_ownership_acre_bitcoin_depositor.ts rename to core/deploy/23_transfer_ownership_bitcoin_depositor.ts index c04f6f679..8d6d5ffde 100644 --- a/core/deploy/23_transfer_ownership_acre_bitcoin_depositor.ts +++ b/core/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,5 +26,5 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func -func.tags = ["TransferOwnershipAcreBitcoinDepositor"] -func.dependencies = ["AcreBitcoinDepositor"] +func.tags = ["TransferOwnershipBitcoinDepositor"] +func.dependencies = ["BitcoinDepositor"] diff --git a/core/deployments/sepolia/AcreBitcoinDepositor.json b/core/deployments/sepolia/BitcoinDepositor.json similarity index 100% rename from core/deployments/sepolia/AcreBitcoinDepositor.json rename to core/deployments/sepolia/BitcoinDepositor.json diff --git a/core/test/AcreBitcoinDepositor.test.ts b/core/test/BitcoinDepositor.test.ts similarity index 99% rename from core/test/AcreBitcoinDepositor.test.ts rename to core/test/BitcoinDepositor.test.ts index 313ba35a3..498edd0d5 100644 --- a/core/test/AcreBitcoinDepositor.test.ts +++ b/core/test/BitcoinDepositor.test.ts @@ -12,7 +12,7 @@ import type { StBTC, BridgeStub, TBTCVaultStub, - AcreBitcoinDepositorHarness, + BitcoinDepositorHarness, 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 @@ -50,7 +50,7 @@ describe("AcreBitcoinDepositor", () => { const depositorFee = to1ePrecision(10, 10) // 10 satoshi const amountToStake = to1ePrecision(896501, 8) // 8965,01 satoshi - let bitcoinDepositor: AcreBitcoinDepositorHarness + let bitcoinDepositor: BitcoinDepositorHarness let tbtcBridge: BridgeStub let tbtcVault: TBTCVaultStub let stbtc: StBTC diff --git a/core/test/AcreBitcoinDepositor.upgrade.test.ts b/core/test/BitcoinDepositor.upgrade.test.ts similarity index 90% rename from core/test/AcreBitcoinDepositor.upgrade.test.ts rename to core/test/BitcoinDepositor.upgrade.test.ts index 7bad1eda9..3016d2ed9 100644 --- a/core/test/AcreBitcoinDepositor.upgrade.test.ts +++ b/core/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 () => { @@ -45,8 +45,8 @@ describe("AcreBitcoinDepositor contract upgrade", () => { it("should throw an error", async () => { await expect( helpers.upgrades.upgradeProxy( - "AcreBitcoinDepositor", - "AcreBitcoinDepositorMisplacedSlot", + "BitcoinDepositor", + "BitcoinDepositorMisplacedSlot", { initializerArgs: [ await tbtcBridge.getAddress(), @@ -67,8 +67,8 @@ describe("AcreBitcoinDepositor contract upgrade", () => { it("should throw an error", async () => { await expect( helpers.upgrades.upgradeProxy( - "AcreBitcoinDepositor", - "AcreBitcoinDepositorMissingSlot", + "BitcoinDepositor", + "BitcoinDepositorMissingSlot", { initializerArgs: [ await tbtcBridge.getAddress(), @@ -86,7 +86,7 @@ describe("AcreBitcoinDepositor contract upgrade", () => { context("when upgrading to a valid contract", () => { const newVariable = 1n - let bitcoinDepositorV2: AcreBitcoinDepositorV2 + let bitcoinDepositorV2: BitcoinDepositorV2 let v1InitialParameters: { minStakeAmount: bigint maxSingleStakeAmount: bigint @@ -111,8 +111,8 @@ describe("AcreBitcoinDepositor contract upgrade", () => { } const [upgradedDepositor] = await helpers.upgrades.upgradeProxy( - "AcreBitcoinDepositor", - "AcreBitcoinDepositorV2", + "BitcoinDepositor", + "BitcoinDepositorV2", { factoryOpts: { signer: governance }, proxyOpts: { @@ -124,8 +124,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 () => { diff --git a/core/test/helpers/context.ts b/core/test/helpers/context.ts index 239721235..839138653 100644 --- a/core/test/helpers/context.ts +++ b/core/test/helpers/context.ts @@ -8,7 +8,7 @@ import type { BridgeStub, TestERC4626, TBTCVaultStub, - AcreBitcoinDepositorHarness, + BitcoinDepositorHarness, } from "../../typechain" // eslint-disable-next-line import/prefer-default-export @@ -16,8 +16,8 @@ export async function deployment() { await deployments.fixture() const stbtc: stBTC = await getDeployedContract("stBTC") - const bitcoinDepositor: AcreBitcoinDepositorHarness = - await getDeployedContract("AcreBitcoinDepositor") + const bitcoinDepositor: BitcoinDepositorHarness = + await getDeployedContract("BitcoinDepositor") const tbtc: TestERC20 = await getDeployedContract("TBTC") const tbtcBridge: BridgeStub = await getDeployedContract("Bridge") diff --git a/sdk/src/lib/contracts/bitcoin-depositor.ts b/sdk/src/lib/contracts/bitcoin-depositor.ts index 960bc4e1f..749f78b73 100644 --- a/sdk/src/lib/contracts/bitcoin-depositor.ts +++ b/sdk/src/lib/contracts/bitcoin-depositor.ts @@ -10,7 +10,7 @@ export type DecodedExtraData = { } /** - * Interface for communication with the AcreBitcoinDepositor on-chain contract. + * Interface for communication with the BitcoinDepositor on-chain contract. */ export interface BitcoinDepositor extends DepositorProxy { /** diff --git a/sdk/src/lib/ethereum/artifacts/sepolia/AcreBitcoinDepositor.json b/sdk/src/lib/ethereum/artifacts/sepolia/BitcoinDepositor.json similarity index 100% rename from sdk/src/lib/ethereum/artifacts/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..d968ee5b6 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/core/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) { diff --git a/sdk/src/modules/staking/stake-initialization.ts b/sdk/src/modules/staking/stake-initialization.ts index 4d66a111c..869de4452 100644 --- a/sdk/src/modules/staking/stake-initialization.ts +++ b/sdk/src/modules/staking/stake-initialization.ts @@ -120,7 +120,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/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/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(), From bcf91ae5d28f59322f300013ec5593d44af3c353 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 3 Apr 2024 21:03:24 +0200 Subject: [PATCH 032/123] Fix code format --- core/contracts/test/BitcoinDepositorHarness.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/contracts/test/BitcoinDepositorHarness.sol b/core/contracts/test/BitcoinDepositorHarness.sol index 3a87bb3af..7b2f7ac41 100644 --- a/core/contracts/test/BitcoinDepositorHarness.sol +++ b/core/contracts/test/BitcoinDepositorHarness.sol @@ -25,9 +25,7 @@ contract BitcoinDepositorHarness is BitcoinDepositor { } /// @dev A test contract to stub tBTC Bridge contract. -contract BridgeStub is MockBridge { - -} +contract BridgeStub is MockBridge {} /// @dev A test contract to stub tBTC Vault contract. contract TBTCVaultStub is MockTBTCVault { From ed9139db4345031b0ad425f541bafb3ad57dfdff Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Sun, 10 Mar 2024 23:27:56 +0100 Subject: [PATCH 033/123] Implement Bitcoin Redeemer contract The contract is used to integrate stBTC unstaking with bridging to Bitcoin via tBTC Bridge. The `requestRedemption` function expects the owner of stBTC shares to approve stBTC token shares to the BticoinRedeemer contract. Then when `requestRedemption` function is called it redeems stBTC and calls TBTC token contract to redeem tBTC to Bitcoin. --- core/contracts/BitcoinRedeemer.sol | 89 ++++++++++++++++++++++++++++ core/contracts/bridge/ITBTCToken.sol | 25 ++++++++ 2 files changed, 114 insertions(+) create mode 100644 core/contracts/BitcoinRedeemer.sol create mode 100644 core/contracts/bridge/ITBTCToken.sol diff --git a/core/contracts/BitcoinRedeemer.sol b/core/contracts/BitcoinRedeemer.sol new file mode 100644 index 000000000..0c21c7829 --- /dev/null +++ b/core/contracts/BitcoinRedeemer.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "./stBTC.sol"; +import "./bridge/ITBTCToken.sol"; + +/// @title Bitcoin Redeemer +/// @notice This contract facilitates redemption of stBTC tokens to Bitcoin through +/// tBTC redemption process. +contract BitcoinRedeemer is Initializable { + /// Interface for tBTC token contract. + ITBTCToken public tbtcToken; + + /// stBTC token contract. + stBTC public stbtc; + + /// 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 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 + function initialize(address _tbtcToken, address _stbtc) public initializer { + if (address(_tbtcToken) == address(0)) { + revert TbtcTokenZeroAddress(); + } + if (address(_stbtc) == address(0)) { + revert StbtcZeroAddress(); + } + + tbtcToken = ITBTCToken(_tbtcToken); + stbtc = stBTC(_stbtc); + } + + /// @notice Initiates the redemption process by exchanging stBTC tokens for + /// tBTC tokens. + /// @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 + /// @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. + function requestRedemption( + address owner, + uint256 shares, + bytes calldata tbtcRedemptionData + ) public { + uint256 tbtcAmount = stbtc.redeem(shares, address(this), owner); + + emit RedemptionRequested(owner, shares, tbtcAmount); + + if ( + !tbtcToken.approveAndCall( + tbtcToken.owner(), + tbtcAmount, + tbtcRedemptionData + ) + ) { + revert ApproveAndCallFailed(); + } + } +} diff --git a/core/contracts/bridge/ITBTCToken.sol b/core/contracts/bridge/ITBTCToken.sol new file mode 100644 index 000000000..7b660ec06 --- /dev/null +++ b/core/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); +} From 71f4708efc1d23b58418b41b23f60432bbd71d34 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Sun, 10 Mar 2024 23:37:11 +0100 Subject: [PATCH 034/123] Add reference to BitcoinRedeemer in stBTC --- core/contracts/stBTC.sol | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index c287b322b..8c3d30d24 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "./BitcoinRedeemer.sol"; import "./Dispatcher.sol"; import "./lib/ERC4626Fees.sol"; @@ -24,6 +25,9 @@ contract stBTC is ERC4626Fees, Ownable2StepUpgradeable { /// Dispatcher contract that routes tBTC from stBTC to a given vault and back. Dispatcher public dispatcher; + /// BitcoinRedeemer contract. + BitcoinRedeemer public bitcoinRedeemer; + /// Address of the treasury wallet, where fees should be transferred to. address public treasury; @@ -55,6 +59,14 @@ contract stBTC is ERC4626Fees, Ownable2StepUpgradeable { uint256 maximumTotalAssets ); + /// Emitted when the BitcoinRedeemer contract is updated. + /// @param oldBitcoinRedeemer Address of the old BitcoinRedeemer contract. + /// @param newBitcoinRedeemer Address of the new BitcoinRedeemer contract. + event BitcoinRedeemerUpdated( + address oldBitcoinRedeemer, + address newBitcoinRedeemer + ); + /// Emitted when the dispatcher contract is updated. /// @param oldDispatcher Address of the old dispatcher contract. /// @param newDispatcher Address of the new dispatcher contract. @@ -139,6 +151,23 @@ contract stBTC is ERC4626Fees, Ownable2StepUpgradeable { ); } + /// @notice Updates the BitcoinRedeemer contract. + /// @param newBitcoinRedeemer Address of the new BitcoinRedeemer contract. + function updateBitcoinRedeemer( + address newBitcoinRedeemer + ) external onlyOwner { + if (newBitcoinRedeemer == address(0)) { + revert ZeroAddress(); + } + + emit BitcoinRedeemerUpdated( + address(bitcoinRedeemer), + newBitcoinRedeemer + ); + + bitcoinRedeemer = BitcoinRedeemer(newBitcoinRedeemer); + } + // 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 From 64a797f0466e3bcbf11b6d90f55aeb46314930ce Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Sun, 10 Mar 2024 23:44:00 +0100 Subject: [PATCH 035/123] Add test implementation of TBTC token --- core/contracts/test/TestTBTC.sol | 35 ++++++++++++++++++++++++++++ core/deploy/00_resolve_tbtc_token.ts | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 core/contracts/test/TestTBTC.sol diff --git a/core/contracts/test/TestTBTC.sol b/core/contracts/test/TestTBTC.sol new file mode 100644 index 000000000..83e8f2b8a --- /dev/null +++ b/core/contracts/test/TestTBTC.sol @@ -0,0 +1,35 @@ +// 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; + + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + + 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 owner() external pure returns (address) { + return address(1); + } +} diff --git a/core/deploy/00_resolve_tbtc_token.ts b/core/deploy/00_resolve_tbtc_token.ts index ef00bcdef..d1ddf731e 100644 --- a/core/deploy/00_resolve_tbtc_token.ts +++ b/core/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, From 20099f10e92d94aa1bef8a3565dd93349ffc464e Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Sun, 10 Mar 2024 23:45:35 +0100 Subject: [PATCH 036/123] Add deployment script for BitcoinRedeemer contract --- core/deploy/04_deploy_bitcoin_redeemer.ts | 35 +++++++++++++++++++ .../13_stbtc_update_bitcoin_redeemer.ts | 21 +++++++++++ 2 files changed, 56 insertions(+) create mode 100644 core/deploy/04_deploy_bitcoin_redeemer.ts create mode 100644 core/deploy/13_stbtc_update_bitcoin_redeemer.ts diff --git a/core/deploy/04_deploy_bitcoin_redeemer.ts b/core/deploy/04_deploy_bitcoin_redeemer.ts new file mode 100644 index 000000000..4eeccf4cf --- /dev/null +++ b/core/deploy/04_deploy_bitcoin_redeemer.ts @@ -0,0 +1,35 @@ +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 [_, deployment] = await helpers.upgrades.deployProxy( + "BitcoinRedeemer", + { + contractName: "BitcoinRedeemer", + initializerArgs: [tbtc.address, stbtc.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"] diff --git a/core/deploy/13_stbtc_update_bitcoin_redeemer.ts b/core/deploy/13_stbtc_update_bitcoin_redeemer.ts new file mode 100644 index 000000000..d7a2de43a --- /dev/null +++ b/core/deploy/13_stbtc_update_bitcoin_redeemer.ts @@ -0,0 +1,21 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { deployer } = await getNamedAccounts() + + const bitcoinRedeemer = await deployments.get("BitcoinRedeemer") + + await deployments.execute( + "stBTC", + { from: deployer, log: true, waitConfirmations: 1 }, + "updateBitcoinRedeemer", + bitcoinRedeemer.address, + ) +} + +export default func + +func.tags = ["AcreUpdateBitcoinRedeemer"] +func.dependencies = ["stBTC", "BitcoinRedeemer"] From 3d8cc5a1c0b0da296a1b8ac4be9dd07674b7826b Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 11 Mar 2024 00:08:35 +0100 Subject: [PATCH 037/123] Disable slither's reentrancy-event for requestRedemption The event is emitted after stbtc.redeem call, but we control the stBTC contract, so there is no risk. --- core/contracts/BitcoinRedeemer.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/core/contracts/BitcoinRedeemer.sol b/core/contracts/BitcoinRedeemer.sol index 0c21c7829..7f2de4cd3 100644 --- a/core/contracts/BitcoinRedeemer.sol +++ b/core/contracts/BitcoinRedeemer.sol @@ -74,6 +74,7 @@ contract BitcoinRedeemer is Initializable { ) public { uint256 tbtcAmount = stbtc.redeem(shares, address(this), owner); + // slither-disable-next-line reentrancy-events emit RedemptionRequested(owner, shares, tbtcAmount); if ( From d8eba95dc681ef0ee845841ae9eb151280becfb1 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 11 Mar 2024 22:57:14 +0100 Subject: [PATCH 038/123] Add unit tests for updateBitcoinRedeemer --- core/test/stBTC.test.ts | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/core/test/stBTC.test.ts b/core/test/stBTC.test.ts index 192adfcef..741da6d29 100644 --- a/core/test/stBTC.test.ts +++ b/core/test/stBTC.test.ts @@ -1362,6 +1362,53 @@ describe("stBTC", () => { }) }) + describe("updateBitcoinRedeemer", () => { + beforeAfterSnapshotWrapper() + + context("when caller is not governance", () => { + it("should revert", async () => { + await expect( + stbtc.connect(thirdParty).updateBitcoinRedeemer(ZeroAddress), + ) + .to.be.revertedWithCustomError(stbtc, "OwnableUnauthorizedAccount") + .withArgs(thirdParty.address) + }) + }) + + context("when caller is governance", () => { + context("when a new BitcoinRedeemer is zero address", () => { + it("should revert", async () => { + await expect( + stbtc.connect(governance).updateBitcoinRedeemer(ZeroAddress), + ).to.be.revertedWithCustomError(stbtc, "ZeroAddress") + }) + }) + + context("when a new BitcoinRedeemer is an allowed address", () => { + let newBitcoinRedeemer: string + let tx: ContractTransactionResponse + + before(async () => { + newBitcoinRedeemer = await ethers.Wallet.createRandom().getAddress() + + tx = await stbtc + .connect(governance) + .updateBitcoinRedeemer(newBitcoinRedeemer) + }) + + it("should update the treasury", async () => { + expect(await stbtc.bitcoinRedeemer()).to.be.equal(newBitcoinRedeemer) + }) + + it("should emit BitcoinRedeemerUpdated event", async () => { + await expect(tx) + .to.emit(stbtc, "BitcoinRedeemerUpdated") + .withArgs(await bitcoinRedeemer.getAddress(), newBitcoinRedeemer) + }) + }) + }) + }) + describe("updateTreasury", () => { beforeAfterSnapshotWrapper() From b16704947f34f8e160192a83ab66eaa56cb88c30 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 11 Mar 2024 22:57:42 +0100 Subject: [PATCH 039/123] Use beforeAfterSnapshotWrapper in updateDispatcher test We use the wrapper in tests instead the snapshots, so we want to be consistent. --- core/test/stBTC.test.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/core/test/stBTC.test.ts b/core/test/stBTC.test.ts index 741da6d29..98e112985 100644 --- a/core/test/stBTC.test.ts +++ b/core/test/stBTC.test.ts @@ -1293,15 +1293,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 () => { From a0d5055aeff24196b763b4d65fbca7c497b309fc Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 11 Mar 2024 22:58:58 +0100 Subject: [PATCH 040/123] Modify TreasuryUpdated event to include old address For consistency with BitcoinRedeemerUpdated and DispatcherUpdated we added old treasury address in the event. --- core/contracts/stBTC.sol | 10 ++++++---- core/test/stBTC.test.ts | 11 ++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 8c3d30d24..6fd6489b1 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -48,8 +48,9 @@ contract stBTC is ERC4626Fees, Ownable2StepUpgradeable { 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. @@ -124,9 +125,10 @@ contract stBTC is ERC4626Fees, Ownable2StepUpgradeable { if (newTreasury == address(this)) { revert DisallowedAddress(); } - treasury = newTreasury; - emit TreasuryUpdated(newTreasury); + emit TreasuryUpdated(treasury, newTreasury); + + treasury = newTreasury; } /// @notice Updates deposit parameters. diff --git a/core/test/stBTC.test.ts b/core/test/stBTC.test.ts index 98e112985..59934ab3b 100644 --- a/core/test/stBTC.test.ts +++ b/core/test/stBTC.test.ts @@ -1430,19 +1430,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) + }) }) }) }) From f9ded483fd5712a1b42de6f2f5efbb549a086511 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 13 Mar 2024 10:40:06 +0100 Subject: [PATCH 041/123] Rename requestRedemption to redeemSharesAndUnmint This function name should better describe what is happening inside. --- core/contracts/BitcoinRedeemer.sol | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/contracts/BitcoinRedeemer.sol b/core/contracts/BitcoinRedeemer.sol index 7f2de4cd3..9b74563ad 100644 --- a/core/contracts/BitcoinRedeemer.sol +++ b/core/contracts/BitcoinRedeemer.sol @@ -55,7 +55,7 @@ contract BitcoinRedeemer is Initializable { } /// @notice Initiates the redemption process by exchanging stBTC tokens for - /// tBTC tokens. + /// 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 @@ -63,11 +63,15 @@ contract BitcoinRedeemer is Initializable { /// 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 + /// 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. /// @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. - function requestRedemption( + /// See `redemptionData` parameter description of `Bridge.requestRedemption` + /// function. + function redeemSharesAndUnmint( address owner, uint256 shares, bytes calldata tbtcRedemptionData From 57b56d27ecb455c1378b02e0f89bd6db22d0d98e Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 15 Mar 2024 15:38:30 +0100 Subject: [PATCH 042/123] Implement approveAndCall patter in stBTC contract Instead of requiring the user to approve and request redemption in two transactions we use approveAndCall pattern to cover it within one transaction. --- core/contracts/BitcoinRedeemer.sol | 36 ++++++++++++++++++++++++++++-- core/contracts/stBTC.sol | 28 +++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/core/contracts/BitcoinRedeemer.sol b/core/contracts/BitcoinRedeemer.sol index 9b74563ad..158024004 100644 --- a/core/contracts/BitcoinRedeemer.sol +++ b/core/contracts/BitcoinRedeemer.sol @@ -2,13 +2,16 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import "@thesis/solidity-contracts/contracts/token/IReceiveApproval.sol"; + import "./stBTC.sol"; import "./bridge/ITBTCToken.sol"; /// @title Bitcoin Redeemer /// @notice This contract facilitates redemption of stBTC tokens to Bitcoin through /// tBTC redemption process. -contract BitcoinRedeemer is Initializable { +contract BitcoinRedeemer is Initializable, IReceiveApproval { /// Interface for tBTC token contract. ITBTCToken public tbtcToken; @@ -31,6 +34,15 @@ contract BitcoinRedeemer is Initializable { /// Reverts if the stBTC address is zero. error StbtcZeroAddress(); + /// 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(); + /// Reverts when approveAndCall to tBTC contract fails. error ApproveAndCallFailed(); @@ -54,6 +66,26 @@ contract BitcoinRedeemer is Initializable { stbtc = stBTC(_stbtc); } + /// @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 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 @@ -75,7 +107,7 @@ contract BitcoinRedeemer is Initializable { address owner, uint256 shares, bytes calldata tbtcRedemptionData - ) public { + ) internal { uint256 tbtcAmount = stbtc.redeem(shares, address(this), owner); // slither-disable-next-line reentrancy-events diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 6fd6489b1..7058cfccf 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@thesis/solidity-contracts/contracts/token/IReceiveApproval.sol"; + import "./BitcoinRedeemer.sol"; import "./Dispatcher.sol"; import "./lib/ERC4626Fees.sol"; @@ -222,6 +224,32 @@ contract stBTC is ERC4626Fees, Ownable2StepUpgradeable { 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; + } + /// @notice Mints shares to receiver by depositing exactly amount of /// tBTC tokens. /// @dev Takes into account a deposit parameter, minimum deposit amount, From e40158c48d67f2f019bbda86baf4c80db55d57bb Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 3 Apr 2024 21:44:17 +0200 Subject: [PATCH 043/123] Add dependency to @thesis-co/solidity-contracts We use @thesis-co/solidity-contracts in BitcoinRedeemer for receive approval interfaces. --- core/package.json | 1 + pnpm-lock.yaml | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/core/package.json b/core/package.json index 3a4cb303d..51403a40b 100644 --- a/core/package.json +++ b/core/package.json @@ -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/pnpm-lock.yaml b/pnpm-lock.yaml index cbf6ccdfc..771014a36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@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 @@ -22154,6 +22157,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' From cf86aa5fccfe362061c0b11039ae01ad2d4ad920 Mon Sep 17 00:00:00 2001 From: ioay Date: Thu, 4 Apr 2024 00:56:11 +0200 Subject: [PATCH 044/123] Naming convention for useFetchBTCBalance, enhancers & devtools update --- dapp/package.json | 1 - dapp/src/hooks/index.ts | 1 + dapp/src/hooks/useFetchBTCBalance.ts | 28 ++++++++++++ dapp/src/hooks/useFetchBtcBalance.ts | 2 +- dapp/src/hooks/useInitApp.ts | 4 +- dapp/src/store/devTools.ts | 22 +++++++++ dapp/src/store/enhancers.ts | 30 ------------- dapp/src/store/index.ts | 5 +-- dapp/src/utils/json.ts | 5 ++- pnpm-lock.yaml | 67 ++++++++++++++++++---------- 10 files changed, 103 insertions(+), 62 deletions(-) create mode 100644 dapp/src/hooks/useFetchBTCBalance.ts create mode 100644 dapp/src/store/devTools.ts delete mode 100644 dapp/src/store/enhancers.ts diff --git a/dapp/package.json b/dapp/package.json index 968b3350f..f93626084 100644 --- a/dapp/package.json +++ b/dapp/package.json @@ -21,7 +21,6 @@ "@emotion/styled": "^11.11.0", "@ledgerhq/wallet-api-client": "^1.5.0", "@ledgerhq/wallet-api-client-react": "^1.3.0", - "@redux-devtools/extension": "^3.3.0", "@reduxjs/toolkit": "^2.2.0", "@sentry/react": "^7.98.0", "@sentry/types": "^7.102.0", diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index 292568f86..3a581470e 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -17,3 +17,4 @@ export * from "./useInitApp" export * from "./useCurrencyConversion" export * from "./useDepositTelemetry" export * from "./useFetchBTCPriceUSD" +export * from "./useFetchBTCBalance" 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/useFetchBtcBalance.ts b/dapp/src/hooks/useFetchBtcBalance.ts index b41eb4470..360135312 100644 --- a/dapp/src/hooks/useFetchBtcBalance.ts +++ b/dapp/src/hooks/useFetchBtcBalance.ts @@ -6,7 +6,7 @@ import { setEstimatedBtcBalance, setSharesBalance } from "#/store/btc" import { useWalletContext } from "./useWalletContext" import { useAppDispatch } from "./store" -export function useFetchBtcBalance() { +export function useFetchBTCBalance() { const { acre, isInitialized } = useAcreContext() const { ethAccount } = useWalletContext() const dispatch = useAppDispatch() diff --git a/dapp/src/hooks/useInitApp.ts b/dapp/src/hooks/useInitApp.ts index 570651bf1..906bcd608 100644 --- a/dapp/src/hooks/useInitApp.ts +++ b/dapp/src/hooks/useInitApp.ts @@ -1,7 +1,7 @@ import { useSentry } from "./sentry" import { useInitializeAcreSdk } from "./useInitializeAcreSdk" import { useFetchBTCPriceUSD } from "./useFetchBTCPriceUSD" -import { useFetchBtcBalance } from "./useFetchBtcBalance" +import { useFetchBTCBalance } from "./useFetchBTCBalance" export function useInitApp() { // TODO: Let's uncomment when dark mode is ready @@ -9,5 +9,5 @@ export function useInitApp() { useSentry() useInitializeAcreSdk() useFetchBTCPriceUSD() - useFetchBtcBalance() + useFetchBTCBalance() } diff --git a/dapp/src/store/devTools.ts b/dapp/src/store/devTools.ts new file mode 100644 index 000000000..220b34182 --- /dev/null +++ b/dapp/src/store/devTools.ts @@ -0,0 +1,22 @@ +import { encodeJSON } from "#/utils" + +function devToolsSanitizer(input: T) { + 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)) as T + default: + return input + } +} + +export const devTools = !import.meta.env.PROD + ? { + actionSanitizer: devToolsSanitizer, + stateSanitizer: devToolsSanitizer, + } + : false diff --git a/dapp/src/store/enhancers.ts b/dapp/src/store/enhancers.ts deleted file mode 100644 index ef089a73d..000000000 --- a/dapp/src/store/enhancers.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { encodeJSON } from "#/utils" -import { devToolsEnhancer } from "@redux-devtools/extension" - -// This sanitizer runs on store and action data before serializing for remote -// redux devtools. The goal is to end up with an object that is directly -// JSON-serializable and deserializable; the remote end will display the -// resulting objects without additional processing or decoding logic. -function devToolsSanitizer(input: T) { - 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)) as T - default: - return input - } -} - -export const enhancers = import.meta.env.DEV - ? { - autoBatch: undefined, - devToolsEnhancer: devToolsEnhancer({ - actionSanitizer: devToolsSanitizer, - stateSanitizer: devToolsSanitizer, - }), - } - : {} diff --git a/dapp/src/store/index.ts b/dapp/src/store/index.ts index b099ca384..3353ac94a 100644 --- a/dapp/src/store/index.ts +++ b/dapp/src/store/index.ts @@ -1,13 +1,12 @@ import { configureStore } from "@reduxjs/toolkit" import { middleware } from "./middleware" import { reducer } from "./reducer" -import { enhancers } from "./enhancers" +import { devTools } from "./devTools" export const store = configureStore({ reducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware(middleware), - enhancers: (getDefaultEnhancers) => getDefaultEnhancers(enhancers), - devTools: !import.meta.env.PROD, + devTools, }) export type RootState = ReturnType diff --git a/dapp/src/utils/json.ts b/dapp/src/utils/json.ts index ed50aef94..3ceb7a3c2 100644 --- a/dapp/src/utils/json.ts +++ b/dapp/src/utils/json.ts @@ -3,11 +3,12 @@ * * @param input an object, array, or primitive to encode as JSON */ -export function encodeJSON(input: bigint | object | null): string { +export function encodeJSON(input: unknown): string { return JSON.stringify(input, (_, value): object | null => { if (typeof value === "bigint") { return { B_I_G_I_N_T: value.toString() } } - return value as object | null + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return value }) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5533f025e..3960009bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,9 +150,6 @@ importers: '@ledgerhq/wallet-api-client-react': specifier: ^1.3.0 version: 1.3.0(react@18.2.0) - '@redux-devtools/extension': - specifier: ^3.3.0 - version: 3.3.0(redux@5.0.1) '@reduxjs/toolkit': specifier: ^2.2.0 version: 2.2.1(react-redux@9.1.0)(react@18.2.0) @@ -167,7 +164,7 @@ importers: version: 8.11.7(react-dom@18.2.0)(react@18.2.0) axios: specifier: ^1.6.7 - version: 1.6.7(debug@4.3.4) + version: 1.6.7 ethers: specifier: ^6.10.0 version: 6.10.0 @@ -474,7 +471,7 @@ packages: '@babel/traverse': 7.23.4 '@babel/types': 7.23.4 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1773,7 +1770,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.4 '@babel/types': 7.23.4 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3497,7 +3494,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 espree: 9.6.1 globals: 13.23.0 ignore: 5.3.0 @@ -4152,7 +4149,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -6189,16 +6186,6 @@ packages: resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} dev: false - /@redux-devtools/extension@3.3.0(redux@5.0.1): - resolution: {integrity: sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==} - peerDependencies: - redux: ^3.1.0 || ^4.0.0 || ^5.0.0 - dependencies: - '@babel/runtime': 7.23.4 - immutable: 4.3.4 - redux: 5.0.1 - dev: false - /@reduxjs/toolkit@2.2.1(react-redux@9.1.0)(react@18.2.0): resolution: {integrity: sha512-8CREoqJovQW/5I4yvvijm/emUiCCmcs4Ev4XPWd4mizSO+dD3g5G6w34QK5AGeNrSH7qM8Fl66j4vuV7dpOdkw==} peerDependencies: @@ -7339,7 +7326,7 @@ packages: '@typescript-eslint/type-utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) '@typescript-eslint/utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 eslint: 8.54.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -7384,7 +7371,7 @@ packages: '@typescript-eslint/types': 6.12.0 '@typescript-eslint/typescript-estree': 6.12.0(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 eslint: 8.54.0 typescript: 5.3.2 transitivePeerDependencies: @@ -7435,7 +7422,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.12.0(typescript@5.3.2) '@typescript-eslint/utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 eslint: 8.54.0 ts-api-utils: 1.0.3(typescript@5.3.2) typescript: 5.3.2 @@ -7482,7 +7469,7 @@ packages: dependencies: '@typescript-eslint/types': 6.12.0 '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -8304,6 +8291,16 @@ packages: transitivePeerDependencies: - debug + /axios@1.6.7: + resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} + dependencies: + follow-redirects: 1.15.5 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axios@1.6.7(debug@4.3.4): resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} dependencies: @@ -8312,6 +8309,7 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug + dev: true /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} @@ -10246,6 +10244,17 @@ packages: dependencies: ms: 2.1.3 + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -11494,7 +11503,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -12304,6 +12313,16 @@ packages: dependencies: debug: 4.3.4(supports-color@8.1.1) + /follow-redirects@1.15.5: + resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /follow-redirects@1.15.5(debug@4.3.4): resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} engines: {node: '>=4.0'} @@ -12314,6 +12333,7 @@ packages: optional: true dependencies: debug: 4.3.4(supports-color@8.1.1) + dev: true /follow-redirects@1.5.10: resolution: {integrity: sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==} @@ -13947,6 +13967,7 @@ packages: /immutable@4.3.4: resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} + dev: true /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} From b677f93c97f343bb8ac0604abcfdb82a022d44af Mon Sep 17 00:00:00 2001 From: ioay Date: Thu, 4 Apr 2024 10:09:43 +0200 Subject: [PATCH 045/123] Naming convention for btc hooks --- dapp/src/hooks/store/index.ts | 2 +- dapp/src/hooks/store/useEstimatedBTCBalance.ts | 6 ++++++ dapp/src/hooks/store/useEstimatedBtcBalance.ts | 2 +- dapp/src/pages/OverviewPage/PositionDetails.tsx | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 dapp/src/hooks/store/useEstimatedBTCBalance.ts diff --git a/dapp/src/hooks/store/index.ts b/dapp/src/hooks/store/index.ts index 9afcc7285..379d10f6b 100644 --- a/dapp/src/hooks/store/index.ts +++ b/dapp/src/hooks/store/index.ts @@ -1,3 +1,3 @@ export * from "./useAppDispatch" export * from "./useAppSelector" -export * from "./useEstimatedBtcBalance" +export * from "./useEstimatedBTCBalance" 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/useEstimatedBtcBalance.ts b/dapp/src/hooks/store/useEstimatedBtcBalance.ts index 9820c6c4b..b8ce0bb67 100644 --- a/dapp/src/hooks/store/useEstimatedBtcBalance.ts +++ b/dapp/src/hooks/store/useEstimatedBtcBalance.ts @@ -1,6 +1,6 @@ import { selectEstimatedBtcBalance } from "#/store/btc" import { useAppSelector } from "./useAppSelector" -export function useEstimatedBtcBalance() { +export function useEstimatedBTCBalance() { return useAppSelector(selectEstimatedBtcBalance) } diff --git a/dapp/src/pages/OverviewPage/PositionDetails.tsx b/dapp/src/pages/OverviewPage/PositionDetails.tsx index f6d9d42b5..b28460108 100644 --- a/dapp/src/pages/OverviewPage/PositionDetails.tsx +++ b/dapp/src/pages/OverviewPage/PositionDetails.tsx @@ -14,10 +14,10 @@ import { TextMd } from "#/components/shared/Typography" import { Info } from "#/assets/icons" import { ACTION_FLOW_TYPES, ActionFlowType } from "#/types" import TransactionModal from "#/components/TransactionModal" -import { useEstimatedBtcBalance } from "#/hooks/store" +import { useEstimatedBTCBalance } from "#/hooks/store" export default function PositionDetails(props: CardProps) { - const estimatedBtcBalance = useEstimatedBtcBalance() + const estimatedBtcBalance = useEstimatedBTCBalance() const [actionFlowType, setActionFlowType] = useState< ActionFlowType | undefined >(undefined) From 638dc46cce5e5f26bbe679169fee9c0bebda6857 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 4 Apr 2024 10:47:45 +0200 Subject: [PATCH 046/123] Udpate import of @thesis-co/solidity-contracts The package is named: `@thesis-co/solidity-contracts` in its package.json file. To reduce confusion we match the name here. --- core/contracts/BitcoinRedeemer.sol | 2 +- core/contracts/stBTC.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/contracts/BitcoinRedeemer.sol b/core/contracts/BitcoinRedeemer.sol index 158024004..438f4c4e2 100644 --- a/core/contracts/BitcoinRedeemer.sol +++ b/core/contracts/BitcoinRedeemer.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@thesis/solidity-contracts/contracts/token/IReceiveApproval.sol"; +import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol"; import "./stBTC.sol"; import "./bridge/ITBTCToken.sol"; diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 7058cfccf..73c1d9760 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import "@thesis/solidity-contracts/contracts/token/IReceiveApproval.sol"; +import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol"; import "./BitcoinRedeemer.sol"; import "./Dispatcher.sol"; From f96c1b201ec366a483c164084eb137c0936f7428 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 4 Apr 2024 12:05:26 +0200 Subject: [PATCH 047/123] Add unit tests for BitcoinRedeemer contract --- core/contracts/test/TestTBTC.sol | 4 + core/test/BitcoinRedeemer.test.ts | 236 ++++++++++++++++++++++++++++++ core/test/data/tbtc.ts | 9 +- core/test/helpers/context.ts | 8 +- core/test/stBTC.test.ts | 12 +- 5 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 core/test/BitcoinRedeemer.test.ts diff --git a/core/contracts/test/TestTBTC.sol b/core/contracts/test/TestTBTC.sol index 83e8f2b8a..63bf580c3 100644 --- a/core/contracts/test/TestTBTC.sol +++ b/core/contracts/test/TestTBTC.sol @@ -32,4 +32,8 @@ contract TestTBTC is ITBTCToken, ERC20 { function owner() external pure returns (address) { return address(1); } + + function setApproveAndCallResult(bool value) public { + approveAndCallResult = value; + } } diff --git a/core/test/BitcoinRedeemer.test.ts b/core/test/BitcoinRedeemer.test.ts new file mode 100644 index 000000000..f456b5fed --- /dev/null +++ b/core/test/BitcoinRedeemer.test.ts @@ -0,0 +1,236 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" +import { expect } from "chai" +import { ContractTransactionResponse, encodeBytes32String } from "ethers" +import { 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 { getUnnamedSigners } = helpers.signers + +async function fixture() { + const { tbtc, stbtc, bitcoinRedeemer } = await deployment() + + const [depositor] = await getUnnamedSigners() + + const amountToMint = to1e18(100000) + await tbtc.mint(depositor, amountToMint) + + return { + stbtc, + tbtc, + bitcoinRedeemer, + depositor, + } +} + +describe("BitcoinRedeemer", () => { + let stbtc: stBTC + let tbtc: TestTBTC + let bitcoinRedeemer: BitcoinRedeemer + + let depositor: HardhatEthersSigner + + before(async () => { + ;({ stbtc, tbtc, bitcoinRedeemer, depositor } = 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 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, "ERC4626ExceededMaxRedeem") + .withArgs(await depositor.getAddress(), to1e18(1), 0) + }) + }) + + 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, + "ERC4626ExceededMaxRedeem", + ) + .withArgs( + await depositor.getAddress(), + amountToRedeem, + depositAmount, + ) + }) + }) + + 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 burn stBTC tokens", async () => { + await expect(tx).to.changeTokenBalances( + stbtc, + [depositor], + [-stBtcAmountToRedeem], + ) + + 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", + ) + }) + }) + }) + }) + }) + }) + }) +}) diff --git a/core/test/data/tbtc.ts b/core/test/data/tbtc.ts index fd491081f..fcf97c50d 100644 --- a/core/test/data/tbtc.ts +++ b/core/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 @@ -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/context.ts b/core/test/helpers/context.ts index 239721235..713c852e0 100644 --- a/core/test/helpers/context.ts +++ b/core/test/helpers/context.ts @@ -4,11 +4,12 @@ import { getDeployedContract } from "./contract" import type { StBTC as stBTC, Dispatcher, - TestERC20, BridgeStub, TestERC4626, TBTCVaultStub, AcreBitcoinDepositorHarness, + BitcoinRedeemer, + TestTBTC, } from "../../typechain" // eslint-disable-next-line import/prefer-default-export @@ -18,8 +19,10 @@ export async function deployment() { const stbtc: stBTC = await getDeployedContract("stBTC") const bitcoinDepositor: AcreBitcoinDepositorHarness = await getDeployedContract("AcreBitcoinDepositor") + 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") @@ -31,6 +34,7 @@ export async function deployment() { tbtc, stbtc, bitcoinDepositor, + bitcoinRedeemer, tbtcBridge, tbtcVault, dispatcher, diff --git a/core/test/stBTC.test.ts b/core/test/stBTC.test.ts index 59934ab3b..c69330260 100644 --- a/core/test/stBTC.test.ts +++ b/core/test/stBTC.test.ts @@ -12,12 +12,17 @@ import { beforeAfterSnapshotWrapper, deployment } from "./helpers" import { to1e18 } from "./utils" -import type { StBTC as stBTC, TestERC20, Dispatcher } from "../typechain" +import type { + StBTC as stBTC, + TestERC20, + Dispatcher, + BitcoinRedeemer, +} from "../typechain" const { getNamedSigners, getUnnamedSigners } = helpers.signers async function fixture() { - const { tbtc, stbtc, dispatcher } = await deployment() + const { tbtc, stbtc, dispatcher, bitcoinRedeemer } = await deployment() const { governance, treasury } = await getNamedSigners() const [depositor1, depositor2, thirdParty] = await getUnnamedSigners() @@ -29,6 +34,7 @@ async function fixture() { return { stbtc, tbtc, + bitcoinRedeemer, depositor1, depositor2, dispatcher, @@ -46,6 +52,7 @@ describe("stBTC", () => { let stbtc: stBTC let tbtc: TestERC20 let dispatcher: Dispatcher + let bitcoinRedeemer: BitcoinRedeemer let governance: HardhatEthersSigner let depositor1: HardhatEthersSigner @@ -57,6 +64,7 @@ describe("stBTC", () => { ;({ stbtc, tbtc, + bitcoinRedeemer, depositor1, depositor2, dispatcher, From 08165fd4ee210db52cbbb2d48112b1729e6ba468 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 4 Apr 2024 12:08:24 +0200 Subject: [PATCH 048/123] Update test contract for stBTC upgrades --- core/contracts/test/upgrades/stBTCV2.sol | 145 ++++++++++++++++++++--- 1 file changed, 129 insertions(+), 16 deletions(-) diff --git a/core/contracts/test/upgrades/stBTCV2.sol b/core/contracts/test/upgrades/stBTCV2.sol index 586c1e246..4e49b0938 100644 --- a/core/contracts/test/upgrades/stBTCV2.sol +++ b/core/contracts/test/upgrades/stBTCV2.sol @@ -1,22 +1,27 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol"; + +import "../../BitcoinRedeemer.sol"; import "../../Dispatcher.sol"; +import "../../lib/ERC4626Fees.sol"; /// @title stBTCV2 /// @dev This is a contract used to test stBTC upgradeability. It is a copy of /// stBTC contract with some differences marked with `TEST:` comments. -contract stBTCV2 is ERC4626Upgradeable, Ownable2StepUpgradeable { +contract stBTCV2 is ERC4626Fees, Ownable2StepUpgradeable { using SafeERC20 for IERC20; /// Dispatcher contract that routes tBTC from stBTC to a given vault and back. Dispatcher public dispatcher; + /// BitcoinRedeemer contract. + BitcoinRedeemer public bitcoinRedeemer; + /// Address of the treasury wallet, where fees should be transferred to. address public treasury; @@ -40,8 +45,9 @@ contract stBTCV2 is ERC4626Upgradeable, Ownable2StepUpgradeable { 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. @@ -51,11 +57,27 @@ contract stBTCV2 is ERC4626Upgradeable, Ownable2StepUpgradeable { uint256 maximumTotalAssets ); + /// Emitted when the BitcoinRedeemer contract is updated. + /// @param oldBitcoinRedeemer Address of the old BitcoinRedeemer contract. + /// @param newBitcoinRedeemer Address of the new BitcoinRedeemer contract. + event BitcoinRedeemerUpdated( + address oldBitcoinRedeemer, + address newBitcoinRedeemer + ); + /// Emitted when the dispatcher contract is updated. /// @param oldDispatcher Address of the old dispatcher contract. /// @param newDispatcher Address of the new dispatcher contract. event DispatcherUpdated(address oldDispatcher, address newDispatcher); + /// Emitted when the entry fee basis points are updated. + /// @param entryFeeBasisPoints New value of the fee basis points. + event EntryFeeBasisPointsUpdated(uint256 entryFeeBasisPoints); + + /// Emitted when the exit fee basis points are updated. + /// @param exitFeeBasisPoints New value of the fee basis points. + event ExitFeeBasisPointsUpdated(uint256 exitFeeBasisPoints); + // TEST: New event. event NewEvent(); @@ -95,9 +117,10 @@ contract stBTCV2 is ERC4626Upgradeable, Ownable2StepUpgradeable { if (newTreasury == address(this)) { revert DisallowedAddress(); } - treasury = newTreasury; - emit TreasuryUpdated(newTreasury); + emit TreasuryUpdated(treasury, newTreasury); + + treasury = newTreasury; } /// @notice Updates deposit parameters. @@ -122,6 +145,23 @@ contract stBTCV2 is ERC4626Upgradeable, Ownable2StepUpgradeable { ); } + /// @notice Updates the BitcoinRedeemer contract. + /// @param newBitcoinRedeemer Address of the new BitcoinRedeemer contract. + function updateBitcoinRedeemer( + address newBitcoinRedeemer + ) external onlyOwner { + if (newBitcoinRedeemer == address(0)) { + revert ZeroAddress(); + } + + emit BitcoinRedeemerUpdated( + address(bitcoinRedeemer), + newBitcoinRedeemer + ); + + bitcoinRedeemer = BitcoinRedeemer(newBitcoinRedeemer); + } + // 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 @@ -150,6 +190,56 @@ contract stBTCV2 is ERC4626Upgradeable, Ownable2StepUpgradeable { 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( + uint256 newEntryFeeBasisPoints + ) external onlyOwner { + entryFeeBasisPoints = newEntryFeeBasisPoints; + + 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( + uint256 newExitFeeBasisPoints + ) external onlyOwner { + exitFeeBasisPoints = newExitFeeBasisPoints; + + 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, @@ -169,11 +259,15 @@ contract stBTCV2 is ERC4626Upgradeable, Ownable2StepUpgradeable { /// which determines the minimum amount for a single deposit operation. /// The amount of the assets has to be pre-approved in the tBTC /// contract. - /// The msg.sender is required to grant approval for tBTC transfer. + /// The msg.sender is required to grant approval for the transfer of a + /// certain amount of tBTC, and in addition, approval for the associated + /// fee. Specifically, the total amount to be approved (amountToApprove) + /// should be equal to the sum of the deposited amount and the fee. /// To determine the total assets amount necessary for approval /// corresponding to a given share amount, use the `previewMint` function. /// @param shares Amount of shares to mint. /// @param receiver The address to which the shares will be minted. + /// @return assets Used assets to mint shares. function mint( uint256 shares, address receiver @@ -195,9 +289,10 @@ contract stBTCV2 is ERC4626Upgradeable, Ownable2StepUpgradeable { /// deposited into the vault for the receiver through a deposit /// call. It takes into account the deposit parameter, maximum total /// assets, which determines the total amount of tBTC token held by - /// Acre protocol. - /// @dev When the remaining amount of unused limit is less than the minimum - /// deposit amount, this function returns 0. + /// Acre. This function always returns available limit for deposits, + /// but the fee is not taken into account. As a result of this, there + /// always will be some dust left. If the dust is lower than the + /// minimum deposit amount, this function will return 0. /// @return The maximum amount of tBTC token that can be deposited into /// Acre protocol for the receiver. function maxDeposit(address) public view override returns (uint256) { @@ -205,12 +300,14 @@ contract stBTCV2 is ERC4626Upgradeable, Ownable2StepUpgradeable { return type(uint256).max; } - uint256 _totalAssets = totalAssets(); + uint256 currentTotalAssets = totalAssets(); + if (currentTotalAssets >= maximumTotalAssets) return 0; - return - _totalAssets >= maximumTotalAssets - ? 0 - : maximumTotalAssets - _totalAssets; + // Max amount left for next deposits. If it is lower than the minimum + // deposit amount, return 0. + uint256 unusedLimit = maximumTotalAssets - currentTotalAssets; + + return minimumDepositAmount > unusedLimit ? 0 : unusedLimit; } /// @notice Returns the maximum amount of the vault shares that can be @@ -232,4 +329,20 @@ contract stBTCV2 is ERC4626Upgradeable, Ownable2StepUpgradeable { function depositParameters() public view returns (uint256, uint256) { return (minimumDepositAmount, maximumTotalAssets); } + + /// @return Returns entry fee basis point used in deposits. + function _entryFeeBasisPoints() internal view override returns (uint256) { + return entryFeeBasisPoints; + } + + /// @return Returns exit fee basis point used in withdrawals. + function _exitFeeBasisPoints() internal view override returns (uint256) { + return exitFeeBasisPoints; + } + + /// @notice Returns the address of the treasury wallet, where fees should be + /// transferred to. + function _feeRecipient() internal view override returns (address) { + return treasury; + } } From 4ae19312a5c38d155992d21afbcd5a383523469b Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 4 Apr 2024 12:17:23 +0200 Subject: [PATCH 049/123] Ignore slither missing inheritance in stBTC Slither is reporting: ``` stBTC (contracts/stBTC.sol#24-365) should inherit from ITBTCToken (contracts/bridge/ITBTCToken.sol#7-25) Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#missing-inheritance ``` The stBTC contract shouldn't inherit from ITBTCToken contract. --- core/contracts/stBTC.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 73c1d9760..60ae2e7c3 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -21,6 +21,7 @@ import "./lib/ERC4626Fees.sol"; /// of yield-bearing vaults. This contract facilitates the minting and /// burning of shares (stBTC), which are represented as standard ERC20 /// tokens, providing a seamless exchange with tBTC tokens. +// slither-disable-next-line missing-inheritance contract stBTC is ERC4626Fees, Ownable2StepUpgradeable { using SafeERC20 for IERC20; From 8a34d3bcdd7fb47b3e819c8232b7adbe7702ea38 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 4 Apr 2024 14:04:59 +0200 Subject: [PATCH 050/123] Remove BitcoinRedeemer reference from stBTC We don't need the reference as it's not used in redemption process. --- core/contracts/stBTC.sol | 29 --------- core/contracts/test/upgrades/stBTCV2.sol | 29 --------- .../13_stbtc_update_bitcoin_redeemer.ts | 21 ------- core/test/stBTC.test.ts | 59 +------------------ 4 files changed, 2 insertions(+), 136 deletions(-) delete mode 100644 core/deploy/13_stbtc_update_bitcoin_redeemer.ts diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 493a2a954..a6270e553 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -6,7 +6,6 @@ import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol"; -import "./BitcoinRedeemer.sol"; import "./Dispatcher.sol"; import "./lib/ERC4626Fees.sol"; @@ -28,9 +27,6 @@ contract stBTC is ERC4626Fees, Ownable2StepUpgradeable { /// Dispatcher contract that routes tBTC from stBTC to a given vault and back. Dispatcher public dispatcher; - /// BitcoinRedeemer contract. - BitcoinRedeemer public bitcoinRedeemer; - /// Address of the treasury wallet, where fees should be transferred to. address public treasury; @@ -56,14 +52,6 @@ contract stBTC is ERC4626Fees, Ownable2StepUpgradeable { /// @param minimumDepositAmount New value of the minimum deposit amount. event MinimumDepositAmountUpdated(uint256 minimumDepositAmount); - /// Emitted when the BitcoinRedeemer contract is updated. - /// @param oldBitcoinRedeemer Address of the old BitcoinRedeemer contract. - /// @param newBitcoinRedeemer Address of the new BitcoinRedeemer contract. - event BitcoinRedeemerUpdated( - address oldBitcoinRedeemer, - address newBitcoinRedeemer - ); - /// Emitted when the dispatcher contract is updated. /// @param oldDispatcher Address of the old dispatcher contract. /// @param newDispatcher Address of the new dispatcher contract. @@ -138,23 +126,6 @@ contract stBTC is ERC4626Fees, Ownable2StepUpgradeable { emit MinimumDepositAmountUpdated(newMinimumDepositAmount); } - /// @notice Updates the BitcoinRedeemer contract. - /// @param newBitcoinRedeemer Address of the new BitcoinRedeemer contract. - function updateBitcoinRedeemer( - address newBitcoinRedeemer - ) external onlyOwner { - if (newBitcoinRedeemer == address(0)) { - revert ZeroAddress(); - } - - emit BitcoinRedeemerUpdated( - address(bitcoinRedeemer), - newBitcoinRedeemer - ); - - bitcoinRedeemer = BitcoinRedeemer(newBitcoinRedeemer); - } - // 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 diff --git a/core/contracts/test/upgrades/stBTCV2.sol b/core/contracts/test/upgrades/stBTCV2.sol index bbfbfb632..215a541d9 100644 --- a/core/contracts/test/upgrades/stBTCV2.sol +++ b/core/contracts/test/upgrades/stBTCV2.sol @@ -6,7 +6,6 @@ import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol"; -import "../../BitcoinRedeemer.sol"; import "../../Dispatcher.sol"; import "../../lib/ERC4626Fees.sol"; @@ -19,9 +18,6 @@ contract stBTCV2 is ERC4626Fees, Ownable2StepUpgradeable { /// Dispatcher contract that routes tBTC from stBTC to a given vault and back. Dispatcher public dispatcher; - /// BitcoinRedeemer contract. - BitcoinRedeemer public bitcoinRedeemer; - /// Address of the treasury wallet, where fees should be transferred to. address public treasury; @@ -50,14 +46,6 @@ contract stBTCV2 is ERC4626Fees, Ownable2StepUpgradeable { /// @param minimumDepositAmount New value of the minimum deposit amount. event DepositParametersUpdated(uint256 minimumDepositAmount); - /// Emitted when the BitcoinRedeemer contract is updated. - /// @param oldBitcoinRedeemer Address of the old BitcoinRedeemer contract. - /// @param newBitcoinRedeemer Address of the new BitcoinRedeemer contract. - event BitcoinRedeemerUpdated( - address oldBitcoinRedeemer, - address newBitcoinRedeemer - ); - /// Emitted when the dispatcher contract is updated. /// @param oldDispatcher Address of the old dispatcher contract. /// @param newDispatcher Address of the new dispatcher contract. @@ -128,23 +116,6 @@ contract stBTCV2 is ERC4626Fees, Ownable2StepUpgradeable { emit DepositParametersUpdated(_minimumDepositAmount); } - /// @notice Updates the BitcoinRedeemer contract. - /// @param newBitcoinRedeemer Address of the new BitcoinRedeemer contract. - function updateBitcoinRedeemer( - address newBitcoinRedeemer - ) external onlyOwner { - if (newBitcoinRedeemer == address(0)) { - revert ZeroAddress(); - } - - emit BitcoinRedeemerUpdated( - address(bitcoinRedeemer), - newBitcoinRedeemer - ); - - bitcoinRedeemer = BitcoinRedeemer(newBitcoinRedeemer); - } - // 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 diff --git a/core/deploy/13_stbtc_update_bitcoin_redeemer.ts b/core/deploy/13_stbtc_update_bitcoin_redeemer.ts deleted file mode 100644 index d7a2de43a..000000000 --- a/core/deploy/13_stbtc_update_bitcoin_redeemer.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { HardhatRuntimeEnvironment } from "hardhat/types" -import type { DeployFunction } from "hardhat-deploy/types" - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { getNamedAccounts, deployments } = hre - const { deployer } = await getNamedAccounts() - - const bitcoinRedeemer = await deployments.get("BitcoinRedeemer") - - await deployments.execute( - "stBTC", - { from: deployer, log: true, waitConfirmations: 1 }, - "updateBitcoinRedeemer", - bitcoinRedeemer.address, - ) -} - -export default func - -func.tags = ["AcreUpdateBitcoinRedeemer"] -func.dependencies = ["stBTC", "BitcoinRedeemer"] diff --git a/core/test/stBTC.test.ts b/core/test/stBTC.test.ts index 842a3fe9a..26d4fb42b 100644 --- a/core/test/stBTC.test.ts +++ b/core/test/stBTC.test.ts @@ -12,17 +12,12 @@ import { beforeAfterSnapshotWrapper, deployment } from "./helpers" import { to1e18 } from "./utils" -import type { - StBTC as stBTC, - TestERC20, - Dispatcher, - BitcoinRedeemer, -} from "../typechain" +import type { StBTC as stBTC, TestERC20, Dispatcher } from "../typechain" const { getNamedSigners, getUnnamedSigners } = helpers.signers async function fixture() { - const { tbtc, stbtc, dispatcher, bitcoinRedeemer } = await deployment() + const { tbtc, stbtc, dispatcher } = await deployment() const { governance, treasury } = await getNamedSigners() const [depositor1, depositor2, thirdParty] = await getUnnamedSigners() @@ -34,7 +29,6 @@ async function fixture() { return { stbtc, tbtc, - bitcoinRedeemer, depositor1, depositor2, dispatcher, @@ -52,7 +46,6 @@ describe("stBTC", () => { let stbtc: stBTC let tbtc: TestERC20 let dispatcher: Dispatcher - let bitcoinRedeemer: BitcoinRedeemer let governance: HardhatEthersSigner let depositor1: HardhatEthersSigner @@ -64,7 +57,6 @@ describe("stBTC", () => { ;({ stbtc, tbtc, - bitcoinRedeemer, depositor1, depositor2, dispatcher, @@ -1136,53 +1128,6 @@ describe("stBTC", () => { }) }) - describe("updateBitcoinRedeemer", () => { - beforeAfterSnapshotWrapper() - - context("when caller is not governance", () => { - it("should revert", async () => { - await expect( - stbtc.connect(thirdParty).updateBitcoinRedeemer(ZeroAddress), - ) - .to.be.revertedWithCustomError(stbtc, "OwnableUnauthorizedAccount") - .withArgs(thirdParty.address) - }) - }) - - context("when caller is governance", () => { - context("when a new BitcoinRedeemer is zero address", () => { - it("should revert", async () => { - await expect( - stbtc.connect(governance).updateBitcoinRedeemer(ZeroAddress), - ).to.be.revertedWithCustomError(stbtc, "ZeroAddress") - }) - }) - - context("when a new BitcoinRedeemer is an allowed address", () => { - let newBitcoinRedeemer: string - let tx: ContractTransactionResponse - - before(async () => { - newBitcoinRedeemer = await ethers.Wallet.createRandom().getAddress() - - tx = await stbtc - .connect(governance) - .updateBitcoinRedeemer(newBitcoinRedeemer) - }) - - it("should update the treasury", async () => { - expect(await stbtc.bitcoinRedeemer()).to.be.equal(newBitcoinRedeemer) - }) - - it("should emit BitcoinRedeemerUpdated event", async () => { - await expect(tx) - .to.emit(stbtc, "BitcoinRedeemerUpdated") - .withArgs(await bitcoinRedeemer.getAddress(), newBitcoinRedeemer) - }) - }) - }) - }) - describe("updateTreasury", () => { beforeAfterSnapshotWrapper() From 1e580d6e236659d25a5085c46d0e6ff7e9db7c9c Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 4 Apr 2024 14:07:48 +0200 Subject: [PATCH 051/123] Add comment about TBTC token owner --- core/contracts/BitcoinRedeemer.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/core/contracts/BitcoinRedeemer.sol b/core/contracts/BitcoinRedeemer.sol index 438f4c4e2..94828859a 100644 --- a/core/contracts/BitcoinRedeemer.sol +++ b/core/contracts/BitcoinRedeemer.sol @@ -115,6 +115,7 @@ contract BitcoinRedeemer is Initializable, IReceiveApproval { if ( !tbtcToken.approveAndCall( + // TBTC Token contract owner resolves to the TBTCVault contract. tbtcToken.owner(), tbtcAmount, tbtcRedemptionData From 6c18a4991aca501ce892ddbdb3e40a3901e323ee Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 4 Apr 2024 14:14:03 +0200 Subject: [PATCH 052/123] Rename MezoAllocator deposit -> allocate --- core/contracts/MezoAllocator.sol | 4 ++-- core/test/MezoAllocator.test.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index c4efa81f9..5800897e7 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -76,10 +76,10 @@ contract MezoAllocator is Ownable2Step { tbtc = _tbtc; } - /// @notice Deposits tBTC to MezoPortal. + /// @notice Allocate tBTC to MezoPortal. /// @dev This function can be invoked periodically by a bot. /// @param amount Amount of tBTC to deposit to Mezo Portal. - function deposit(uint96 amount) external onlyMaintainerAndOwner { + function allocate(uint96 amount) external onlyMaintainerAndOwner { // slither-disable-next-line arbitrary-send-erc20 IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), amount); IERC20(tbtc).forceApprove(mezoPortal, amount); diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index 9dfd2b2c1..bbc5232fe 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -57,13 +57,13 @@ describe("MezoAllocator", () => { } = await loadFixture(fixture)) }) - describe("deposit", () => { + describe("allocate", () => { beforeAfterSnapshotWrapper() context("when the caller is not an owner", () => { it("should revert", async () => { await expect( - mezoAllocator.connect(thirdParty).deposit(to1e18(1)), + mezoAllocator.connect(thirdParty).allocate(to1e18(1)), ).to.be.revertedWithCustomError(mezoAllocator, "NotAuthorized") }) }) @@ -71,7 +71,7 @@ describe("MezoAllocator", () => { context("when the caller is an owner", () => { it("should not revert", async () => { await expect( - mezoAllocator.connect(governance).deposit(to1e18(1)), + mezoAllocator.connect(governance).allocate(to1e18(1)), ).to.not.be.revertedWithCustomError(mezoAllocator, "NotAuthorized") }) }) @@ -86,7 +86,7 @@ describe("MezoAllocator", () => { .connect(governance) .updateMaintainer(maintainer.address) - tx = await mezoAllocator.connect(maintainer).deposit(to1e18(1)) + tx = await mezoAllocator.connect(maintainer).allocate(to1e18(1)) }) it("should deposit and transfer tBTC to Mezo Portal", async () => { @@ -144,7 +144,7 @@ describe("MezoAllocator", () => { .connect(governance) .updateMaintainer(maintainer.address) - await mezoAllocator.connect(maintainer).deposit(to1e18(5)) + await mezoAllocator.connect(maintainer).allocate(to1e18(5)) }) it("should increment the deposits array", async () => { From 2b17080e99be329570ce1a4e063cec393b7fe52c Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 4 Apr 2024 14:55:09 +0200 Subject: [PATCH 053/123] Allocate tBTC to MezoPortal as a "rolling" deposit Here we change the deposit approach from tracking all the Mezo Deposits to having only a single deposit that accumulates all deposited tBTC by Acre into a single deposit. An "old" deposit is abondoned and balance is zeroed. A "new" deposit is created with a new id and a new balance is equal to the old balance + new requested amount. --- core/contracts/MezoAllocator.sol | 54 +++++++++++++++++++------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 5800897e7..463bddf76 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -17,9 +17,10 @@ interface IMezoPortal { contract MezoAllocator is Ownable2Step { using SafeERC20 for IERC20; - /// @notice DepositInfo keeps track of the deposit balance, creation time, - /// and unlock time. + /// @notice DepositInfo keeps track of the deposit Id, deposit balance, + /// creation time, and unlock time. struct DepositInfo { + uint256 id; uint96 balance; uint32 createdAt; uint32 unlockAt; @@ -35,10 +36,8 @@ contract MezoAllocator is Ownable2Step { /// @notice Maintainer address which can trigger deposit flow. address public maintainer; - // Deposit ID -> Deposit Info - mapping(uint256 => DepositInfo) public depositsById; - // Deposit IDs - uint256[] public deposits; + /// @notice keeps track of the deposit info. + DepositInfo public depositInfo; /// Emitted when tBTC is deposited to MezoPortal. event DepositAllocated(uint256 depositId, uint256 amount); @@ -76,33 +75,49 @@ contract MezoAllocator is Ownable2Step { tbtc = _tbtc; } - /// @notice Allocate tBTC to MezoPortal. + /// @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 used. /// @dev This function can be invoked periodically by a bot. /// @param amount Amount of tBTC to deposit to Mezo Portal. function allocate(uint96 amount) external onlyMaintainerAndOwner { + // Free all Acre's tBTC from MezoPortal before creating a new deposit. + free(); // slither-disable-next-line arbitrary-send-erc20 IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), amount); - IERC20(tbtc).forceApprove(mezoPortal, amount); + + // Add freed tBTC from the previous deposit and add the new amount. + depositInfo.balance += amount; + + IERC20(tbtc).forceApprove(mezoPortal, depositInfo.balance); // 0 denotes no lock period for this deposit. The zero lock time is // hardcoded as of biz decision. - IMezoPortal(mezoPortal).deposit(address(tbtc), amount, 0); + IMezoPortal(mezoPortal).deposit(address(tbtc), depositInfo.balance, 0); // MezoPortal doesn't return depositId, so we have to read depositCounter // which assignes depositId to the current deposit. uint256 depositId = IMezoPortal(mezoPortal).depositCount(); // slither-disable-next-line reentrancy-benign - depositsById[depositId] = DepositInfo({ - balance: amount, - // solhint-disable-next-line not-rely-on-time - createdAt: uint32(block.timestamp), - // solhint-disable-next-line not-rely-on-time - unlockAt: uint32(block.timestamp) - }); - deposits.push(depositId); + depositInfo.id = depositId; + depositInfo.createdAt = uint32(block.timestamp); + depositInfo.unlockAt = uint32(block.timestamp); // slither-disable-next-line reentrancy-events emit DepositAllocated(depositId, amount); } + /// @notice Withdraw all Acre's tBTC from MezoPortal. + function free() private { + if (depositInfo.balance > 0) { + IMezoPortal(mezoPortal).withdraw( + address(tbtc), + depositInfo.id, + depositInfo.balance + ); + } + } + /// @notice Updates the tBTC storage address. /// @dev At first this is going to be the stBTC contract. Once Acre /// works with more destinations for tBTC, this will be updated to @@ -139,9 +154,4 @@ contract MezoAllocator is Ownable2Step { // TODO: update depositsById and deposits data structures. // IERC20(tbtc).safeTransfer(address(tbtcStorage), amount); } - - /// @notice Returns the deposit IDs. - function getDeposits() external view returns (uint256[] memory) { - return deposits; - } } From 37540fb85c1a001d8c3d8fac775cd5383d45a400 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 4 Apr 2024 15:32:06 +0200 Subject: [PATCH 054/123] Cleanup and slithering --- core/contracts/MezoAllocator.sol | 49 +++++++++++++++++++------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 463bddf76..27bceb631 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -40,7 +40,11 @@ contract MezoAllocator is Ownable2Step { DepositInfo public depositInfo; /// Emitted when tBTC is deposited to MezoPortal. - event DepositAllocated(uint256 depositId, uint256 amount); + event DepositAllocated( + uint256 indexed oldDepositId, + uint256 indexed newDepositId, + uint256 amount + ); /// @notice Emitted when the tBTC storage address is updated. event TbtcStorageUpdated(address indexed tbtcStorage); @@ -87,35 +91,28 @@ contract MezoAllocator is Ownable2Step { free(); // slither-disable-next-line arbitrary-send-erc20 IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), amount); - + // Add freed tBTC from the previous deposit and add the new amount. - depositInfo.balance += amount; - - IERC20(tbtc).forceApprove(mezoPortal, depositInfo.balance); + uint96 newBalance = depositInfo.balance + amount; + + IERC20(tbtc).forceApprove(mezoPortal, newBalance); // 0 denotes no lock period for this deposit. The zero lock time is // hardcoded as of biz decision. - IMezoPortal(mezoPortal).deposit(address(tbtc), depositInfo.balance, 0); + IMezoPortal(mezoPortal).deposit(address(tbtc), newBalance, 0); // MezoPortal doesn't return depositId, so we have to read depositCounter // which assignes depositId to the current deposit. - uint256 depositId = IMezoPortal(mezoPortal).depositCount(); + uint256 newDepositId = IMezoPortal(mezoPortal).depositCount(); // slither-disable-next-line reentrancy-benign - depositInfo.id = depositId; + uint256 oldDepositId = depositInfo.id; + depositInfo.id = newDepositId; + depositInfo.balance = newBalance; + // solhint-disable-next-line not-rely-on-time depositInfo.createdAt = uint32(block.timestamp); + // solhint-disable-next-line not-rely-on-time depositInfo.unlockAt = uint32(block.timestamp); // slither-disable-next-line reentrancy-events - emit DepositAllocated(depositId, amount); - } - - /// @notice Withdraw all Acre's tBTC from MezoPortal. - function free() private { - if (depositInfo.balance > 0) { - IMezoPortal(mezoPortal).withdraw( - address(tbtc), - depositInfo.id, - depositInfo.balance - ); - } + emit DepositAllocated(oldDepositId, newDepositId, amount); } /// @notice Updates the tBTC storage address. @@ -154,4 +151,16 @@ contract MezoAllocator is Ownable2Step { // TODO: update depositsById and deposits data structures. // IERC20(tbtc).safeTransfer(address(tbtcStorage), amount); } + + /// @notice Withdraw all Acre's tBTC from MezoPortal. + function free() private { + if (depositInfo.balance > 0) { + // slither-disable-next-line reentrancy-no-eth + IMezoPortal(mezoPortal).withdraw( + address(tbtc), + depositInfo.id, + depositInfo.balance + ); + } + } } From dde4c23ec84ad9f723b79e3db2495f8f1a7d6fa7 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 4 Apr 2024 15:54:05 +0200 Subject: [PATCH 055/123] Fixing tests for rolling deposits --- core/contracts/test/MezoPortalStub.sol | 4 +++- core/test/MezoAllocator.test.ts | 29 +++++++++++--------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/core/contracts/test/MezoPortalStub.sol b/core/contracts/test/MezoPortalStub.sol index 496f05394..bdeaae0ee 100644 --- a/core/contracts/test/MezoPortalStub.sol +++ b/core/contracts/test/MezoPortalStub.sol @@ -13,7 +13,9 @@ contract MezoPortalStub { address token, uint256 depositId, uint96 amount - ) external {} + ) external { + IERC20(token).safeTransfer(msg.sender, amount); + } function deposit(address token, uint96 amount, uint32 lockPeriod) external { depositCount++; diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index bbc5232fe..7fb25c48a 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -18,13 +18,11 @@ import { to1e18 } from "./utils" const { getNamedSigners, getUnnamedSigners } = helpers.signers async function fixture() { - const { tbtc, stbtc, dispatcher, mezoAllocator, mezoPortal } = - await deployment() + const { tbtc, stbtc, mezoAllocator, mezoPortal } = await deployment() const { governance, maintainer } = await getNamedSigners() const [thirdParty] = await getUnnamedSigners() return { - dispatcher, governance, thirdParty, maintainer, @@ -98,17 +96,13 @@ describe("MezoAllocator", () => { ) }) - it("should populate deposits array", async () => { - expect(await mezoAllocator.deposits(0)).to.equal(1) - }) - it("should set deposit balance", async () => { - const deposit = await mezoAllocator.depositsById(1) + const deposit = await mezoAllocator.depositInfo() expect(deposit.balance).to.equal(to1e18(1)) }) it("should set creation timestamp", async () => { - const deposit = await mezoAllocator.depositsById(1) + const deposit = await mezoAllocator.depositInfo() const dateTime = new Date() // Check if the block timestamp is within 60 seconds of the current // test time @@ -119,7 +113,7 @@ describe("MezoAllocator", () => { }) it("should set unlocking timestamp", async () => { - const deposit = await mezoAllocator.depositsById(1) + const deposit = await mezoAllocator.depositInfo() const dateTime = new Date() // Check if the block timestamp is within 60 seconds of the current // test time @@ -130,10 +124,9 @@ describe("MezoAllocator", () => { }) it("should emit Deposit event", async () => { - const latestDepositId = await mezoAllocator.deposits(0) await expect(tx) .to.emit(mezoAllocator, "DepositAllocated") - .withArgs(latestDepositId, to1e18(1)) + .withArgs(0, 1, to1e18(1)) }) }) @@ -147,13 +140,15 @@ describe("MezoAllocator", () => { await mezoAllocator.connect(maintainer).allocate(to1e18(5)) }) - it("should increment the deposits array", async () => { - expect(await mezoAllocator.deposits(1)).to.equal(2) + it("should increment the deposit id", async () => { + const depositInfo = await mezoAllocator.depositInfo() + expect(depositInfo.id).to.equal(2) }) - it("should populate deposits mapping", async () => { - const deposit = await mezoAllocator.depositsById(2) - expect(deposit.balance).to.equal(to1e18(5)) + it("should populate deposit balance", async () => { + const deposit = await mezoAllocator.depositInfo() + // 1 + 5 = 6 + expect(deposit.balance).to.equal(to1e18(6)) }) }) }) From 3b372b00bcc7b6a1f79a94af660981586a00b4c2 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 4 Apr 2024 20:37:29 +0200 Subject: [PATCH 056/123] Use msg.sender instead of _msgSender() --- core/contracts/stBTC.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index e3e95383d..857ff19a4 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -191,7 +191,7 @@ contract stBTC is ERC4626Fees, PausableOwnable { ) external returns (bool) { if (approve(spender, value)) { IReceiveApproval(spender).receiveApproval( - _msgSender(), + msg.sender, value, address(this), extraData From 597b0f8970f5941ab8cb5bffad432678853f97c0 Mon Sep 17 00:00:00 2001 From: ioay Date: Thu, 4 Apr 2024 22:45:38 +0200 Subject: [PATCH 057/123] Type update for encjodeJSON function --- dapp/src/store/devTools.ts | 4 ++-- dapp/src/utils/json.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/dapp/src/store/devTools.ts b/dapp/src/store/devTools.ts index 220b34182..7495ca110 100644 --- a/dapp/src/store/devTools.ts +++ b/dapp/src/store/devTools.ts @@ -1,6 +1,6 @@ import { encodeJSON } from "#/utils" -function devToolsSanitizer(input: T) { +function devToolsSanitizer(input: unknown): unknown { switch (typeof input) { // We can make use of encodeJSON instead of recursively looping through // the input @@ -8,7 +8,7 @@ function devToolsSanitizer(input: T) { case "object": // We only need to sanitize bigints and objects // that may or may not contain them. - return JSON.parse(encodeJSON(input)) as T + return JSON.parse(encodeJSON(input)) default: return input } diff --git a/dapp/src/utils/json.ts b/dapp/src/utils/json.ts index 3ceb7a3c2..4214e25db 100644 --- a/dapp/src/utils/json.ts +++ b/dapp/src/utils/json.ts @@ -3,12 +3,11 @@ * * @param input an object, array, or primitive to encode as JSON */ -export function encodeJSON(input: unknown): string { - return JSON.stringify(input, (_, value): object | null => { +export function encodeJSON(input: unknown) { + return JSON.stringify(input, (_, value: unknown) => { if (typeof value === "bigint") { return { B_I_G_I_N_T: value.toString() } } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return value }) } From d71a13b9e2cef5a8c053713954580f23a41e0e9e Mon Sep 17 00:00:00 2001 From: ioay Date: Thu, 4 Apr 2024 23:11:56 +0200 Subject: [PATCH 058/123] Passing shares balance to liquid popover --- dapp/src/components/LiquidStakingTokenPopover.tsx | 5 +++-- dapp/src/hooks/store/index.ts | 1 + dapp/src/hooks/store/useSharesBalance.ts | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 dapp/src/hooks/store/useSharesBalance.ts 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/hooks/store/index.ts b/dapp/src/hooks/store/index.ts index 379d10f6b..ac22e8a41 100644 --- a/dapp/src/hooks/store/index.ts +++ b/dapp/src/hooks/store/index.ts @@ -1,3 +1,4 @@ export * from "./useAppDispatch" export * from "./useAppSelector" export * from "./useEstimatedBTCBalance" +export * from "./useSharesBalance" 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) +} From 1a24f0009d4196340ce748bd61acefb44f210d09 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 5 Apr 2024 11:09:02 +0200 Subject: [PATCH 059/123] Make MezoAllocator upgradable Make the MezoAllocator contract upgradable and deploy it as transparent proxy with Open Zeppelin upgrades plugin. --- core/contracts/MezoAllocator.sol | 19 ++++++++++++++----- core/deploy/02_deploy_mezo_allocator.ts | 24 +++++++++++++++--------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 27bceb631..addc3953b 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -3,7 +3,7 @@ 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/access/Ownable2Step.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; interface IMezoPortal { function deposit(address token, uint96 amount, uint32 lockPeriod) external; @@ -14,7 +14,7 @@ interface IMezoPortal { } /// @notice MezoAllocator routes tBTC to/from MezoPortal. -contract MezoAllocator is Ownable2Step { +contract MezoAllocator is Ownable2StepUpgradeable { using SafeERC20 for IERC20; /// @notice DepositInfo keeps track of the deposit Id, deposit balance, @@ -27,9 +27,9 @@ contract MezoAllocator is Ownable2Step { } /// Address of the MezoPortal contract. - address public immutable mezoPortal; + address public mezoPortal; /// tBTC token contract. - IERC20 public immutable tbtc; + IERC20 public tbtc; /// Contract holding tBTC deposited by stakers. address public tbtcStorage; @@ -65,16 +65,25 @@ contract MezoAllocator is Ownable2Step { _; } + /// @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. - constructor(address _mezoPortal, IERC20 _tbtc) Ownable(msg.sender) { + function initialize(address _mezoPortal, IERC20 _tbtc) public initializer { + __Ownable2Step_init(); + __Ownable_init(msg.sender); + if (_mezoPortal == address(0)) { revert ZeroAddress(); } if (address(_tbtc) == address(0)) { revert ZeroAddress(); } + mezoPortal = _mezoPortal; tbtc = _tbtc; } diff --git a/core/deploy/02_deploy_mezo_allocator.ts b/core/deploy/02_deploy_mezo_allocator.ts index 33cee9344..dd40acbe3 100644 --- a/core/deploy/02_deploy_mezo_allocator.ts +++ b/core/deploy/02_deploy_mezo_allocator.ts @@ -1,23 +1,29 @@ import type { HardhatRuntimeEnvironment } from "hardhat/types" import type { DeployFunction } from "hardhat-deploy/types" -import { waitConfirmationsNumber } from "../helpers/deployment" +import { waitForTransaction } from "../helpers/deployment" const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, deployments, helpers } = hre - const { deployer } = await getNamedAccounts() + const { governance } = await getNamedAccounts() + const { deployer } = await helpers.signers.getNamedSigners() const tbtc = await deployments.get("TBTC") const mezoPortal = await deployments.get("MezoPortal") - const mezoAllocator = await deployments.deploy("MezoAllocator", { - from: deployer, - args: [mezoPortal.address, tbtc.address], - log: true, - waitConfirmations: waitConfirmationsNumber(hre), + const [, deployment] = await helpers.upgrades.deployProxy("MezoAllocator", { + factoryOpts: { + signer: deployer, + }, + initializerArgs: [mezoPortal.address, tbtc.address], + proxyOpts: { + kind: "transparent", + initialOwner: governance, + }, }) - if (hre.network.tags.etherscan) { - await helpers.etherscan.verify(mezoAllocator) + if (deployment.transactionHash && hre.network.tags.etherscan) { + await waitForTransaction(hre, deployment.transactionHash) + await helpers.etherscan.verify(deployment) } // TODO: Add Tenderly verification From ac1e4498c38b1306f7856c8d839303efec05f50b Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 5 Apr 2024 12:29:58 +0200 Subject: [PATCH 060/123] Ignoring solhint errors in contracts/test/ --- core/.solhintignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/.solhintignore b/core/.solhintignore index a908c5b25..e2af04a45 100644 --- a/core/.solhintignore +++ b/core/.solhintignore @@ -1,2 +1,2 @@ node_modules/ -MezoPortalStub.sol +contracts/test/ \ No newline at end of file From f2d8ca76f9c875a408fb8b7ebab93516088cc762 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 5 Apr 2024 12:31:47 +0200 Subject: [PATCH 061/123] Importing ZeroAddress error from utils --- core/contracts/MezoAllocator.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 27bceb631..a5a0dbc4f 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -4,6 +4,7 @@ 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/access/Ownable2Step.sol"; +import {ZeroAddress} from "./utils/Errors.sol"; interface IMezoPortal { function deposit(address token, uint96 amount, uint32 lockPeriod) external; @@ -55,9 +56,6 @@ contract MezoAllocator is Ownable2Step { /// @notice Reverts if the caller is not an authorized account. error NotAuthorized(); - /// @notice Reverts if the address is 0. - error ZeroAddress(); - modifier onlyMaintainerAndOwner() { if (msg.sender != maintainer && owner() != msg.sender) { revert NotAuthorized(); From d99c98f30137a33a77fe6639891d156a245290d0 Mon Sep 17 00:00:00 2001 From: ioay Date: Fri, 5 Apr 2024 09:29:00 +0200 Subject: [PATCH 062/123] Dapp build fix --- .../src/hooks/store/useEstimatedBtcBalance.ts | 6 ---- dapp/src/hooks/useFetchBtcBalance.ts | 28 ------------------- dapp/src/store/devTools.ts | 5 ++-- 3 files changed, 3 insertions(+), 36 deletions(-) delete mode 100644 dapp/src/hooks/store/useEstimatedBtcBalance.ts delete mode 100644 dapp/src/hooks/useFetchBtcBalance.ts diff --git a/dapp/src/hooks/store/useEstimatedBtcBalance.ts b/dapp/src/hooks/store/useEstimatedBtcBalance.ts deleted file mode 100644 index b8ce0bb67..000000000 --- a/dapp/src/hooks/store/useEstimatedBtcBalance.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { selectEstimatedBtcBalance } from "#/store/btc" -import { useAppSelector } from "./useAppSelector" - -export function useEstimatedBTCBalance() { - return useAppSelector(selectEstimatedBtcBalance) -} diff --git a/dapp/src/hooks/useFetchBtcBalance.ts b/dapp/src/hooks/useFetchBtcBalance.ts deleted file mode 100644 index 360135312..000000000 --- a/dapp/src/hooks/useFetchBtcBalance.ts +++ /dev/null @@ -1,28 +0,0 @@ -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/store/devTools.ts b/dapp/src/store/devTools.ts index 7495ca110..1edd8dee7 100644 --- a/dapp/src/store/devTools.ts +++ b/dapp/src/store/devTools.ts @@ -1,3 +1,4 @@ +import { DevToolsEnhancerOptions } from "@reduxjs/toolkit" import { encodeJSON } from "#/utils" function devToolsSanitizer(input: unknown): unknown { @@ -15,8 +16,8 @@ function devToolsSanitizer(input: unknown): unknown { } export const devTools = !import.meta.env.PROD - ? { + ? ({ actionSanitizer: devToolsSanitizer, stateSanitizer: devToolsSanitizer, - } + } as DevToolsEnhancerOptions) : false From 17995d972252335be1672e1099f500e4de14dc8b Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 5 Apr 2024 11:42:00 +0200 Subject: [PATCH 063/123] Add tests for MezoAllocator upgrades These are tests we use for contracts upgradeability testing. --- .../test/upgrades/MezoAllocatorV2.sol | 182 ++++++++++++++++++ core/test/MezoAllocator.upgrade.test.ts | 91 +++++++++ 2 files changed, 273 insertions(+) create mode 100644 core/contracts/test/upgrades/MezoAllocatorV2.sol create mode 100644 core/test/MezoAllocator.upgrade.test.ts diff --git a/core/contracts/test/upgrades/MezoAllocatorV2.sol b/core/contracts/test/upgrades/MezoAllocatorV2.sol new file mode 100644 index 000000000..e5e862e07 --- /dev/null +++ b/core/contracts/test/upgrades/MezoAllocatorV2.sol @@ -0,0 +1,182 @@ +// 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"; + +interface IMezoPortal { + function deposit(address token, uint96 amount, uint32 lockPeriod) external; + + function withdraw(address token, uint256 depositId, uint96 amount) external; + + function depositCount() external view returns (uint256); +} + +/// @dev This is a contract used to test stBTC upgradeability. It is a copy of +/// stBTC contract with some differences marked with `TEST:` comments. +contract MezoAllocatorV2 is Ownable2StepUpgradeable { + using SafeERC20 for IERC20; + + /// @notice DepositInfo keeps track of the deposit Id, deposit balance, + /// creation time, and unlock time. + struct DepositInfo { + uint256 id; + uint96 balance; + uint32 createdAt; + uint32 unlockAt; + } + + /// Address of the MezoPortal contract. + address public mezoPortal; + /// tBTC token contract. + IERC20 public tbtc; + /// Contract holding tBTC deposited by stakers. + address public tbtcStorage; + + /// @notice Maintainer address which can trigger deposit flow. + address public maintainer; + + /// @notice keeps track of the deposit info. + DepositInfo public depositInfo; + + // TEST: New variable. + uint256 public newVariable; + + /// Emitted when tBTC is deposited to MezoPortal. + event DepositAllocated( + uint256 indexed oldDepositId, + uint256 indexed newDepositId, + uint256 amount + ); + + /// @notice Emitted when the tBTC storage address is updated. + event TbtcStorageUpdated(address indexed tbtcStorage); + + /// @notice Emitted when the maintainer address is updated. + event MaintainerUpdated(address indexed maintainer); + + // TEST: New event. + event NewEvent(); + + /// @notice Reverts if the caller is not an authorized account. + error NotAuthorized(); + + /// @notice Reverts if the address is 0. + error ZeroAddress(); + + modifier onlyMaintainerAndOwner() { + if (msg.sender != maintainer && owner() != msg.sender) { + revert NotAuthorized(); + } + _; + } + + /// @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, IERC20 _tbtc) 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 used. + /// @dev This function can be invoked periodically by a bot. + /// @param amount Amount of tBTC to deposit to Mezo Portal. + function allocate(uint96 amount) external onlyMaintainerAndOwner { + // Free all Acre's tBTC from MezoPortal before creating a new deposit. + free(); + // slither-disable-next-line arbitrary-send-erc20 + IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), amount); + + // Add freed tBTC from the previous deposit and add the new amount. + uint96 newBalance = depositInfo.balance + amount; + + IERC20(tbtc).forceApprove(mezoPortal, newBalance); + // 0 denotes no lock period for this deposit. The zero lock time is + // hardcoded as of biz decision. + IMezoPortal(mezoPortal).deposit(address(tbtc), newBalance, 0); + // MezoPortal doesn't return depositId, so we have to read depositCounter + // which assignes depositId to the current deposit. + uint256 newDepositId = IMezoPortal(mezoPortal).depositCount(); + // slither-disable-next-line reentrancy-benign + uint256 oldDepositId = depositInfo.id; + depositInfo.id = newDepositId; + depositInfo.balance = newBalance; + // solhint-disable-next-line not-rely-on-time + depositInfo.createdAt = uint32(block.timestamp); + // solhint-disable-next-line not-rely-on-time + depositInfo.unlockAt = uint32(block.timestamp); + + // slither-disable-next-line reentrancy-events + emit DepositAllocated(oldDepositId, newDepositId, amount); + } + + /// @notice Updates the tBTC storage address. + /// @dev At first this is going to be the stBTC contract. Once Acre + /// works with more destinations for tBTC, this will be updated to + /// the new storage contract like AcreDispatcher. + /// @param _tbtcStorage Address of the new tBTC storage. + // TEST: Modified function. + function updateTbtcStorage(address _tbtcStorage) external onlyOwner { + if (_tbtcStorage == address(0)) { + revert ZeroAddress(); + } + tbtcStorage = _tbtcStorage; + + emit TbtcStorageUpdated(_tbtcStorage); + + // TEST: Emit new event. + emit NewEvent(); + } + + /// @notice Updates the maintainer address. + /// @param _maintainer Address of the new maintainer. + function updateMaintainer(address _maintainer) external onlyOwner { + if (_maintainer == address(0)) { + revert ZeroAddress(); + } + maintainer = _maintainer; + + emit MaintainerUpdated(_maintainer); + } + + // TODO: add updatable withdrawer and onlyWithdrawer modifier (stBTC or AcreDispatcher). + /// @notice Withdraws tBTC from MezoPortal and transfers it to stBTC. + function withdraw(uint96 amount) external { + // TODO: Take the last deposit and pull the funds from it (FIFO). + // If not enough funds, take everything from that deposit and + // take the rest from the next deposit id until the amount is + // reached. Delete deposit ids that are empty. + // IMezoPortal(mezoPortal).withdraw(address(tbtc), depositId, amount); + // TODO: update depositsById and deposits data structures. + // IERC20(tbtc).safeTransfer(address(tbtcStorage), amount); + } + + /// @notice Withdraw all Acre's tBTC from MezoPortal. + function free() private { + if (depositInfo.balance > 0) { + // slither-disable-next-line reentrancy-no-eth + IMezoPortal(mezoPortal).withdraw( + address(tbtc), + depositInfo.id, + depositInfo.balance + ); + } + } +} diff --git a/core/test/MezoAllocator.upgrade.test.ts b/core/test/MezoAllocator.upgrade.test.ts new file mode 100644 index 000000000..4c891e127 --- /dev/null +++ b/core/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.tbtcStorage()).to.eq(await stbtc.getAddress()) + }) + }) + + describe("upgraded `updateTbtcStorage` function", () => { + let tx: ContractTransactionResponse + + before(async () => { + const newAddress = await ethers.Wallet.createRandom().getAddress() + + tx = await allocatorV2.connect(governance).updateTbtcStorage(newAddress) + }) + + it("should emit `NewEvent` event", async () => { + await expect(tx).to.emit(allocatorV2, "NewEvent") + }) + }) + }) +}) From 8faf5422ea2c01b2df7388dcf303ec7cdbacea98 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 5 Apr 2024 14:19:06 +0200 Subject: [PATCH 064/123] Allocating 100% of tBTC Changed the current approach of allocating 100% from deposited tBTC in stBTC. As this is MVP, a bot won't need to set an exact amount that should be allocated in Mezo Portal. Other changes include inlining a "free" function for some gas optimization, removing DepositInfo struct as tracking of depositId is enough and some other minor refactoring. --- core/contracts/MezoAllocator.sol | 90 +++++++++++++++----------------- 1 file changed, 41 insertions(+), 49 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index a5a0dbc4f..f8f307792 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -7,44 +7,45 @@ import "@openzeppelin/contracts/access/Ownable2Step.sol"; import {ZeroAddress} from "./utils/Errors.sol"; interface IMezoPortal { + struct DepositInfo { + uint96 balance; + uint32 unlockAt; + } + function deposit(address token, uint96 amount, uint32 lockPeriod) external; function withdraw(address token, uint256 depositId, uint96 amount) external; function depositCount() external view returns (uint256); + + function getDeposit( + address depositor, + address token, + uint256 depositId + ) external view returns (DepositInfo memory); } /// @notice MezoAllocator routes tBTC to/from MezoPortal. contract MezoAllocator is Ownable2Step { using SafeERC20 for IERC20; - /// @notice DepositInfo keeps track of the deposit Id, deposit balance, - /// creation time, and unlock time. - struct DepositInfo { - uint256 id; - uint96 balance; - uint32 createdAt; - uint32 unlockAt; - } - /// Address of the MezoPortal contract. - address public immutable mezoPortal; + IMezoPortal public immutable mezoPortal; /// tBTC token contract. IERC20 public immutable tbtc; /// Contract holding tBTC deposited by stakers. address public tbtcStorage; - /// @notice Maintainer address which can trigger deposit flow. address public maintainer; - - /// @notice keeps track of the deposit info. - DepositInfo public depositInfo; + /// @notice keeps track of the latest deposit ID assigned in Mezo Portal. + uint256 public depositId; /// Emitted when tBTC is deposited to MezoPortal. event DepositAllocated( uint256 indexed oldDepositId, uint256 indexed newDepositId, - uint256 amount + uint256 addedAmount, + uint256 newDepositAmount ); /// @notice Emitted when the tBTC storage address is updated. @@ -73,7 +74,7 @@ contract MezoAllocator is Ownable2Step { if (address(_tbtc) == address(0)) { revert ZeroAddress(); } - mezoPortal = _mezoPortal; + mezoPortal = IMezoPortal(_mezoPortal); tbtc = _tbtc; } @@ -81,36 +82,39 @@ contract MezoAllocator is Ownable2Step { /// 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 used. + /// is created and the previous deposit id is no longer in use. /// @dev This function can be invoked periodically by a bot. - /// @param amount Amount of tBTC to deposit to Mezo Portal. - function allocate(uint96 amount) external onlyMaintainerAndOwner { - // Free all Acre's tBTC from MezoPortal before creating a new deposit. - free(); + function allocate() external onlyMaintainerAndOwner { + uint96 depositBalance = mezoPortal + .getDeposit(address(this), address(tbtc), depositId) + .balance; + 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); + } + uint256 addedAmount = IERC20(tbtc).balanceOf(address(tbtcStorage)); // slither-disable-next-line arbitrary-send-erc20 - IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), amount); + IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), addedAmount); - // Add freed tBTC from the previous deposit and add the new amount. - uint96 newBalance = depositInfo.balance + amount; + uint96 newDepositAmount = uint96(IERC20(tbtc).balanceOf(address(this))); - IERC20(tbtc).forceApprove(mezoPortal, newBalance); + IERC20(tbtc).forceApprove(address(mezoPortal), newDepositAmount); // 0 denotes no lock period for this deposit. The zero lock time is // hardcoded as of biz decision. - IMezoPortal(mezoPortal).deposit(address(tbtc), newBalance, 0); + mezoPortal.deposit(address(tbtc), newDepositAmount, 0); + uint256 oldDepositId = depositId; // MezoPortal doesn't return depositId, so we have to read depositCounter - // which assignes depositId to the current deposit. - uint256 newDepositId = IMezoPortal(mezoPortal).depositCount(); - // slither-disable-next-line reentrancy-benign - uint256 oldDepositId = depositInfo.id; - depositInfo.id = newDepositId; - depositInfo.balance = newBalance; - // solhint-disable-next-line not-rely-on-time - depositInfo.createdAt = uint32(block.timestamp); - // solhint-disable-next-line not-rely-on-time - depositInfo.unlockAt = uint32(block.timestamp); + // which assigns depositId to the current deposit. + depositId = mezoPortal.depositCount(); // slither-disable-next-line reentrancy-events - emit DepositAllocated(oldDepositId, newDepositId, amount); + emit DepositAllocated( + oldDepositId, + depositId, + addedAmount, + newDepositAmount + ); } /// @notice Updates the tBTC storage address. @@ -149,16 +153,4 @@ contract MezoAllocator is Ownable2Step { // TODO: update depositsById and deposits data structures. // IERC20(tbtc).safeTransfer(address(tbtcStorage), amount); } - - /// @notice Withdraw all Acre's tBTC from MezoPortal. - function free() private { - if (depositInfo.balance > 0) { - // slither-disable-next-line reentrancy-no-eth - IMezoPortal(mezoPortal).withdraw( - address(tbtc), - depositInfo.id, - depositInfo.balance - ); - } - } } From 020528a31573f4a1b1554c5abb5c452f81268353 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 5 Apr 2024 14:42:50 +0200 Subject: [PATCH 065/123] Updating deployment files --- core/deploy/11_stbtc_update_dispatcher.ts | 2 +- core/deploy/13_mezo_allocator_update_storage.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/deploy/11_stbtc_update_dispatcher.ts b/core/deploy/11_stbtc_update_dispatcher.ts index 280950a1d..9fc08fc51 100644 --- a/core/deploy/11_stbtc_update_dispatcher.ts +++ b/core/deploy/11_stbtc_update_dispatcher.ts @@ -18,4 +18,4 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func func.tags = ["stBTCUpdateDispatcher"] -func.dependencies = ["stBTC", "Dispatcher"] +func.dependencies = ["stBTC", "MezoAllocator"] diff --git a/core/deploy/13_mezo_allocator_update_storage.ts b/core/deploy/13_mezo_allocator_update_storage.ts index 8f6785756..dad7d1a6f 100644 --- a/core/deploy/13_mezo_allocator_update_storage.ts +++ b/core/deploy/13_mezo_allocator_update_storage.ts @@ -23,4 +23,4 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func func.tags = ["MezoAllocatorUpdateStorage"] -func.dependencies = ["stBTC"] +func.dependencies = ["stBTC", "MezoAllocator"] From 109421349b016402171ba740a26238eae633e121 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 5 Apr 2024 14:45:24 +0200 Subject: [PATCH 066/123] Updating deployment Mezo Portal artifacts --- core/external/mainnet/MezoPortal.json | 827 +++++++++++++++++++++++++- core/external/sepolia/MezoPortal.json | 827 +++++++++++++++++++++++++- 2 files changed, 1652 insertions(+), 2 deletions(-) diff --git a/core/external/mainnet/MezoPortal.json b/core/external/mainnet/MezoPortal.json index 858fc0b32..07a2d5bce 100644 --- a/core/external/mainnet/MezoPortal.json +++ b/core/external/mainnet/MezoPortal.json @@ -1,3 +1,828 @@ { - "address": "0xAB13B8eecf5AA2460841d75da5d5D861fD5B8A39" + "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/sepolia/MezoPortal.json b/core/external/sepolia/MezoPortal.json index 7c6c1309f..2ba1a0e07 100644 --- a/core/external/sepolia/MezoPortal.json +++ b/core/external/sepolia/MezoPortal.json @@ -1,3 +1,828 @@ { - "address": "0x6978E3e11b8Bc34ea836C1706fC742aC4Cb6b0Db" + "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 From fee50a4c31e81141885e0ad8a3cf5cb8b0ab8b9f Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 5 Apr 2024 15:54:00 +0200 Subject: [PATCH 067/123] Replacing tbtcStorage with stBTC contract Acre will pull the funds directly from stBTC contract to allocate in MezoPortal. --- core/contracts/MezoAllocator.sol | 42 +++++++++++++------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index f8f307792..c6f1777b8 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -5,6 +5,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/Ownable2Step.sol"; import {ZeroAddress} from "./utils/Errors.sol"; +import "./stBTC.sol"; interface IMezoPortal { struct DepositInfo { @@ -33,8 +34,8 @@ contract MezoAllocator is Ownable2Step { IMezoPortal public immutable mezoPortal; /// tBTC token contract. IERC20 public immutable tbtc; - /// Contract holding tBTC deposited by stakers. - address public tbtcStorage; + /// stBTC token vault contract. + stBTC public immutable stbtc; /// @notice Maintainer address which can trigger deposit flow. address public maintainer; /// @notice keeps track of the latest deposit ID assigned in Mezo Portal. @@ -48,9 +49,6 @@ contract MezoAllocator is Ownable2Step { uint256 newDepositAmount ); - /// @notice Emitted when the tBTC storage address is updated. - event TbtcStorageUpdated(address indexed tbtcStorage); - /// @notice Emitted when the maintainer address is updated. event MaintainerUpdated(address indexed maintainer); @@ -67,7 +65,11 @@ contract MezoAllocator is Ownable2Step { /// @notice Initializes the MezoAllocator contract. /// @param _mezoPortal Address of the MezoPortal contract. /// @param _tbtc Address of the tBTC token contract. - constructor(address _mezoPortal, IERC20 _tbtc) Ownable(msg.sender) { + constructor( + address _mezoPortal, + IERC20 _tbtc, + stBTC _stbtc + ) Ownable(msg.sender) { if (_mezoPortal == address(0)) { revert ZeroAddress(); } @@ -76,6 +78,7 @@ contract MezoAllocator is Ownable2Step { } mezoPortal = IMezoPortal(_mezoPortal); tbtc = _tbtc; + stbtc = _stbtc; } /// @notice Allocate tBTC to MezoPortal. Each allocation creates a new "rolling" @@ -93,15 +96,18 @@ contract MezoAllocator is Ownable2Step { // slither-disable-next-line reentrancy-no-eth mezoPortal.withdraw(address(tbtc), depositId, depositBalance); } - uint256 addedAmount = IERC20(tbtc).balanceOf(address(tbtcStorage)); + uint256 addedAmount = IERC20(tbtc).balanceOf(address(stbtc)); // slither-disable-next-line arbitrary-send-erc20 - IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), addedAmount); + IERC20(tbtc).safeTransferFrom( + address(stbtc), + address(this), + addedAmount + ); uint96 newDepositAmount = uint96(IERC20(tbtc).balanceOf(address(this))); IERC20(tbtc).forceApprove(address(mezoPortal), newDepositAmount); - // 0 denotes no lock period for this deposit. The zero lock time is - // hardcoded as of biz decision. + // 0 denotes no lock period for this deposit. mezoPortal.deposit(address(tbtc), newDepositAmount, 0); uint256 oldDepositId = depositId; // MezoPortal doesn't return depositId, so we have to read depositCounter @@ -117,20 +123,6 @@ contract MezoAllocator is Ownable2Step { ); } - /// @notice Updates the tBTC storage address. - /// @dev At first this is going to be the stBTC contract. Once Acre - /// works with more destinations for tBTC, this will be updated to - /// the new storage contract like AcreDispatcher. - /// @param _tbtcStorage Address of the new tBTC storage. - function updateTbtcStorage(address _tbtcStorage) external onlyOwner { - if (_tbtcStorage == address(0)) { - revert ZeroAddress(); - } - tbtcStorage = _tbtcStorage; - - emit TbtcStorageUpdated(_tbtcStorage); - } - /// @notice Updates the maintainer address. /// @param _maintainer Address of the new maintainer. function updateMaintainer(address _maintainer) external onlyOwner { @@ -151,6 +143,6 @@ contract MezoAllocator is Ownable2Step { // reached. Delete deposit ids that are empty. // IMezoPortal(mezoPortal).withdraw(address(tbtc), depositId, amount); // TODO: update depositsById and deposits data structures. - // IERC20(tbtc).safeTransfer(address(tbtcStorage), amount); + // IERC20(tbtc).safeTransfer(address(stbtc), amount); } } From 67993ce3ed2bf831f55ffa991facefc6faed318d Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 5 Apr 2024 22:14:35 +0200 Subject: [PATCH 068/123] Drafting withdrawals from Mezo Allocator Acre can pull partial or all assets from Mezo Allocator for given deposit id. These assets are transferred back to stBTC contract and users shares are burned and tBTC is returned to a receiver. --- core/contracts/MezoAllocator.sol | 53 ++++++++++++++++++++++++++------ core/contracts/stBTC.sol | 28 ++++++++++++++--- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index c6f1777b8..0e4f5120c 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -38,6 +38,8 @@ contract MezoAllocator is Ownable2Step { stBTC public immutable stbtc; /// @notice Maintainer address which can trigger deposit flow. address public maintainer; + /// @notice Address that can withdraw tBTC from Mezo Portal. + address public withdrawer; /// @notice keeps track of the latest deposit ID assigned in Mezo Portal. uint256 public depositId; @@ -49,12 +51,21 @@ contract MezoAllocator is Ownable2Step { uint256 newDepositAmount ); + /// Emitted when tBTC is withdrawn from MezoPortal. + event DepositWithdraw(uint256 indexed depositId, uint96 amount); + /// @notice Emitted when the maintainer address is updated. event MaintainerUpdated(address indexed maintainer); + /// @notice Emitted when the withdrawer address is updated. + event WithdrawerUpdated(address indexed withdrawer); + /// @notice Reverts if the caller is not an authorized account. error NotAuthorized(); + /// @notice Reverts if the caller tries to withdraw more tBTC than available. + error InsufficientBalance(); + modifier onlyMaintainerAndOwner() { if (msg.sender != maintainer && owner() != msg.sender) { revert NotAuthorized(); @@ -62,6 +73,13 @@ contract MezoAllocator is Ownable2Step { _; } + modifier onlyWithdrawer() { + if (msg.sender != withdrawer) { + revert NotAuthorized(); + } + _; + } + /// @notice Initializes the MezoAllocator contract. /// @param _mezoPortal Address of the MezoPortal contract. /// @param _tbtc Address of the tBTC token contract. @@ -123,6 +141,22 @@ contract MezoAllocator is Ownable2Step { ); } + /// @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(uint96 amount) external onlyWithdrawer { + uint96 balance = mezoPortal + .getDeposit(address(this), address(tbtc), depositId) + .balance; + if (amount > balance) { + revert InsufficientBalance(); + } + emit DepositWithdraw(depositId, amount); + mezoPortal.withdraw(address(tbtc), depositId, amount); + IERC20(tbtc).safeTransfer(address(stbtc), amount); + } + /// @notice Updates the maintainer address. /// @param _maintainer Address of the new maintainer. function updateMaintainer(address _maintainer) external onlyOwner { @@ -134,15 +168,14 @@ contract MezoAllocator is Ownable2Step { emit MaintainerUpdated(_maintainer); } - // TODO: add updatable withdrawer and onlyWithdrawer modifier (stBTC or AcreDispatcher). - /// @notice Withdraws tBTC from MezoPortal and transfers it to stBTC. - function withdraw(uint96 amount) external { - // TODO: Take the last deposit and pull the funds from it (FIFO). - // If not enough funds, take everything from that deposit and - // take the rest from the next deposit id until the amount is - // reached. Delete deposit ids that are empty. - // IMezoPortal(mezoPortal).withdraw(address(tbtc), depositId, amount); - // TODO: update depositsById and deposits data structures. - // IERC20(tbtc).safeTransfer(address(stbtc), amount); + /// @notice Updates the withdrawer address. + /// @param _withdrawer Address of the new withdrawer. + function updateWithdrawer(address _withdrawer) external onlyOwner { + if (_withdrawer == address(0)) { + revert ZeroAddress(); + } + withdrawer = _withdrawer; + + emit WithdrawerUpdated(_withdrawer); } } diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 563918084..c4a862d91 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -8,6 +8,11 @@ import "./PausableOwnable.sol"; import "./lib/ERC4626Fees.sol"; import {ZeroAddress} from "./utils/Errors.sol"; +// slither-disable-next-line missing-inheritance +interface IDispatcher { + function withdraw(uint256 amount) external; +} + /// @title stBTC /// @notice This contract implements the ERC-4626 tokenized vault standard. By /// staking tBTC, users acquire a liquid staking token called stBTC, @@ -22,9 +27,9 @@ import {ZeroAddress} from "./utils/Errors.sol"; contract stBTC is ERC4626Fees, PausableOwnable { using SafeERC20 for IERC20; - /// Dispatcher contract that routes tBTC from stBTC to a given destination - /// 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; @@ -124,7 +129,7 @@ contract stBTC is ERC4626Fees, PausableOwnable { /// @notice Updates the dispatcher contract and gives it an unlimited /// allowance to transfer staked tBTC. /// @param newDispatcher Address of the new dispatcher contract. - function updateDispatcher(Dispatcher newDispatcher) external onlyOwner { + function updateDispatcher(IDispatcher newDispatcher) external onlyOwner { if (address(newDispatcher) == address(0)) { revert ZeroAddress(); } @@ -215,14 +220,27 @@ 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) { + ) public override returns (uint256) { + if (assets > totalAssets()) { + uint256 missingAmount = assets - totalAssets(); + dispatcher.withdraw(missingAmount); + } + return super.withdraw(assets, receiver, owner); } + // TODO: change this function to pull assets from the dispatcher. function redeem( uint256 shares, address receiver, From eae07205a735b1cc60abb3f4eac3a37eba51dcc1 Mon Sep 17 00:00:00 2001 From: ioay Date: Mon, 8 Apr 2024 09:29:07 +0200 Subject: [PATCH 069/123] Styles update & added useCallback to useActivity hook --- dapp/src/components/GlobalStyles/index.tsx | 2 ++ dapp/src/components/shared/Carousel.tsx | 3 ++- dapp/src/hooks/useActivities.ts | 11 ++++++++--- dapp/src/pages/ActivityPage/ActivityDetails.tsx | 14 ++++++++------ .../ActivityCarousel/ActivityCarousel.tsx | 6 +++--- dapp/src/pages/OverviewPage/DocsCard.tsx | 1 - dapp/src/pages/OverviewPage/index.tsx | 5 +++-- 7 files changed, 26 insertions(+), 16 deletions(-) diff --git a/dapp/src/components/GlobalStyles/index.tsx b/dapp/src/components/GlobalStyles/index.tsx index 5fd79a4e5..5c93f8dc2 100644 --- a/dapp/src/components/GlobalStyles/index.tsx +++ b/dapp/src/components/GlobalStyles/index.tsx @@ -43,6 +43,8 @@ export default function GlobalStyles() { } // 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; } diff --git a/dapp/src/components/shared/Carousel.tsx b/dapp/src/components/shared/Carousel.tsx index 524a7aa2c..3ca5e2b2a 100644 --- a/dapp/src/components/shared/Carousel.tsx +++ b/dapp/src/components/shared/Carousel.tsx @@ -22,7 +22,8 @@ export const Carousel = forwardRef( diff --git a/dapp/src/hooks/useActivities.ts b/dapp/src/hooks/useActivities.ts index d6764549b..bf7a80a98 100644 --- a/dapp/src/hooks/useActivities.ts +++ b/dapp/src/hooks/useActivities.ts @@ -24,7 +24,10 @@ export function useActivities() { [activities], ) - const selectedActivity = getActivity(params.activityId) + const selectedActivity = useCallback( + () => getActivity(params.activityId), + [getActivity, params.activityId], + ) const isSelected = useCallback( (activity: ActivityInfo): boolean => @@ -32,8 +35,10 @@ export function useActivities() { [getActivity, params.activityId], ) - const isCompleted = (activity: ActivityInfo): boolean => - activity.status === "completed" + const isCompleted = useCallback( + (activity: ActivityInfo): boolean => activity.status === "completed", + [], + ) return { activities, diff --git a/dapp/src/pages/ActivityPage/ActivityDetails.tsx b/dapp/src/pages/ActivityPage/ActivityDetails.tsx index dc45dd7a1..44fd84b21 100644 --- a/dapp/src/pages/ActivityPage/ActivityDetails.tsx +++ b/dapp/src/pages/ActivityPage/ActivityDetails.tsx @@ -19,11 +19,13 @@ import { useActivities } from "#/hooks" function ActivityDetails() { const { selectedActivity } = useActivities() - if (!selectedActivity) return null + const currentActivity = selectedActivity() + + if (!currentActivity) return null return ( - {selectedActivity.status === "pending" && ( + {currentActivity.status === "pending" && ( @@ -49,12 +51,12 @@ function ActivityDetails() { fontWeight="semibold" textTransform="capitalize" > - {selectedActivity.action} + {currentActivity.action} ))} diff --git a/dapp/src/pages/OverviewPage/DocsCard.tsx b/dapp/src/pages/OverviewPage/DocsCard.tsx index 42db18181..dafe74c04 100644 --- a/dapp/src/pages/OverviewPage/DocsCard.tsx +++ b/dapp/src/pages/OverviewPage/DocsCard.tsx @@ -10,7 +10,6 @@ export function DocsCard(props: CardProps) { return ( From 3b5aea00c193b2b07e5ff9ce03c79dc12f471372 Mon Sep 17 00:00:00 2001 From: ioay Date: Mon, 8 Apr 2024 09:43:38 +0200 Subject: [PATCH 070/123] Moved carosuel settings directly to carousel component --- .../ActivityCarousel/ActivityCarousel.tsx | 81 ++++++++++++++++++- .../ActivityCarousel/settings.tsx | 81 ------------------- 2 files changed, 80 insertions(+), 82 deletions(-) delete mode 100644 dapp/src/pages/OverviewPage/ActivityCarousel/settings.tsx diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx index bcd8130eb..3a35b9a9e 100644 --- a/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx +++ b/dapp/src/pages/OverviewPage/ActivityCarousel/ActivityCarousel.tsx @@ -5,7 +5,86 @@ import { Carousel } from "#/components/shared/Carousel" import { ActivityCard } from "#/components/shared/ActivityCard" import { useActivities } from "#/hooks" import { ActivityInfo } from "#/types" -import { activityCarouselSettings } from "./settings" +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) diff --git a/dapp/src/pages/OverviewPage/ActivityCarousel/settings.tsx b/dapp/src/pages/OverviewPage/ActivityCarousel/settings.tsx deleted file mode 100644 index ec0989a56..000000000 --- a/dapp/src/pages/OverviewPage/ActivityCarousel/settings.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from "react" -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, - }, - }, - ], -} From fe194470437e03117fb60aa199f188fa95903e05 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 8 Apr 2024 11:23:27 +0200 Subject: [PATCH 071/123] Adding redeeming function Took assets from stBTC first before calling for withdrawal on the Dispatcher contract. --- core/contracts/stBTC.sol | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index c4a862d91..338a4d3c2 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -231,7 +231,7 @@ contract stBTC is ERC4626Fees, PausableOwnable { uint256 assets, address receiver, address owner - ) public override returns (uint256) { + ) public override whenNotPaused returns (uint256) { if (assets > totalAssets()) { uint256 missingAmount = assets - totalAssets(); dispatcher.withdraw(missingAmount); @@ -240,12 +240,23 @@ contract stBTC is ERC4626Fees, PausableOwnable { return super.withdraw(assets, receiver, owner); } - // TODO: change this function to pull assets from the dispatcher. + /// @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 = super.previewRedeem(shares); + if (assets > totalAssets()) { + uint256 missingAmount = assets - totalAssets(); + dispatcher.withdraw(missingAmount); + } + return super.redeem(shares, receiver, owner); } From 0e519372a033c01d211046daea1790e64267dce6 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 8 Apr 2024 11:30:31 +0200 Subject: [PATCH 072/123] Updating deloyment scripts --- core/deploy/02_deploy_mezo_allocator.ts | 5 +++-- .../deploy/12_dispatcher_update_maintainer.ts | 19 ------------------- ...=> 14_mezo_allocator_update_withdrawer.ts} | 10 +++++----- 3 files changed, 8 insertions(+), 26 deletions(-) delete mode 100644 core/deploy/12_dispatcher_update_maintainer.ts rename core/deploy/{13_mezo_allocator_update_storage.ts => 14_mezo_allocator_update_withdrawer.ts} (75%) diff --git a/core/deploy/02_deploy_mezo_allocator.ts b/core/deploy/02_deploy_mezo_allocator.ts index 33cee9344..f1002afba 100644 --- a/core/deploy/02_deploy_mezo_allocator.ts +++ b/core/deploy/02_deploy_mezo_allocator.ts @@ -7,11 +7,12 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployer } = await getNamedAccounts() const tbtc = await deployments.get("TBTC") + const stbtc = await deployments.get("stBTC") const mezoPortal = await deployments.get("MezoPortal") const mezoAllocator = await deployments.deploy("MezoAllocator", { from: deployer, - args: [mezoPortal.address, tbtc.address], + args: [mezoPortal.address, tbtc.address, stbtc.address], log: true, waitConfirmations: waitConfirmationsNumber(hre), }) @@ -26,4 +27,4 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func func.tags = ["MezoAllocator"] -func.dependencies = ["TBTC", "MezoPortal"] +func.dependencies = ["TBTC", "stBTC", "MezoPortal"] diff --git a/core/deploy/12_dispatcher_update_maintainer.ts b/core/deploy/12_dispatcher_update_maintainer.ts deleted file mode 100644 index 8f616fac0..000000000 --- a/core/deploy/12_dispatcher_update_maintainer.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { HardhatRuntimeEnvironment } from "hardhat/types" -import type { DeployFunction } from "hardhat-deploy/types" - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { getNamedAccounts, deployments } = hre - const { deployer, maintainer } = await getNamedAccounts() - - await deployments.execute( - "Dispatcher", - { from: deployer, log: true, waitConfirmations: 1 }, - "updateMaintainer", - maintainer, - ) -} - -export default func - -func.tags = ["DispatcherUpdateMaintainer"] -func.dependencies = ["Dispatcher"] diff --git a/core/deploy/13_mezo_allocator_update_storage.ts b/core/deploy/14_mezo_allocator_update_withdrawer.ts similarity index 75% rename from core/deploy/13_mezo_allocator_update_storage.ts rename to core/deploy/14_mezo_allocator_update_withdrawer.ts index dad7d1a6f..86a178173 100644 --- a/core/deploy/13_mezo_allocator_update_storage.ts +++ b/core/deploy/14_mezo_allocator_update_withdrawer.ts @@ -6,7 +6,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, deployments } = hre const { deployer } = await getNamedAccounts() - const stbtc = await deployments.get("stBTC") + const withdrawer = await deployments.get("stBTC") await deployments.execute( "MezoAllocator", @@ -15,12 +15,12 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { log: true, waitConfirmations: waitConfirmationsNumber(hre), }, - "updateTbtcStorage", - stbtc.address, + "updateWithdrawer", + withdrawer.address, ) } export default func -func.tags = ["MezoAllocatorUpdateStorage"] -func.dependencies = ["stBTC", "MezoAllocator"] +func.tags = ["MezoAllocatorUpdateWithdrawer"] +func.dependencies = ["stBTC"] From b7ed6000962a61c7a89ff35e987cde1f682a0dbe Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 8 Apr 2024 11:32:14 +0200 Subject: [PATCH 073/123] Fixing deployment tests --- core/test/Deployment.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/test/Deployment.test.ts b/core/test/Deployment.test.ts index 5b8d42de8..86f79fbd8 100644 --- a/core/test/Deployment.test.ts +++ b/core/test/Deployment.test.ts @@ -29,7 +29,7 @@ describe("Deployment", () => { await loadFixture(fixture)) }) - describe("Acre", () => { + describe("stBTC", () => { describe("constructor", () => { context("when treasury has been set", () => { it("should be set to a treasury address", async () => { @@ -72,12 +72,12 @@ describe("Deployment", () => { }) }) - describe("updateTbtcStorage", () => { - context("when a new stBTC address has been set", () => { - it("should be set to a new stBTC address by the deployment script", async () => { - const actualTbtcStorage = await mezoAllocator.tbtcStorage() + describe("updateWithdrawer", () => { + context("when a new withdrawer has been set", () => { + it("should be set to a new maintainer address", async () => { + const actualWithdrawer = await mezoAllocator.withdrawer() - expect(actualTbtcStorage).to.be.equal(await stbtc.getAddress()) + expect(actualWithdrawer).to.be.equal(await stbtc.getAddress()) }) }) }) From 0f6f6f7b9fd8c76671335d3270ec78508b8b8615 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 8 Apr 2024 14:40:18 +0200 Subject: [PATCH 074/123] Check if TBTC token owner matches TBTCVault Current implementation of the redemptions mechanism assumes specific TBTCVault contract implementation to be the owner of the TBTC Token contract. We want to ensure that the owner of TBTC Vault haven't changed, so we don't accidentially lock the tBTC tokens. --- core/contracts/BitcoinRedeemer.sol | 61 +++- core/contracts/test/TestTBTC.sol | 14 +- core/deploy/00_resolve_tbtc_vault.ts | 9 +- core/deploy/04_deploy_bitcoin_redeemer.ts | 5 +- .../24_transfer_ownership_bitcoin_redeemer.ts | 31 ++ core/test/BitcoinRedeemer.test.ts | 286 ++++++++++++------ 6 files changed, 289 insertions(+), 117 deletions(-) create mode 100644 core/deploy/24_transfer_ownership_bitcoin_redeemer.ts diff --git a/core/contracts/BitcoinRedeemer.sol b/core/contracts/BitcoinRedeemer.sol index 94828859a..097f1b077 100644 --- a/core/contracts/BitcoinRedeemer.sol +++ b/core/contracts/BitcoinRedeemer.sol @@ -1,23 +1,32 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +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 Initializable, IReceiveApproval { +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. @@ -34,6 +43,9 @@ contract BitcoinRedeemer is Initializable, IReceiveApproval { /// 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); @@ -43,6 +55,9 @@ contract BitcoinRedeemer is Initializable, IReceiveApproval { /// 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(); @@ -51,19 +66,31 @@ contract BitcoinRedeemer is Initializable, IReceiveApproval { _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 - function initialize(address _tbtcToken, address _stbtc) public initializer { + /// @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. @@ -86,6 +113,18 @@ contract BitcoinRedeemer is Initializable, IReceiveApproval { 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 @@ -108,18 +147,16 @@ contract BitcoinRedeemer is Initializable, IReceiveApproval { 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( - // TBTC Token contract owner resolves to the TBTCVault contract. - tbtcToken.owner(), - tbtcAmount, - tbtcRedemptionData - ) + !tbtcToken.approveAndCall(tbtcVault, tbtcAmount, tbtcRedemptionData) ) { revert ApproveAndCallFailed(); } diff --git a/core/contracts/test/TestTBTC.sol b/core/contracts/test/TestTBTC.sol index 63bf580c3..e6ed5152a 100644 --- a/core/contracts/test/TestTBTC.sol +++ b/core/contracts/test/TestTBTC.sol @@ -13,7 +13,11 @@ contract TestTBTC is ITBTCToken, ERC20 { bool public approveAndCallResult = true; - constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + 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); @@ -29,11 +33,11 @@ contract TestTBTC is ITBTCToken, ERC20 { return approveAndCallResult; } - function owner() external pure returns (address) { - return address(1); - } - function setApproveAndCallResult(bool value) public { approveAndCallResult = value; } + + function setOwner(address newOwner) public { + owner = newOwner; + } } diff --git a/core/deploy/00_resolve_tbtc_vault.ts b/core/deploy/00_resolve_tbtc_vault.ts index 46edbd919..27034347f 100644 --- a/core/deploy/00_resolve_tbtc_vault.ts +++ b/core/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/04_deploy_bitcoin_redeemer.ts b/core/deploy/04_deploy_bitcoin_redeemer.ts index 4eeccf4cf..7dbc0d847 100644 --- a/core/deploy/04_deploy_bitcoin_redeemer.ts +++ b/core/deploy/04_deploy_bitcoin_redeemer.ts @@ -8,12 +8,13 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { 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], + initializerArgs: [tbtc.address, stbtc.address, tbtcVault.address], factoryOpts: { signer: deployer }, proxyOpts: { kind: "transparent", @@ -32,4 +33,4 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func func.tags = ["BitcoinRedeemer"] -func.dependencies = ["TBTC", "stBTC"] +func.dependencies = ["TBTC", "stBTC", "TBTCVault"] diff --git a/core/deploy/24_transfer_ownership_bitcoin_redeemer.ts b/core/deploy/24_transfer_ownership_bitcoin_redeemer.ts new file mode 100644 index 000000000..f3e78caee --- /dev/null +++ b/core/deploy/24_transfer_ownership_bitcoin_redeemer.ts @@ -0,0 +1,31 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { deployer, governance } = await getNamedAccounts() + const { log } = deployments + + log(`transferring ownership of BitcoinRedeemer contract to ${governance}`) + + await deployments.execute( + "BitcoinRedeemer", + { from: deployer, log: true, waitConfirmations: 1 }, + "transferOwnership", + governance, + ) + + if (hre.network.name !== "mainnet") { + await deployments.execute( + "BitcoinRedeemer", + { from: governance, log: true, waitConfirmations: 1 }, + "acceptOwnership", + ) + } +} + +export default func + +func.tags = ["TransferOwnershipBitcoinRedeemer"] +func.dependencies = ["BitcoinRedeemer"] +func.runAtTheEnd = true diff --git a/core/test/BitcoinRedeemer.test.ts b/core/test/BitcoinRedeemer.test.ts index f456b5fed..ddec546e8 100644 --- a/core/test/BitcoinRedeemer.test.ts +++ b/core/test/BitcoinRedeemer.test.ts @@ -1,6 +1,10 @@ import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" import { expect } from "chai" -import { ContractTransactionResponse, encodeBytes32String } from "ethers" +import { + ContractTransactionResponse, + encodeBytes32String, + ZeroAddress, +} from "ethers" import { helpers } from "hardhat" import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" @@ -11,12 +15,14 @@ import { to1e18 } from "./utils" import type { StBTC as stBTC, BitcoinRedeemer, TestTBTC } from "../typechain" import { tbtcRedemptionData } from "./data/tbtc" -const { getUnnamedSigners } = helpers.signers +const { getNamedSigners, getUnnamedSigners } = helpers.signers async function fixture() { const { tbtc, stbtc, bitcoinRedeemer } = await deployment() - const [depositor] = await getUnnamedSigners() + const { governance } = await getNamedSigners() + + const [depositor, thirdParty] = await getUnnamedSigners() const amountToMint = to1e18(100000) await tbtc.mint(depositor, amountToMint) @@ -25,7 +31,9 @@ async function fixture() { stbtc, tbtc, bitcoinRedeemer, + governance, depositor, + thirdParty, } } @@ -34,10 +42,13 @@ describe("BitcoinRedeemer", () => { let tbtc: TestTBTC let bitcoinRedeemer: BitcoinRedeemer + let governance: HardhatEthersSigner let depositor: HardhatEthersSigner + let thirdParty: HardhatEthersSigner before(async () => { - ;({ stbtc, tbtc, bitcoinRedeemer, depositor } = await loadFixture(fixture)) + ;({ stbtc, tbtc, bitcoinRedeemer, governance, depositor, thirdParty } = + await loadFixture(fixture)) }) describe("receiveApproval", () => { @@ -87,7 +98,14 @@ describe("BitcoinRedeemer", () => { }) context("when called with non-empty extraData", () => { - context("when caller has no deposit", () => { + 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 @@ -97,38 +115,22 @@ describe("BitcoinRedeemer", () => { to1e18(1), tbtcRedemptionData.redemptionData, ), + ).to.be.revertedWithCustomError( + bitcoinRedeemer, + "UnexpectedTbtcTokenOwner", ) - .to.be.revertedWithCustomError(stbtc, "ERC4626ExceededMaxRedeem") - .withArgs(await depositor.getAddress(), to1e18(1), 0) }) }) - 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 + 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(), - amountToRedeem, + to1e18(1), tbtcRedemptionData.redemptionData, ), ) @@ -136,96 +138,134 @@ describe("BitcoinRedeemer", () => { stbtc, "ERC4626ExceededMaxRedeem", ) - .withArgs( - await depositor.getAddress(), - amountToRedeem, - depositAmount, - ) + .withArgs(await depositor.getAddress(), to1e18(1), 0) }) }) - 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, - ) - }) + context("when caller has deposit", () => { + beforeAfterSnapshotWrapper() - it("should emit RedemptionRequested event", async () => { - await expect(tx) - .to.emit(bitcoinRedeemer, "RedemptionRequested") - .withArgs( - depositor.address, - stBtcAmountToRedeem, - tbtcAmountToRedeem, - ) - }) + const depositAmount = to1e18(10) + const earnedYield = to1e18(8) - it("should burn stBTC tokens", async () => { - await expect(tx).to.changeTokenBalances( - stbtc, - [depositor], - [-stBtcAmountToRedeem], - ) + before(async () => { + await tbtc + .connect(depositor) + .approve(await stbtc.getAddress(), depositAmount) + await stbtc + .connect(depositor) + .deposit(depositAmount, depositor.address) - expect(await stbtc.totalSupply()).to.be.equal( - depositAmount - stBtcAmountToRedeem, - ) - }) + await tbtc.mint(await stbtc.getAddress(), earnedYield) + }) - it("should transfer tBTC tokens", async () => { - await expect(tx).to.changeTokenBalances( - tbtc, - [stbtc, bitcoinRedeemer], - [-tbtcAmountToRedeem, tbtcAmountToRedeem], + 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, + ), ) - }) - - it("should call approveAndCall in tBTC contract", async () => { - await expect(tx) - .to.emit(tbtc, "ApproveAndCallCalled") + .to.be.revertedWithCustomError( + stbtc, + "ERC4626ExceededMaxRedeem", + ) .withArgs( - await tbtc.owner(), - tbtcAmountToRedeem, - tbtcRedemptionData.redemptionData, + await depositor.getAddress(), + amountToRedeem, + depositAmount, ) }) }) - context("when tBTC.approveAndCall returns false", () => { - beforeAfterSnapshotWrapper() + 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 - before(async () => { - await tbtc.setApproveAndCallResult(false) - }) + context("when tBTC.approveAndCall returns true", () => { + beforeAfterSnapshotWrapper() - it("should revert", async () => { - await expect( - stbtc + let tx: ContractTransactionResponse + + before(async () => { + tx = await stbtc .connect(depositor) .approveAndCall( await bitcoinRedeemer.getAddress(), stBtcAmountToRedeem, tbtcRedemptionData.redemptionData, - ), - ).to.be.revertedWithCustomError( - bitcoinRedeemer, - "ApproveAndCallFailed", - ) + ) + }) + + it("should emit RedemptionRequested event", async () => { + await expect(tx) + .to.emit(bitcoinRedeemer, "RedemptionRequested") + .withArgs( + depositor.address, + stBtcAmountToRedeem, + tbtcAmountToRedeem, + ) + }) + + it("should burn stBTC tokens", async () => { + await expect(tx).to.changeTokenBalances( + stbtc, + [depositor], + [-stBtcAmountToRedeem], + ) + + 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", + ) + }) }) }) }) @@ -233,4 +273,56 @@ describe("BitcoinRedeemer", () => { }) }) }) + + 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) + }) + }) + }) + }) }) From be029b81c562caed77c6db3676430a1a83dd50cf Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 8 Apr 2024 15:02:21 +0200 Subject: [PATCH 075/123] Reading balance of tBTC in stBTC using balanceOf instead of totalAssets totalAssets should be reserved for reading total assets across all the yield sources including stBTC contract. --- core/contracts/stBTC.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 338a4d3c2..0c0cf946c 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -232,9 +232,9 @@ contract stBTC is ERC4626Fees, PausableOwnable { address receiver, address owner ) public override whenNotPaused returns (uint256) { - if (assets > totalAssets()) { - uint256 missingAmount = assets - totalAssets(); - dispatcher.withdraw(missingAmount); + uint256 currentAssetsBalance = IERC20(asset()).balanceOf(address(this)); + if (assets > currentAssetsBalance) { + dispatcher.withdraw(assets - currentAssetsBalance); } return super.withdraw(assets, receiver, owner); @@ -252,9 +252,9 @@ contract stBTC is ERC4626Fees, PausableOwnable { address owner ) public override whenNotPaused returns (uint256) { uint256 assets = super.previewRedeem(shares); - if (assets > totalAssets()) { - uint256 missingAmount = assets - totalAssets(); - dispatcher.withdraw(missingAmount); + uint256 currentAssetsBalance = IERC20(asset()).balanceOf(address(this)); + if (assets > currentAssetsBalance) { + dispatcher.withdraw(assets - currentAssetsBalance); } return super.redeem(shares, receiver, owner); From f2729c37deff755044787dfb160e4d694f1665e5 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 8 Apr 2024 15:09:15 +0200 Subject: [PATCH 076/123] Removing IERC20 casting for tBTC. It's already declared as IERC20 --- core/contracts/MezoAllocator.sol | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 0e4f5120c..45d60fb8e 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -114,17 +114,13 @@ contract MezoAllocator is Ownable2Step { // slither-disable-next-line reentrancy-no-eth mezoPortal.withdraw(address(tbtc), depositId, depositBalance); } - uint256 addedAmount = IERC20(tbtc).balanceOf(address(stbtc)); + uint256 addedAmount = tbtc.balanceOf(address(stbtc)); // slither-disable-next-line arbitrary-send-erc20 - IERC20(tbtc).safeTransferFrom( - address(stbtc), - address(this), - addedAmount - ); + tbtc.safeTransferFrom(address(stbtc), address(this), addedAmount); - uint96 newDepositAmount = uint96(IERC20(tbtc).balanceOf(address(this))); + uint96 newDepositAmount = uint96(tbtc.balanceOf(address(this))); - IERC20(tbtc).forceApprove(address(mezoPortal), newDepositAmount); + tbtc.forceApprove(address(mezoPortal), newDepositAmount); // 0 denotes no lock period for this deposit. mezoPortal.deposit(address(tbtc), newDepositAmount, 0); uint256 oldDepositId = depositId; @@ -154,7 +150,7 @@ contract MezoAllocator is Ownable2Step { } emit DepositWithdraw(depositId, amount); mezoPortal.withdraw(address(tbtc), depositId, amount); - IERC20(tbtc).safeTransfer(address(stbtc), amount); + tbtc.safeTransfer(address(stbtc), amount); } /// @notice Updates the maintainer address. From d7fafb3d75580e56714a3e012c56ce3963acfcd6 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 8 Apr 2024 15:33:02 +0200 Subject: [PATCH 077/123] Implementing totalAssets for tBTC balance check totalAssets in stBTC should return all the tBTC across all allocations and its own contract totalAssets in MezoAllocator should return all tBTC staked in Mezo Portal --- core/contracts/MezoAllocator.sol | 20 +++++++++++++++++--- core/contracts/interfaces/IDispatcher.sol | 6 ++++++ core/contracts/stBTC.sol | 14 +++++++++----- 3 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 core/contracts/interfaces/IDispatcher.sol diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 45d60fb8e..70d918f03 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/Ownable2Step.sol"; import {ZeroAddress} from "./utils/Errors.sol"; import "./stBTC.sol"; +import "./interfaces/IDispatcher.sol"; interface IMezoPortal { struct DepositInfo { @@ -52,7 +53,7 @@ contract MezoAllocator is Ownable2Step { ); /// Emitted when tBTC is withdrawn from MezoPortal. - event DepositWithdraw(uint256 indexed depositId, uint96 amount); + event DepositWithdraw(uint256 indexed depositId, uint256 amount); /// @notice Emitted when the maintainer address is updated. event MaintainerUpdated(address indexed maintainer); @@ -141,7 +142,7 @@ contract MezoAllocator is Ownable2Step { /// 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(uint96 amount) external onlyWithdrawer { + function withdraw(uint256 amount) external onlyWithdrawer { uint96 balance = mezoPortal .getDeposit(address(this), address(tbtc), depositId) .balance; @@ -149,7 +150,7 @@ contract MezoAllocator is Ownable2Step { revert InsufficientBalance(); } emit DepositWithdraw(depositId, amount); - mezoPortal.withdraw(address(tbtc), depositId, amount); + mezoPortal.withdraw(address(tbtc), depositId, uint96(amount)); tbtc.safeTransfer(address(stbtc), amount); } @@ -174,4 +175,17 @@ contract MezoAllocator is Ownable2Step { emit WithdrawerUpdated(_withdrawer); } + + /// @notice Returns the total amount of tBTC allocated to MezoPortal. + function totalAssets() + external + view + override + returns (uint256 totalAmount) + { + return + mezoPortal + .getDeposit(address(this), address(tbtc), depositId) + .balance; + } } diff --git a/core/contracts/interfaces/IDispatcher.sol b/core/contracts/interfaces/IDispatcher.sol new file mode 100644 index 000000000..5a8abbe2c --- /dev/null +++ b/core/contracts/interfaces/IDispatcher.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.8.21; + +interface IDispatcher { + function withdraw(uint256 amount) external; + function totalAssets() external view returns (uint256); +} diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 0c0cf946c..dce7e4073 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -6,13 +6,9 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Dispatcher.sol"; import "./PausableOwnable.sol"; import "./lib/ERC4626Fees.sol"; +import "./interfaces/IDispatcher.sol"; import {ZeroAddress} from "./utils/Errors.sol"; -// slither-disable-next-line missing-inheritance -interface IDispatcher { - function withdraw(uint256 amount) external; -} - /// @title stBTC /// @notice This contract implements the ERC-4626 tokenized vault standard. By /// staking tBTC, users acquire a liquid staking token called stBTC, @@ -176,6 +172,14 @@ 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) { + uint256 totalAmount = IERC20(asset()).balanceOf(address(this)); + totalAmount += dispatcher.totalAssets(); + return totalAmount; + } + /// @notice Mints shares to receiver by depositing exactly amount of /// tBTC tokens. /// @dev Takes into account a deposit parameter, minimum deposit amount, From 4a3eb9d1fd791c5a03570ceac3ae0377f4f2cdcc Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 8 Apr 2024 15:41:25 +0200 Subject: [PATCH 078/123] Replacing withdrawer role with hardcoded stBTC contract --- core/contracts/MezoAllocator.sol | 27 ++----------------- .../14_mezo_allocator_update_withdrawer.ts | 26 ------------------ core/test/Deployment.test.ts | 10 ------- 3 files changed, 2 insertions(+), 61 deletions(-) delete mode 100644 core/deploy/14_mezo_allocator_update_withdrawer.ts diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 70d918f03..b6c82f749 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -39,8 +39,6 @@ contract MezoAllocator is Ownable2Step { stBTC public immutable stbtc; /// @notice Maintainer address which can trigger deposit flow. address public maintainer; - /// @notice Address that can withdraw tBTC from Mezo Portal. - address public withdrawer; /// @notice keeps track of the latest deposit ID assigned in Mezo Portal. uint256 public depositId; @@ -58,9 +56,6 @@ contract MezoAllocator is Ownable2Step { /// @notice Emitted when the maintainer address is updated. event MaintainerUpdated(address indexed maintainer); - /// @notice Emitted when the withdrawer address is updated. - event WithdrawerUpdated(address indexed withdrawer); - /// @notice Reverts if the caller is not an authorized account. error NotAuthorized(); @@ -74,13 +69,6 @@ contract MezoAllocator is Ownable2Step { _; } - modifier onlyWithdrawer() { - if (msg.sender != withdrawer) { - revert NotAuthorized(); - } - _; - } - /// @notice Initializes the MezoAllocator contract. /// @param _mezoPortal Address of the MezoPortal contract. /// @param _tbtc Address of the tBTC token contract. @@ -142,7 +130,8 @@ contract MezoAllocator is Ownable2Step { /// 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 onlyWithdrawer { + function withdraw(uint256 amount) external { + if (msg.sender != address(stbtc)) revert NotAuthorized(); uint96 balance = mezoPortal .getDeposit(address(this), address(tbtc), depositId) .balance; @@ -165,22 +154,10 @@ contract MezoAllocator is Ownable2Step { emit MaintainerUpdated(_maintainer); } - /// @notice Updates the withdrawer address. - /// @param _withdrawer Address of the new withdrawer. - function updateWithdrawer(address _withdrawer) external onlyOwner { - if (_withdrawer == address(0)) { - revert ZeroAddress(); - } - withdrawer = _withdrawer; - - emit WithdrawerUpdated(_withdrawer); - } - /// @notice Returns the total amount of tBTC allocated to MezoPortal. function totalAssets() external view - override returns (uint256 totalAmount) { return diff --git a/core/deploy/14_mezo_allocator_update_withdrawer.ts b/core/deploy/14_mezo_allocator_update_withdrawer.ts deleted file mode 100644 index 86a178173..000000000 --- a/core/deploy/14_mezo_allocator_update_withdrawer.ts +++ /dev/null @@ -1,26 +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 { deployer } = await getNamedAccounts() - - const withdrawer = await deployments.get("stBTC") - - await deployments.execute( - "MezoAllocator", - { - from: deployer, - log: true, - waitConfirmations: waitConfirmationsNumber(hre), - }, - "updateWithdrawer", - withdrawer.address, - ) -} - -export default func - -func.tags = ["MezoAllocatorUpdateWithdrawer"] -func.dependencies = ["stBTC"] diff --git a/core/test/Deployment.test.ts b/core/test/Deployment.test.ts index 86f79fbd8..6571063bc 100644 --- a/core/test/Deployment.test.ts +++ b/core/test/Deployment.test.ts @@ -71,15 +71,5 @@ describe("Deployment", () => { }) }) }) - - describe("updateWithdrawer", () => { - context("when a new withdrawer has been set", () => { - it("should be set to a new maintainer address", async () => { - const actualWithdrawer = await mezoAllocator.withdrawer() - - expect(actualWithdrawer).to.be.equal(await stbtc.getAddress()) - }) - }) - }) }) }) From 06b542622155a2f014c212d30fdd88f41fe48e71 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 8 Apr 2024 15:57:08 +0200 Subject: [PATCH 079/123] Import ethers to solve type checks --- core/test/BitcoinRedeemer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/test/BitcoinRedeemer.test.ts b/core/test/BitcoinRedeemer.test.ts index ddec546e8..d4acc1d5e 100644 --- a/core/test/BitcoinRedeemer.test.ts +++ b/core/test/BitcoinRedeemer.test.ts @@ -5,7 +5,7 @@ import { encodeBytes32String, ZeroAddress, } from "ethers" -import { helpers } from "hardhat" +import { ethers, helpers } from "hardhat" import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { beforeAfterSnapshotWrapper, deployment } from "./helpers" From 5e23302d56fa581af5e48f5e584d5c11d51e537b Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 8 Apr 2024 16:03:58 +0200 Subject: [PATCH 080/123] Add more context about redeemer in tBTC redemption data --- core/contracts/BitcoinRedeemer.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/contracts/BitcoinRedeemer.sol b/core/contracts/BitcoinRedeemer.sol index 097f1b077..b3c51ba18 100644 --- a/core/contracts/BitcoinRedeemer.sol +++ b/core/contracts/BitcoinRedeemer.sol @@ -137,6 +137,10 @@ contract BitcoinRedeemer is Ownable2StepUpgradeable, IReceiveApproval { /// 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 staker 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. From 98c6f0098c95def864797713c40f42a314c44808 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 8 Apr 2024 16:14:30 +0200 Subject: [PATCH 081/123] Adding a list of maintainers An owner can approve multiple maintainers that can trigger funds allocation. --- core/contracts/MezoAllocator.sol | 72 +++++++++++++++-------- core/contracts/interfaces/IDispatcher.sol | 2 + 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index b6c82f749..0c808f331 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -31,39 +31,42 @@ interface IMezoPortal { contract MezoAllocator is Ownable2Step { using SafeERC20 for IERC20; - /// Address of the MezoPortal contract. + /// @notice Address of the MezoPortal contract. IMezoPortal public immutable mezoPortal; - /// tBTC token contract. + /// @notice tBTC token contract. IERC20 public immutable tbtc; - /// stBTC token vault contract. + /// @notice stBTC token vault contract. stBTC public immutable stbtc; - /// @notice Maintainer address which can trigger deposit flow. - address public maintainer; + /// @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; - /// Emitted when tBTC is deposited to MezoPortal. + /// @notice Emitted when tBTC is deposited to MezoPortal. event DepositAllocated( uint256 indexed oldDepositId, uint256 indexed newDepositId, uint256 addedAmount, uint256 newDepositAmount ); - - /// Emitted when tBTC is withdrawn from MezoPortal. + /// @notice Emitted when tBTC is withdrawn from MezoPortal. event DepositWithdraw(uint256 indexed depositId, uint256 amount); - /// @notice Emitted when the maintainer address is updated. - event MaintainerUpdated(address indexed maintainer); - + event MaintainerAdded(address indexed maintainer); + /// @notice Emitted when the maintainer address is updated. + event MaintainerRemoved(address indexed maintainer); /// @notice Reverts if the caller is not an authorized account. error NotAuthorized(); - /// @notice Reverts if the caller tries to withdraw more tBTC than available. error InsufficientBalance(); + /// @notice Reverts if the caller is not a maintainer. + error NotMaintainer(); - modifier onlyMaintainerAndOwner() { - if (msg.sender != maintainer && owner() != msg.sender) { + modifier onlyMaintainer() { + if (!isMaintainer[msg.sender]) { revert NotAuthorized(); } _; @@ -93,8 +96,8 @@ contract MezoAllocator is Ownable2Step { /// 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 bot. - function allocate() external onlyMaintainerAndOwner { + /// @dev This function can be invoked periodically by a maintainer. + function allocate() external onlyMaintainer { uint96 depositBalance = mezoPortal .getDeposit(address(this), address(tbtc), depositId) .balance; @@ -144,22 +147,39 @@ contract MezoAllocator is Ownable2Step { } /// @notice Updates the maintainer address. - /// @param _maintainer Address of the new maintainer. - function updateMaintainer(address _maintainer) external onlyOwner { - if (_maintainer == address(0)) { + /// @param maintainerToAdd Address of the new maintainer. + function addMaintainer(address maintainerToAdd) external onlyOwner { + if (maintainerToAdd == address(0)) { revert ZeroAddress(); } - maintainer = _maintainer; + 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 NotMaintainer(); + } + 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 MaintainerUpdated(_maintainer); + emit MaintainerRemoved(maintainerToRemove); } /// @notice Returns the total amount of tBTC allocated to MezoPortal. - function totalAssets() - external - view - returns (uint256 totalAmount) - { + function totalAssets() external view returns (uint256 totalAmount) { return mezoPortal .getDeposit(address(this), address(tbtc), depositId) diff --git a/core/contracts/interfaces/IDispatcher.sol b/core/contracts/interfaces/IDispatcher.sol index 5a8abbe2c..af28b1b10 100644 --- a/core/contracts/interfaces/IDispatcher.sol +++ b/core/contracts/interfaces/IDispatcher.sol @@ -1,5 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; +// slither-disable-next-line missing-inheritance interface IDispatcher { function withdraw(uint256 amount) external; function totalAssets() external view returns (uint256); From ba702cb1dd2bc6204d63b03fa6989ec1a61c262e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 8 Apr 2024 16:15:36 +0200 Subject: [PATCH 082/123] Adding getDeposit function to MezoPortalStub --- core/contracts/test/MezoPortalStub.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/contracts/test/MezoPortalStub.sol b/core/contracts/test/MezoPortalStub.sol index bdeaae0ee..d38b86de8 100644 --- a/core/contracts/test/MezoPortalStub.sol +++ b/core/contracts/test/MezoPortalStub.sol @@ -21,4 +21,15 @@ contract MezoPortalStub { 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 + ); + } } From 1a5dc7dbeff3c0658aacd31569b32b21e5fadeb2 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 8 Apr 2024 21:21:05 +0200 Subject: [PATCH 083/123] Fixing shares->assets by using convertToAssets Accounted for the fees that will be collected by Acre. We need to use convertToAssests instead of previewRedeem function. --- core/contracts/stBTC.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index dce7e4073..8b91845b5 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -255,7 +255,7 @@ contract stBTC is ERC4626Fees, PausableOwnable { address receiver, address owner ) public override whenNotPaused returns (uint256) { - uint256 assets = super.previewRedeem(shares); + uint256 assets = convertToAssets(shares); uint256 currentAssetsBalance = IERC20(asset()).balanceOf(address(this)); if (assets > currentAssetsBalance) { dispatcher.withdraw(assets - currentAssetsBalance); From 1f90524ab7696c235123f252279c1621cc47f535 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 8 Apr 2024 21:26:49 +0200 Subject: [PATCH 084/123] Removing balance check for while withdrawing We can also rely on MezoPortal check that will also include balance validation against the amount to be withdrawn. --- core/contracts/MezoAllocator.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 0c808f331..b8ce32d6e 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -135,12 +135,6 @@ contract MezoAllocator is Ownable2Step { /// @param amount Amount of tBTC to withdraw. function withdraw(uint256 amount) external { if (msg.sender != address(stbtc)) revert NotAuthorized(); - uint96 balance = mezoPortal - .getDeposit(address(this), address(tbtc), depositId) - .balance; - if (amount > balance) { - revert InsufficientBalance(); - } emit DepositWithdraw(depositId, amount); mezoPortal.withdraw(address(tbtc), depositId, uint96(amount)); tbtc.safeTransfer(address(stbtc), amount); From 791b10cfa44bb39c729f6c545ea675d3885f815b Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 8 Apr 2024 21:30:27 +0200 Subject: [PATCH 085/123] Inlining math for totalAssets() --- core/contracts/stBTC.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 8b91845b5..d97aadd8b 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -175,9 +175,8 @@ contract stBTC is ERC4626Fees, PausableOwnable { /// @notice Returns the total amount of assets held by the vault across all /// allocations and this contract. function totalAssets() public view override returns (uint256) { - uint256 totalAmount = IERC20(asset()).balanceOf(address(this)); - totalAmount += dispatcher.totalAssets(); - return totalAmount; + return + IERC20(asset()).balanceOf(address(this)) + dispatcher.totalAssets(); } /// @notice Mints shares to receiver by depositing exactly amount of From ace0dbf2bb278da28d2dc16634e16a28d46e92b3 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 9 Apr 2024 11:44:47 +0200 Subject: [PATCH 086/123] MezoAllocator inheriting IDispatcher interface --- core/contracts/MezoAllocator.sol | 2 +- core/contracts/interfaces/IDispatcher.sol | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index b8ce32d6e..98f6b43ce 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -28,7 +28,7 @@ interface IMezoPortal { } /// @notice MezoAllocator routes tBTC to/from MezoPortal. -contract MezoAllocator is Ownable2Step { +contract MezoAllocator is IDispatcher, Ownable2Step { using SafeERC20 for IERC20; /// @notice Address of the MezoPortal contract. diff --git a/core/contracts/interfaces/IDispatcher.sol b/core/contracts/interfaces/IDispatcher.sol index af28b1b10..0490db373 100644 --- a/core/contracts/interfaces/IDispatcher.sol +++ b/core/contracts/interfaces/IDispatcher.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -// slither-disable-next-line missing-inheritance interface IDispatcher { function withdraw(uint256 amount) external; function totalAssets() external view returns (uint256); From 614dfa01f81fea5064c172e43b266954c00ef362 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 9 Apr 2024 12:22:33 +0200 Subject: [PATCH 087/123] Define separate it for totalSupply check --- core/test/BitcoinRedeemer.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/test/BitcoinRedeemer.test.ts b/core/test/BitcoinRedeemer.test.ts index d4acc1d5e..2874cf9cd 100644 --- a/core/test/BitcoinRedeemer.test.ts +++ b/core/test/BitcoinRedeemer.test.ts @@ -214,13 +214,15 @@ describe("BitcoinRedeemer", () => { ) }) - it("should burn stBTC tokens", async () => { + 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, ) From c777719c623acf1c6f6aabd981bed4a442db9f97 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 9 Apr 2024 14:07:36 +0200 Subject: [PATCH 088/123] Tracking of the deposit balance locally in Mezo Allocator Storing Mezo Portal deposit balance locally instead of calling a view function on Mezo Portal. Added docs around IMezoPortal interface --- core/contracts/MezoAllocator.sol | 50 +++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 98f6b43ce..91fa836c9 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -8,18 +8,49 @@ 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, @@ -44,6 +75,8 @@ contract MezoAllocator is IDispatcher, Ownable2Step { 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( @@ -98,9 +131,6 @@ contract MezoAllocator is IDispatcher, Ownable2Step { /// 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 { - uint96 depositBalance = mezoPortal - .getDeposit(address(this), address(tbtc), depositId) - .balance; if (depositBalance > 0) { // Free all Acre's tBTC from MezoPortal before creating a new deposit. // slither-disable-next-line reentrancy-no-eth @@ -110,11 +140,11 @@ contract MezoAllocator is IDispatcher, Ownable2Step { // slither-disable-next-line arbitrary-send-erc20 tbtc.safeTransferFrom(address(stbtc), address(this), addedAmount); - uint96 newDepositAmount = uint96(tbtc.balanceOf(address(this))); + depositBalance = uint96(tbtc.balanceOf(address(this))); - tbtc.forceApprove(address(mezoPortal), newDepositAmount); + tbtc.forceApprove(address(mezoPortal), depositBalance); // 0 denotes no lock period for this deposit. - mezoPortal.deposit(address(tbtc), newDepositAmount, 0); + 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. @@ -125,7 +155,7 @@ contract MezoAllocator is IDispatcher, Ownable2Step { oldDepositId, depositId, addedAmount, - newDepositAmount + depositBalance ); } @@ -136,6 +166,7 @@ contract MezoAllocator is IDispatcher, Ownable2Step { function withdraw(uint256 amount) external { if (msg.sender != address(stbtc)) revert NotAuthorized(); emit DepositWithdraw(depositId, amount); + depositBalance -= uint96(amount); mezoPortal.withdraw(address(tbtc), depositId, uint96(amount)); tbtc.safeTransfer(address(stbtc), amount); } @@ -174,9 +205,6 @@ contract MezoAllocator is IDispatcher, Ownable2Step { /// @notice Returns the total amount of tBTC allocated to MezoPortal. function totalAssets() external view returns (uint256 totalAmount) { - return - mezoPortal - .getDeposit(address(this), address(tbtc), depositId) - .balance; + return depositBalance; } } From 4de3f2d6d670dcd961e4f5151cef88cc51970917 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 9 Apr 2024 14:16:07 +0200 Subject: [PATCH 089/123] DepositWithdraw -> DepositWithdrawn event --- core/contracts/MezoAllocator.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 91fa836c9..22c50f7ed 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -86,7 +86,7 @@ contract MezoAllocator is IDispatcher, Ownable2Step { uint256 newDepositAmount ); /// @notice Emitted when tBTC is withdrawn from MezoPortal. - event DepositWithdraw(uint256 indexed depositId, uint256 amount); + 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. @@ -165,7 +165,7 @@ contract MezoAllocator is IDispatcher, Ownable2Step { /// @param amount Amount of tBTC to withdraw. function withdraw(uint256 amount) external { if (msg.sender != address(stbtc)) revert NotAuthorized(); - emit DepositWithdraw(depositId, amount); + emit DepositWithdrawn(depositId, amount); depositBalance -= uint96(amount); mezoPortal.withdraw(address(tbtc), depositId, uint96(amount)); tbtc.safeTransfer(address(stbtc), amount); From 67da3b907742a1f4402edd3d07d001ab6648ff81 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 9 Apr 2024 14:16:40 +0200 Subject: [PATCH 090/123] Removing unused event --- core/contracts/MezoAllocator.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 22c50f7ed..8a0f54e48 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -93,8 +93,6 @@ contract MezoAllocator is IDispatcher, Ownable2Step { event MaintainerRemoved(address indexed maintainer); /// @notice Reverts if the caller is not an authorized account. error NotAuthorized(); - /// @notice Reverts if the caller tries to withdraw more tBTC than available. - error InsufficientBalance(); /// @notice Reverts if the caller is not a maintainer. error NotMaintainer(); From 0aa3357e1dd48b49fba7542162b9439d3a8245ee Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 9 Apr 2024 14:18:00 +0200 Subject: [PATCH 091/123] Adding validation for stBTC address not zero --- core/contracts/MezoAllocator.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 8a0f54e48..0da91fd48 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -117,6 +117,9 @@ contract MezoAllocator is IDispatcher, Ownable2Step { if (address(_tbtc) == address(0)) { revert ZeroAddress(); } + if (address(_stbtc) == address(0)) { + revert ZeroAddress(); + } mezoPortal = IMezoPortal(_mezoPortal); tbtc = _tbtc; stbtc = _stbtc; From 5f9fa2f0a273b62c53d2b9ba9e25ae4871002d67 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 9 Apr 2024 14:55:51 +0200 Subject: [PATCH 092/123] Renaming errors around new maintainers NotMaintainer -> MaintainerNotRegistered AlreadyMaintainer -> MaintainerAlreadyRegistered --- core/contracts/MezoAllocator.sol | 14 +++++++++++--- core/contracts/interfaces/IDispatcher.sol | 5 +++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 0da91fd48..e61117a7c 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -94,7 +94,9 @@ contract MezoAllocator is IDispatcher, Ownable2Step { /// @notice Reverts if the caller is not an authorized account. error NotAuthorized(); /// @notice Reverts if the caller is not a maintainer. - error NotMaintainer(); + error MaintainerNotRegistered(); + /// @notice Reverts if the caller is already a maintainer. + error MaintainerAlreadyRegistered(); modifier onlyMaintainer() { if (!isMaintainer[msg.sender]) { @@ -137,12 +139,14 @@ contract MezoAllocator is IDispatcher, Ownable2Step { // 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); @@ -166,6 +170,7 @@ contract MezoAllocator is IDispatcher, Ownable2Step { /// @param amount Amount of tBTC to withdraw. function withdraw(uint256 amount) external { if (msg.sender != address(stbtc)) revert NotAuthorized(); + emit DepositWithdrawn(depositId, amount); depositBalance -= uint96(amount); mezoPortal.withdraw(address(tbtc), depositId, uint96(amount)); @@ -178,6 +183,9 @@ contract MezoAllocator is IDispatcher, Ownable2Step { if (maintainerToAdd == address(0)) { revert ZeroAddress(); } + if (isMaintainer[maintainerToAdd]) { + revert MaintainerAlreadyRegistered(); + } maintainers.push(maintainerToAdd); isMaintainer[maintainerToAdd] = true; @@ -188,7 +196,7 @@ contract MezoAllocator is IDispatcher, Ownable2Step { /// @param maintainerToRemove Address of the maintainer to remove. function removeMaintainer(address maintainerToRemove) external onlyOwner { if (!isMaintainer[maintainerToRemove]) { - revert NotMaintainer(); + revert MaintainerNotRegistered(); } delete (isMaintainer[maintainerToRemove]); diff --git a/core/contracts/interfaces/IDispatcher.sol b/core/contracts/interfaces/IDispatcher.sol index 0490db373..4c762240c 100644 --- a/core/contracts/interfaces/IDispatcher.sol +++ b/core/contracts/interfaces/IDispatcher.sol @@ -1,7 +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); } From 9b1f0697195ea164aae855a74f773dd96ef0b653 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 9 Apr 2024 16:48:50 +0200 Subject: [PATCH 093/123] Pulling assets from Mezo Portal first We need to pull funds from Mezo Portal first before we can update the balance. This is because if amount > then the balance, it should revert. Otherwise, we'll get the underflow error. --- core/contracts/MezoAllocator.sol | 3 ++- core/test/BitcoinRedeemer.test.ts | 19 ++++--------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index e61117a7c..ebc46b677 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -172,8 +172,9 @@ contract MezoAllocator is IDispatcher, Ownable2Step { if (msg.sender != address(stbtc)) revert NotAuthorized(); emit DepositWithdrawn(depositId, amount); - depositBalance -= uint96(amount); mezoPortal.withdraw(address(tbtc), depositId, uint96(amount)); + // slither-disable-next-line reentrancy-benign + depositBalance -= uint96(amount); tbtc.safeTransfer(address(stbtc), amount); } diff --git a/core/test/BitcoinRedeemer.test.ts b/core/test/BitcoinRedeemer.test.ts index 2874cf9cd..396391945 100644 --- a/core/test/BitcoinRedeemer.test.ts +++ b/core/test/BitcoinRedeemer.test.ts @@ -133,12 +133,7 @@ describe("BitcoinRedeemer", () => { to1e18(1), tbtcRedemptionData.redemptionData, ), - ) - .to.be.revertedWithCustomError( - stbtc, - "ERC4626ExceededMaxRedeem", - ) - .withArgs(await depositor.getAddress(), to1e18(1), 0) + ).to.be.revertedWithCustomError(stbtc, "ERC20InsufficientBalance") }) }) @@ -170,16 +165,10 @@ describe("BitcoinRedeemer", () => { amountToRedeem, tbtcRedemptionData.redemptionData, ), + ).to.be.revertedWithCustomError( + stbtc, + "ERC20InsufficientBalance", ) - .to.be.revertedWithCustomError( - stbtc, - "ERC4626ExceededMaxRedeem", - ) - .withArgs( - await depositor.getAddress(), - amountToRedeem, - depositAmount, - ) }) }) From 39bc4176baf13687f50ad1e98e9efca61ee9f313 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 9 Apr 2024 16:52:19 +0200 Subject: [PATCH 094/123] Fixing and adding some of the tests for MezoAllocator --- .../12_mezo_allocator_update_maintainer.ts | 2 +- core/test/Deployment.test.ts | 4 +- core/test/MezoAllocator.test.ts | 151 +++++------------- 3 files changed, 39 insertions(+), 118 deletions(-) diff --git a/core/deploy/12_mezo_allocator_update_maintainer.ts b/core/deploy/12_mezo_allocator_update_maintainer.ts index 4e1f5fa18..aa7cec484 100644 --- a/core/deploy/12_mezo_allocator_update_maintainer.ts +++ b/core/deploy/12_mezo_allocator_update_maintainer.ts @@ -13,7 +13,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { log: true, waitConfirmations: waitConfirmationsNumber(hre), }, - "updateMaintainer", + "addMaintainer", maintainer, ) } diff --git a/core/test/Deployment.test.ts b/core/test/Deployment.test.ts index 6571063bc..d318d9652 100644 --- a/core/test/Deployment.test.ts +++ b/core/test/Deployment.test.ts @@ -65,9 +65,9 @@ describe("Deployment", () => { describe("updateMaintainer", () => { context("when a new maintainer has been set", () => { it("should be set to a new maintainer address", async () => { - const actualMaintainer = await mezoAllocator.maintainer() + const isMaintainer = await mezoAllocator.isMaintainer(maintainer) - expect(actualMaintainer).to.be.equal(await maintainer.getAddress()) + expect(isMaintainer).to.be.equal(true) }) }) }) diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index 7fb25c48a..e12c90eeb 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -39,167 +39,88 @@ describe("MezoAllocator", () => { let mezoAllocator: MezoAllocator let mezoPortal: IMezoPortal - let governance: HardhatEthersSigner let thirdParty: HardhatEthersSigner let maintainer: HardhatEthersSigner before(async () => { - ;({ - governance, - thirdParty, - maintainer, - tbtc, - stbtc, - mezoAllocator, - mezoPortal, - } = await loadFixture(fixture)) + ;({ thirdParty, maintainer, tbtc, stbtc, mezoAllocator, mezoPortal } = + await loadFixture(fixture)) }) describe("allocate", () => { beforeAfterSnapshotWrapper() - context("when the caller is not an owner", () => { + context("when a caller is not a maintainer", () => { it("should revert", async () => { await expect( - mezoAllocator.connect(thirdParty).allocate(to1e18(1)), + mezoAllocator.connect(thirdParty).allocate(), ).to.be.revertedWithCustomError(mezoAllocator, "NotAuthorized") }) }) - context("when the caller is an owner", () => { - it("should not revert", async () => { - await expect( - mezoAllocator.connect(governance).allocate(to1e18(1)), - ).to.not.be.revertedWithCustomError(mezoAllocator, "NotAuthorized") - }) - }) - context("when the caller is maintainer", () => { - context("when first deposit is made", () => { + context("when a first deposit is made", () => { let tx: ContractTransactionResponse before(async () => { - await tbtc.mint(await stbtc.getAddress(), to1e18(1)) - await mezoAllocator - .connect(governance) - .updateMaintainer(maintainer.address) - - tx = await mezoAllocator.connect(maintainer).allocate(to1e18(1)) + await tbtc.mint(await stbtc.getAddress(), to1e18(6)) + tx = await mezoAllocator.connect(maintainer).allocate() }) it("should deposit and transfer tBTC to Mezo Portal", async () => { - expect( - await tbtc.balanceOf(await mezoAllocator.getAddress()), - ).to.equal(0) expect(await tbtc.balanceOf(await mezoPortal.getAddress())).to.equal( - to1e18(1), + to1e18(6), ) }) - it("should set deposit balance", async () => { - const deposit = await mezoAllocator.depositInfo() - expect(deposit.balance).to.equal(to1e18(1)) - }) - - it("should set creation timestamp", async () => { - const deposit = await mezoAllocator.depositInfo() - const dateTime = new Date() - // Check if the block timestamp is within 60 seconds of the current - // test time - expect(deposit.createdAt).to.be.closeTo( - String(dateTime.valueOf()).slice(0, -3), - 60, - ) + it("should not store any tBTC in Mezo Allocator", async () => { + expect( + await tbtc.balanceOf(await mezoAllocator.getAddress()), + ).to.equal(0) }) - it("should set unlocking timestamp", async () => { - const deposit = await mezoAllocator.depositInfo() - const dateTime = new Date() - // Check if the block timestamp is within 60 seconds of the current - // test time - expect(deposit.unlockAt).to.be.closeTo( - String(dateTime.valueOf()).slice(0, -3), - 60, - ) + it("should increment the deposit id", async () => { + const actualDepositId = await mezoAllocator.depositId() + expect(actualDepositId).to.equal(1) }) - it("should emit Deposit event", async () => { + it("should emit DepositAllocated event", async () => { await expect(tx) .to.emit(mezoAllocator, "DepositAllocated") - .withArgs(0, 1, to1e18(1)) + .withArgs(0, 1, to1e18(6), to1e18(6)) }) }) - context("when second deposit is made", () => { + context("when a second deposit is made", () => { + let tx: ContractTransactionResponse + before(async () => { await tbtc.mint(await stbtc.getAddress(), to1e18(5)) - await mezoAllocator - .connect(governance) - .updateMaintainer(maintainer.address) - await mezoAllocator.connect(maintainer).allocate(to1e18(5)) + tx = await mezoAllocator.connect(maintainer).allocate() }) it("should increment the deposit id", async () => { - const depositInfo = await mezoAllocator.depositInfo() - expect(depositInfo.id).to.equal(2) + const actualDepositId = await mezoAllocator.depositId() + expect(actualDepositId).to.equal(2) }) - it("should populate deposit balance", async () => { - const deposit = await mezoAllocator.depositInfo() - // 1 + 5 = 6 - expect(deposit.balance).to.equal(to1e18(6)) + it("should emit DepositAllocated event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "DepositAllocated") + .withArgs(1, 2, to1e18(5), to1e18(11)) }) - }) - }) - }) - - describe("updateTbtcStorage", () => { - context("when the caller is not an owner", () => { - it("should revert", async () => { - await expect( - mezoAllocator - .connect(thirdParty) - .updateTbtcStorage(thirdParty.address), - ).to.be.revertedWithCustomError( - mezoAllocator, - "OwnableUnauthorizedAccount", - ) - }) - }) - - context("when the caller is an owner", () => { - it("should not revert", async () => { - await mezoAllocator - .connect(governance) - .updateTbtcStorage(thirdParty.address) - const tbtcStorageAddress = await mezoAllocator.tbtcStorage() - expect(tbtcStorageAddress).to.equal(thirdParty.address) - }) - }) - }) - describe("updateMaintainer", () => { - context("when the caller is not an owner", () => { - it("should revert", async () => { - await expect( - mezoAllocator - .connect(thirdParty) - .updateMaintainer(thirdParty.address), - ).to.be.revertedWithCustomError( - mezoAllocator, - "OwnableUnauthorizedAccount", - ) - }) - }) + it("should deposit and transfer tBTC to Mezo Portal", async () => { + expect(await tbtc.balanceOf(await mezoPortal.getAddress())).to.equal( + to1e18(11), + ) + }) - context("when the caller is an owner", () => { - it("should not revert", async () => { - await mezoAllocator - .connect(governance) - .updateMaintainer(thirdParty.address) - const maintainerAddress = await mezoAllocator.maintainer() - expect(maintainerAddress).to.equal(thirdParty.address) + it("should increase tracked deposit balance amount", async () => { + const depositBalance = await mezoAllocator.depositBalance() + expect(depositBalance).to.equal(to1e18(11)) + }) }) }) }) From 4036ea904dbe321f95b885feba3715763756efe3 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 9 Apr 2024 15:59:09 +0200 Subject: [PATCH 095/123] Replace stake naming with deposit We decided not to call the operation stake in the dapp, so for consistency we align the naming here. It makes it more consistent also with the AbstractBitcoinDepositor as we call the operation deposit there. --- core/contracts/BitcoinDepositor.sol | 189 ++++++++++++++-------------- 1 file changed, 93 insertions(+), 96 deletions(-) diff --git a/core/contracts/BitcoinDepositor.sol b/core/contracts/BitcoinDepositor.sol index c0de3cd37..0576ff3da 100644 --- a/core/contracts/BitcoinDepositor.sol +++ b/core/contracts/BitcoinDepositor.sol @@ -12,13 +12,13 @@ import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol"; import {stBTC} from "./stBTC.sol"; /// @title Bitcoin Depositor contract. -/// @notice The contract integrates Acre staking with tBTC minting. -/// User who wants to stake BTC in Acre should submit a Bitcoin transaction +/// @notice The contract integrates Acre depositing with tBTC minting. +/// User who wants to deposit BTC in Acre should submit a Bitcoin transaction /// to the most recently created off-chain ECDSA wallets of the tBTC Bridge /// using pay-to-script-hash (P2SH) or pay-to-witness-script-hash (P2WSH) /// containing hashed information about this Depositor contract address, -/// and staker's Ethereum address. -/// Then, the staker initiates tBTC minting by revealing their Ethereum +/// and deposit owner's Ethereum address. +/// Then, the deposit owner initiates tBTC minting by revealing their Ethereum /// address along with their deposit blinding factor, refund public key /// hash and refund locktime on the tBTC Bridge through this Depositor /// contract. @@ -32,22 +32,29 @@ import {stBTC} from "./stBTC.sol"; /// the off-chain ECDSA wallet may decide to pick the deposit transaction /// for sweeping, and when the sweep operation is confirmed on the Bitcoin /// network, the tBTC Bridge and tBTC vault mint the tBTC token to the -/// Depositor address. After tBTC is minted to the Depositor, on the stake -/// finalization tBTC is staked in Acre and stBTC shares are emitted -/// to the staker. +/// Depositor address. After tBTC is minted to the Depositor, on the deposit +/// finalization tBTC is deposited in Acre and stBTC shares are emitted +/// to the deposit owner. contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { using SafeERC20 for IERC20; - /// @notice State of the stake request. - enum StakeRequestState { + /// @notice Reflects the deposit state: + /// - Unknown deposit has not been initialized yet. + /// - Initialized deposit has been initialized with a call to + /// `initializeDeposit` function and is known to this contract. + /// - Finalized deposit led to tBTC ERC20 minting and was finalized + /// with a call to `finalizeDeposit` function that deposited tBTC + /// to the stBTC contract. + enum DepositState { Unknown, Initialized, Finalized } - /// @notice Mapping of stake requests. - /// @dev The key is a deposit key identifying the deposit. - mapping(uint256 => StakeRequestState) public stakeRequests; + /// @notice Holds the deposit state, keyed by the deposit key calculated for + /// the individual deposit during the call to `initializeDeposit` + /// function. + mapping(uint256 => DepositState) public deposits; /// @notice tBTC Token contract. IERC20 public tbtcToken; @@ -55,13 +62,13 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// @notice stBTC contract. stBTC public stbtc; - /// @notice Minimum amount of a single stake request (in tBTC token precision). + /// @notice Minimum amount of a single deposit (in tBTC token precision). /// @dev This parameter should be set to a value exceeding the minimum deposit - /// amount supported by tBTC Bridge. - uint256 public minStakeAmount; + /// amount supported by the tBTC Bridge. + uint256 public minDepositAmount; /// @notice Divisor used to compute the depositor fee taken from each deposit - /// and transferred to the treasury upon stake request finalization. + /// and transferred to the treasury upon deposit finalization. /// @dev That fee is computed as follows: /// `depositorFee = depositedAmount / depositorFeeDivisor` /// for example, if the depositor fee needs to be 2% of each deposit, @@ -69,29 +76,29 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// `1/50 = 0.02 = 2%`. uint64 public depositorFeeDivisor; - /// @notice Emitted when a stake request is initialized. + /// @notice Emitted when a deposit is initialized. /// @dev Deposit details can be fetched from {{ Bridge.DepositRevealed }} /// event emitted in the same transaction. /// @param depositKey Deposit key identifying the deposit. - /// @param caller Address that initialized the stake request. - /// @param staker The address to which the stBTC shares will be minted. + /// @param caller Address that initialized the deposit. + /// @param depositOwner The address to which the stBTC shares will be minted. /// @param initialAmount Amount of funding transaction. - event StakeRequestInitialized( + event DepositInitialized( uint256 indexed depositKey, address indexed caller, - address indexed staker, + address indexed depositOwner, uint256 initialAmount ); - /// @notice Emitted when a stake request is finalized. + /// @notice Emitted when a deposit is finalized. /// @dev Deposit details can be fetched from {{ ERC4626.Deposit }} /// event emitted in the same transaction. /// @param depositKey Deposit key identifying the deposit. - /// @param caller Address that finalized the stake request. + /// @param caller Address that finalized the deposit. /// @param initialAmount Amount of funding transaction. /// @param bridgedAmount Amount of tBTC tokens that was bridged by the tBTC bridge. /// @param depositorFee Depositor fee amount. - event StakeRequestFinalized( + event DepositFinalized( uint256 indexed depositKey, address indexed caller, uint16 indexed referral, @@ -100,10 +107,10 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { uint256 depositorFee ); - /// @notice Emitted when a minimum single stake amount is updated. - /// @param minStakeAmount New value of the minimum single stake + /// @notice Emitted when a minimum single deposit amount is updated. + /// @param minDepositAmount New value of the minimum single deposit /// amount (in tBTC token precision). - event MinStakeAmountUpdated(uint256 minStakeAmount); + event MinDepositAmountUpdated(uint256 minDepositAmount); /// @notice Emitted when a depositor fee divisor is updated. /// @param depositorFeeDivisor New value of the depositor fee divisor. @@ -115,14 +122,13 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// Reverts if the stBTC address is zero. error StbtcZeroAddress(); - /// @dev Staker address is zero. - error StakerIsZeroAddress(); + /// @dev Deposit owner address is zero. + error DepositOwnerIsZeroAddress(); - /// @dev Attempted to execute function for stake request in unexpected current - /// state. - error UnexpectedStakeRequestState( - StakeRequestState currentState, - StakeRequestState expectedState + /// @dev Attempted to execute function for deposit in unexpected current state. + error UnexpectedDepositState( + DepositState actualState, + DepositState expectedState ); /// @dev Calculated depositor fee exceeds the amount of minted tBTC tokens. @@ -131,10 +137,10 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { uint256 bridgedAmount ); - /// @dev Attempted to set minimum stake amount to a value lower than the + /// @dev Attempted to set minimum deposit amount to a value lower than the /// tBTC Bridge deposit dust threshold. - error MinStakeAmountLowerThanBridgeMinDeposit( - uint256 minStakeAmount, + error MinDepositAmountLowerThanBridgeMinDeposit( + uint256 minDepositAmount, uint256 bridgeMinDepositAmount ); @@ -169,18 +175,18 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { stbtc = stBTC(_stbtc); // TODO: Revisit initial values before mainnet deployment. - minStakeAmount = 0.015 * 1e18; // 0.015 BTC + minDepositAmount = 0.015 * 1e18; // 0.015 BTC depositorFeeDivisor = 1000; // 1/1000 == 10bps == 0.1% == 0.001 } - /// @notice This function allows staking process initialization for a Bitcoin + /// @notice This function allows depositing process initialization for a Bitcoin /// deposit made by an user with a P2(W)SH transaction. It uses the /// supplied information to reveal a deposit to the tBTC Bridge contract. /// @dev Requirements: /// - The revealed vault address must match the TBTCVault address, /// - All requirements from {Bridge#revealDepositWithExtraData} /// function must be met. - /// - `staker` must be the staker address used in the P2(W)SH BTC + /// - `depositOwner` must be the deposit owner address used in the P2(W)SH BTC /// deposit transaction as part of the extra data. /// - `referral` must be the referral info used in the P2(W)SH BTC /// deposit transaction as part of the extra data. @@ -188,15 +194,15 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// can be revealed only one time. /// @param fundingTx Bitcoin funding transaction data, see `IBridgeTypes.BitcoinTxInfo`. /// @param reveal Deposit reveal data, see `IBridgeTypes.DepositRevealInfo`. - /// @param staker The address to which the stBTC shares will be minted. + /// @param depositOwner The address to which the stBTC shares will be minted. /// @param referral Data used for referral program. - function initializeStake( + function initializeDeposit( IBridgeTypes.BitcoinTxInfo calldata fundingTx, IBridgeTypes.DepositRevealInfo calldata reveal, - address staker, + address depositOwner, uint16 referral ) external { - if (staker == address(0)) revert StakerIsZeroAddress(); + if (depositOwner == address(0)) revert DepositOwnerIsZeroAddress(); // We don't check if the request was already initialized, as this check // is enforced in `_initializeDeposit` when calling the @@ -204,47 +210,47 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { (uint256 depositKey, uint256 initialAmount) = _initializeDeposit( fundingTx, reveal, - encodeExtraData(staker, referral) + encodeExtraData(depositOwner, referral) ); - // Validate current stake request state. - if (stakeRequests[depositKey] != StakeRequestState.Unknown) - revert UnexpectedStakeRequestState( - stakeRequests[depositKey], - StakeRequestState.Unknown + // Validate current deposit state. + if (deposits[depositKey] != DepositState.Unknown) + revert UnexpectedDepositState( + deposits[depositKey], + DepositState.Unknown ); // Transition to a new state. - stakeRequests[depositKey] = StakeRequestState.Initialized; + deposits[depositKey] = DepositState.Initialized; - emit StakeRequestInitialized( + emit DepositInitialized( depositKey, msg.sender, - staker, + depositOwner, initialAmount ); } - /// @notice This function should be called for previously initialized stake + /// @notice This function should be called for previously initialized deposit /// request, after tBTC minting process completed, meaning tBTC was /// minted to this contract. - /// @dev It calculates the amount to stake based on the approximate minted + /// @dev It calculates the amount to deposit based on the approximate minted /// tBTC amount reduced by the depositor fee. /// @dev IMPORTANT NOTE: The minted tBTC amount used by this function is an /// approximation. See documentation of the /// {{AbstractTBTCDepositor#_calculateTbtcAmount}} responsible for calculating /// this value for more details. /// @param depositKey Deposit key identifying the deposit. - function finalizeStake(uint256 depositKey) external { - // Validate current stake request state. - if (stakeRequests[depositKey] != StakeRequestState.Initialized) - revert UnexpectedStakeRequestState( - stakeRequests[depositKey], - StakeRequestState.Initialized + function finalizeDeposit(uint256 depositKey) external { + // Validate current deposit state. + if (deposits[depositKey] != DepositState.Initialized) + revert UnexpectedDepositState( + deposits[depositKey], + DepositState.Initialized ); // Transition to a new state. - stakeRequests[depositKey] = StakeRequestState.Finalized; + deposits[depositKey] = DepositState.Finalized; ( uint256 initialAmount, @@ -269,9 +275,9 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { tbtcToken.safeTransfer(stbtc.treasury(), depositorFee); } - (address staker, uint16 referral) = decodeExtraData(extraData); + (address depositOwner, uint16 referral) = decodeExtraData(extraData); - emit StakeRequestFinalized( + emit DepositFinalized( depositKey, msg.sender, referral, @@ -280,33 +286,33 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { depositorFee ); - uint256 amountToStake = tbtcAmount - depositorFee; + uint256 amountToDeposit = tbtcAmount - depositorFee; // Deposit tBTC in stBTC. - tbtcToken.safeIncreaseAllowance(address(stbtc), amountToStake); + tbtcToken.safeIncreaseAllowance(address(stbtc), amountToDeposit); // slither-disable-next-line unused-return - stbtc.deposit(amountToStake, staker); + stbtc.deposit(amountToDeposit, depositOwner); } - /// @notice Updates the minimum stake amount. + /// @notice Updates the minimum deposit amount. /// @dev It requires that the new value is greater or equal to the tBTC Bridge /// deposit dust threshold, to ensure deposit will be able to be bridged. - /// @param newMinStakeAmount New minimum stake amount (in tBTC precision). - function updateMinStakeAmount( - uint256 newMinStakeAmount + /// @param newMinDepositAmount New minimum deposit amount (in tBTC precision). + function updateMinDepositAmount( + uint256 newMinDepositAmount ) external onlyOwner { uint256 minBridgeDepositAmount = _minDepositAmount(); // Check if new value is at least equal the tBTC Bridge Deposit Dust Threshold. - if (newMinStakeAmount < minBridgeDepositAmount) - revert MinStakeAmountLowerThanBridgeMinDeposit( - newMinStakeAmount, + if (newMinDepositAmount < minBridgeDepositAmount) + revert MinDepositAmountLowerThanBridgeMinDeposit( + newMinDepositAmount, minBridgeDepositAmount ); - minStakeAmount = newMinStakeAmount; + minDepositAmount = newMinDepositAmount; - emit MinStakeAmountUpdated(newMinStakeAmount); + emit MinDepositAmountUpdated(newMinDepositAmount); } /// @notice Updates the depositor fee divisor. @@ -320,39 +326,30 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { 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))); } From a1a6a7a0436344f55fc64dc1c9d6964361faf3e7 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 9 Apr 2024 17:01:25 +0200 Subject: [PATCH 096/123] Remove TODO --- core/contracts/BitcoinDepositor.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/core/contracts/BitcoinDepositor.sol b/core/contracts/BitcoinDepositor.sol index 0576ff3da..2251dbd57 100644 --- a/core/contracts/BitcoinDepositor.sol +++ b/core/contracts/BitcoinDepositor.sol @@ -320,7 +320,6 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable { function updateDepositorFeeDivisor( uint64 newDepositorFeeDivisor ) external onlyOwner { - // TODO: Introduce a parameters update process. depositorFeeDivisor = newDepositorFeeDivisor; emit DepositorFeeDivisorUpdated(newDepositorFeeDivisor); From cfef3fe09cc73e4cd38a80f8ba0724c5721a90e0 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 9 Apr 2024 17:30:36 +0200 Subject: [PATCH 097/123] Update BitcoinDepositor upgrade test contract --- .../test/upgrades/BitcoinDepositorV2.sol | 174 +++++++++--------- 1 file changed, 85 insertions(+), 89 deletions(-) diff --git a/core/contracts/test/upgrades/BitcoinDepositorV2.sol b/core/contracts/test/upgrades/BitcoinDepositorV2.sol index 106629430..85f02f3c8 100644 --- a/core/contracts/test/upgrades/BitcoinDepositorV2.sol +++ b/core/contracts/test/upgrades/BitcoinDepositorV2.sol @@ -18,16 +18,23 @@ import {stBTC} from "../../stBTC.sol"; contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { using SafeERC20 for IERC20; - /// @notice State of the stake request. - enum StakeRequestState { + /// @notice Reflects the deposit state: + /// - Unknown deposit has not been initialized yet. + /// - Initialized deposit has been initialized with a call to + /// `initializeDeposit` function and is known to this contract. + /// - Finalized deposit led to tBTC ERC20 minting and was finalized + /// with a call to `finalizeDeposit` function that deposited tBTC + /// to the stBTC contract. + enum DepositState { Unknown, Initialized, Finalized } - /// @notice Mapping of stake requests. - /// @dev The key is a deposit key identifying the deposit. - mapping(uint256 => StakeRequestState) public stakeRequests; + /// @notice Holds the deposit state, keyed by the deposit key calculated for + /// the individual deposit during the call to `initializeDeposit` + /// function. + mapping(uint256 => DepositState) public deposits; /// @notice tBTC Token contract. IERC20 public tbtcToken; @@ -35,13 +42,13 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// @notice stBTC contract. stBTC public stbtc; - /// @notice Minimum amount of a single stake request (in tBTC token precision). + /// @notice Minimum amount of a single deposit (in tBTC token precision). /// @dev This parameter should be set to a value exceeding the minimum deposit - /// amount supported by tBTC Bridge. - uint256 public minStakeAmount; + /// amount supported by the tBTC Bridge. + uint256 public minDepositAmount; /// @notice Divisor used to compute the depositor fee taken from each deposit - /// and transferred to the treasury upon stake request finalization. + /// and transferred to the treasury upon deposit finalization. /// @dev That fee is computed as follows: /// `depositorFee = depositedAmount / depositorFeeDivisor` /// for example, if the depositor fee needs to be 2% of each deposit, @@ -52,29 +59,29 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { // TEST: New variable; uint256 public newVariable; - /// @notice Emitted when a stake request is initialized. + /// @notice Emitted when a deposit is initialized. /// @dev Deposit details can be fetched from {{ Bridge.DepositRevealed }} /// event emitted in the same transaction. /// @param depositKey Deposit key identifying the deposit. - /// @param caller Address that initialized the stake request. - /// @param staker The address to which the stBTC shares will be minted. + /// @param caller Address that initialized the deposit. + /// @param depositOwner The address to which the stBTC shares will be minted. /// @param initialAmount Amount of funding transaction. - event StakeRequestInitialized( + event DepositInitialized( uint256 indexed depositKey, address indexed caller, - address indexed staker, + address indexed depositOwner, uint256 initialAmount ); - /// @notice Emitted when a stake request is finalized. + /// @notice Emitted when a deposit is finalized. /// @dev Deposit details can be fetched from {{ ERC4626.Deposit }} /// event emitted in the same transaction. /// @param depositKey Deposit key identifying the deposit. - /// @param caller Address that finalized the stake request. + /// @param caller Address that finalized the deposit. /// @param initialAmount Amount of funding transaction. /// @param bridgedAmount Amount of tBTC tokens that was bridged by the tBTC bridge. /// @param depositorFee Depositor fee amount. - event StakeRequestFinalized( + event DepositFinalized( uint256 indexed depositKey, address indexed caller, uint16 indexed referral, @@ -83,10 +90,10 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { uint256 depositorFee ); - /// @notice Emitted when a minimum single stake amount is updated. - /// @param minStakeAmount New value of the minimum single stake + /// @notice Emitted when a minimum single deposit amount is updated. + /// @param minDepositAmount New value of the minimum single deposit /// amount (in tBTC token precision). - event MinStakeAmountUpdated(uint256 minStakeAmount); + event MinDepositAmountUpdated(uint256 minDepositAmount); /// @notice Emitted when a depositor fee divisor is updated. /// @param depositorFeeDivisor New value of the depositor fee divisor. @@ -101,14 +108,13 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// Reverts if the stBTC address is zero. error StbtcZeroAddress(); - /// @dev Staker address is zero. - error StakerIsZeroAddress(); + /// @dev Deposit owner address is zero. + error DepositOwnerIsZeroAddress(); - /// @dev Attempted to execute function for stake request in unexpected current - /// state. - error UnexpectedStakeRequestState( - StakeRequestState currentState, - StakeRequestState expectedState + /// @dev Attempted to execute function for deposit in unexpected current state. + error UnexpectedDepositState( + DepositState actualState, + DepositState expectedState ); /// @dev Calculated depositor fee exceeds the amount of minted tBTC tokens. @@ -117,10 +123,10 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { uint256 bridgedAmount ); - /// @dev Attempted to set minimum stake amount to a value lower than the + /// @dev Attempted to set minimum deposit amount to a value lower than the /// tBTC Bridge deposit dust threshold. - error MinStakeAmountLowerThanBridgeMinDeposit( - uint256 minStakeAmount, + error MinDepositAmountLowerThanBridgeMinDeposit( + uint256 minDepositAmount, uint256 bridgeMinDepositAmount ); @@ -139,14 +145,14 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { newVariable = _newVariable; } - /// @notice This function allows staking process initialization for a Bitcoin + /// @notice This function allows depositing process initialization for a Bitcoin /// deposit made by an user with a P2(W)SH transaction. It uses the /// supplied information to reveal a deposit to the tBTC Bridge contract. /// @dev Requirements: /// - The revealed vault address must match the TBTCVault address, /// - All requirements from {Bridge#revealDepositWithExtraData} /// function must be met. - /// - `staker` must be the staker address used in the P2(W)SH BTC + /// - `depositOwner` must be the deposit owner address used in the P2(W)SH BTC /// deposit transaction as part of the extra data. /// - `referral` must be the referral info used in the P2(W)SH BTC /// deposit transaction as part of the extra data. @@ -154,15 +160,15 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { /// can be revealed only one time. /// @param fundingTx Bitcoin funding transaction data, see `IBridgeTypes.BitcoinTxInfo`. /// @param reveal Deposit reveal data, see `IBridgeTypes.DepositRevealInfo`. - /// @param staker The address to which the stBTC shares will be minted. + /// @param depositOwner The address to which the stBTC shares will be minted. /// @param referral Data used for referral program. - function initializeStake( + function initializeDeposit( IBridgeTypes.BitcoinTxInfo calldata fundingTx, IBridgeTypes.DepositRevealInfo calldata reveal, - address staker, + address depositOwner, uint16 referral ) external { - if (staker == address(0)) revert StakerIsZeroAddress(); + if (depositOwner == address(0)) revert DepositOwnerIsZeroAddress(); // We don't check if the request was already initialized, as this check // is enforced in `_initializeDeposit` when calling the @@ -170,47 +176,47 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { (uint256 depositKey, uint256 initialAmount) = _initializeDeposit( fundingTx, reveal, - encodeExtraData(staker, referral) + encodeExtraData(depositOwner, referral) ); - // Validate current stake request state. - if (stakeRequests[depositKey] != StakeRequestState.Unknown) - revert UnexpectedStakeRequestState( - stakeRequests[depositKey], - StakeRequestState.Unknown + // Validate current deposit state. + if (deposits[depositKey] != DepositState.Unknown) + revert UnexpectedDepositState( + deposits[depositKey], + DepositState.Unknown ); // Transition to a new state. - stakeRequests[depositKey] = StakeRequestState.Initialized; + deposits[depositKey] = DepositState.Initialized; - emit StakeRequestInitialized( + emit DepositInitialized( depositKey, msg.sender, - staker, + depositOwner, initialAmount ); } - /// @notice This function should be called for previously initialized stake + /// @notice This function should be called for previously initialized deposit /// request, after tBTC minting process completed, meaning tBTC was /// minted to this contract. - /// @dev It calculates the amount to stake based on the approximate minted + /// @dev It calculates the amount to deposit based on the approximate minted /// tBTC amount reduced by the depositor fee. /// @dev IMPORTANT NOTE: The minted tBTC amount used by this function is an /// approximation. See documentation of the /// {{AbstractTBTCDepositor#_calculateTbtcAmount}} responsible for calculating /// this value for more details. /// @param depositKey Deposit key identifying the deposit. - function finalizeStake(uint256 depositKey) external { - // Validate current stake request state. - if (stakeRequests[depositKey] != StakeRequestState.Initialized) - revert UnexpectedStakeRequestState( - stakeRequests[depositKey], - StakeRequestState.Initialized + function finalizeDeposit(uint256 depositKey) external { + // Validate current deposit state. + if (deposits[depositKey] != DepositState.Initialized) + revert UnexpectedDepositState( + deposits[depositKey], + DepositState.Initialized ); // Transition to a new state. - stakeRequests[depositKey] = StakeRequestState.Finalized; + deposits[depositKey] = DepositState.Finalized; ( uint256 initialAmount, @@ -235,9 +241,9 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { tbtcToken.safeTransfer(stbtc.treasury(), depositorFee); } - (address staker, uint16 referral) = decodeExtraData(extraData); + (address depositOwner, uint16 referral) = decodeExtraData(extraData); - emit StakeRequestFinalized( + emit DepositFinalized( depositKey, msg.sender, referral, @@ -246,33 +252,33 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { depositorFee ); - uint256 amountToStake = tbtcAmount - depositorFee; + uint256 amountToDeposit = tbtcAmount - depositorFee; // Deposit tBTC in stBTC. - tbtcToken.safeIncreaseAllowance(address(stbtc), amountToStake); + tbtcToken.safeIncreaseAllowance(address(stbtc), amountToDeposit); // slither-disable-next-line unused-return - stbtc.deposit(amountToStake, staker); + stbtc.deposit(amountToDeposit, depositOwner); } - /// @notice Updates the minimum stake amount. + /// @notice Updates the minimum deposit amount. /// @dev It requires that the new value is greater or equal to the tBTC Bridge /// deposit dust threshold, to ensure deposit will be able to be bridged. - /// @param newMinStakeAmount New minimum stake amount (in tBTC precision). - function updateMinStakeAmount( - uint256 newMinStakeAmount + /// @param newMinDepositAmount New minimum deposit amount (in tBTC precision). + function updateMinDepositAmount( + uint256 newMinDepositAmount ) external onlyOwner { uint256 minBridgeDepositAmount = _minDepositAmount(); // Check if new value is at least equal the tBTC Bridge Deposit Dust Threshold. - if (newMinStakeAmount < minBridgeDepositAmount) - revert MinStakeAmountLowerThanBridgeMinDeposit( - newMinStakeAmount, + if (newMinDepositAmount < minBridgeDepositAmount) + revert MinDepositAmountLowerThanBridgeMinDeposit( + newMinDepositAmount, minBridgeDepositAmount ); - minStakeAmount = newMinStakeAmount; + minDepositAmount = newMinDepositAmount; - emit MinStakeAmountUpdated(newMinStakeAmount); + emit MinDepositAmountUpdated(newMinDepositAmount); // TEST: Emit newly added event. emit NewEvent(); @@ -283,45 +289,35 @@ contract BitcoinDepositorV2 is AbstractTBTCDepositor, Ownable2StepUpgradeable { function updateDepositorFeeDivisor( uint64 newDepositorFeeDivisor ) external onlyOwner { - // TODO: Introduce a parameters update process. depositorFeeDivisor = newDepositorFeeDivisor; emit DepositorFeeDivisorUpdated(newDepositorFeeDivisor); } - /// @notice Minimum stake amount (in tBTC token precision). - /// @dev This function should be used by dApp to check the minimum amount - /// for the stake request. - /// @dev It is not enforced in the `initializeStakeRequest` function, as - /// it is intended to be used in the dApp staking form. - function minStake() external view returns (uint256) { - return minStakeAmount; - } - - /// @notice Encodes staker address and referral as extra data. - /// @dev Packs the data to bytes32: 20 bytes of staker address and + /// @notice Encodes deposit owner address and referral as extra data. + /// @dev Packs the data to bytes32: 20 bytes of deposit owner address and /// 2 bytes of referral, 10 bytes of trailing zeros. - /// @param staker The address to which the stBTC shares will be minted. + /// @param depositOwner The address to which the stBTC shares will be minted. /// @param referral Data used for referral program. /// @return Encoded extra data. function encodeExtraData( - address staker, + address depositOwner, uint16 referral ) public pure returns (bytes32) { - return bytes32(abi.encodePacked(staker, referral)); + return bytes32(abi.encodePacked(depositOwner, referral)); } - /// @notice Decodes staker address and referral from extra data. - /// @dev Unpacks the data from bytes32: 20 bytes of staker address and + /// @notice Decodes deposit owner address and referral from extra data. + /// @dev Unpacks the data from bytes32: 20 bytes of deposit owner address and /// 2 bytes of referral, 10 bytes of trailing zeros. /// @param extraData Encoded extra data. - /// @return staker The address to which the stBTC shares will be minted. + /// @return depositOwner The address to which the stBTC shares will be minted. /// @return referral Data used for referral program. function decodeExtraData( bytes32 extraData - ) public pure returns (address staker, uint16 referral) { - // First 20 bytes of extra data is staker address. - staker = address(uint160(bytes20(extraData))); + ) public pure returns (address depositOwner, uint16 referral) { + // First 20 bytes of extra data is deposit owner address. + depositOwner = address(uint160(bytes20(extraData))); // Next 2 bytes of extra data is referral info. referral = uint16(bytes2(extraData << (8 * 20))); } From f73e7296f393f99101ad5704bf5094829220204b Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 9 Apr 2024 17:34:36 +0200 Subject: [PATCH 098/123] Update BitcoinDepositor tests after stake rename --- core/test/BitcoinDepositor.test.ts | 235 +++++++++++---------- core/test/BitcoinDepositor.upgrade.test.ts | 16 +- core/test/data/tbtc.ts | 2 +- core/test/stBTC.test.ts | 10 +- core/test/stBTC.upgrade.test.ts | 18 +- core/types/index.ts | 2 +- 6 files changed, 147 insertions(+), 136 deletions(-) diff --git a/core/test/BitcoinDepositor.test.ts b/core/test/BitcoinDepositor.test.ts index 2c0fd02f7..79b7c82aa 100644 --- a/core/test/BitcoinDepositor.test.ts +++ b/core/test/BitcoinDepositor.test.ts @@ -6,7 +6,7 @@ import { expect } from "chai" import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { ContractTransactionResponse, MaxUint256, ZeroAddress } from "ethers" -import { StakeRequestState } from "../types" +import { DepositState } from "../types" import type { StBTC, @@ -46,7 +46,7 @@ describe("BitcoinDepositor", () => { const initialDepositAmount = to1ePrecision(10000, 10) // 10000 satoshi const bridgedTbtcAmount = to1ePrecision(897501, 8) // 8975,01 satoshi const depositorFee = to1ePrecision(10, 10) // 10 satoshi - const amountToStake = to1ePrecision(896501, 8) // 8965,01 satoshi + const amountToDeposit = to1ePrecision(896501, 8) // 8965,01 satoshi let bitcoinDepositor: BitcoinDepositor let tbtcBridge: BridgeStub @@ -87,24 +87,27 @@ describe("BitcoinDepositor", () => { .updateDepositorFeeDivisor(defaultDepositorFeeDivisor) }) - describe("initializeStake", () => { + describe("initializeDeposit", () => { beforeAfterSnapshotWrapper() - describe("when staker is zero address", () => { + describe("when depositOwner is zero address", () => { it("should revert", async () => { await expect( - bitcoinDepositor.initializeStake( + bitcoinDepositor.initializeDeposit( tbtcDepositData.fundingTxInfo, tbtcDepositData.reveal, ZeroAddress, 0, ), - ).to.be.revertedWithCustomError(bitcoinDepositor, "StakerIsZeroAddress") + ).to.be.revertedWithCustomError( + bitcoinDepositor, + "DepositOwnerIsZeroAddress", + ) }) }) - describe("when staker is non zero address", () => { - describe("when stake is not in progress", () => { + describe("when depositOwner is non zero address", () => { + describe("when deposit is not in progress", () => { describe("when tbtc vault address is incorrect", () => { beforeAfterSnapshotWrapper() @@ -115,10 +118,10 @@ describe("BitcoinDepositor", () => { await expect( bitcoinDepositor .connect(thirdParty) - .initializeStake( + .initializeDeposit( tbtcDepositData.fundingTxInfo, { ...tbtcDepositData.reveal, vault: invalidTbtcVault }, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, tbtcDepositData.referral, ), ).to.be.revertedWith("Vault address mismatch") @@ -134,31 +137,31 @@ describe("BitcoinDepositor", () => { before(async () => { tx = await bitcoinDepositor .connect(thirdParty) - .initializeStake( + .initializeDeposit( tbtcDepositData.fundingTxInfo, tbtcDepositData.reveal, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, tbtcDepositData.referral, ) }) - it("should emit StakeRequestInitialized event", async () => { + it("should emit DepositInitialized event", async () => { await expect(tx) - .to.emit(bitcoinDepositor, "StakeRequestInitialized") + .to.emit(bitcoinDepositor, "DepositInitialized") .withArgs( tbtcDepositData.depositKey, thirdParty.address, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, initialDepositAmount, ) }) - it("should update stake state", async () => { - const stakeRequest = await bitcoinDepositor.stakeRequests( + it("should update deposit state", async () => { + const deposit = await bitcoinDepositor.deposits( tbtcDepositData.depositKey, ) - expect(stakeRequest).to.be.equal(StakeRequestState.Initialized) + expect(deposit).to.be.equal(DepositState.Initialized) }) it("should reveal the deposit to the bridge contract with extra data", async () => { @@ -185,10 +188,10 @@ describe("BitcoinDepositor", () => { await expect( bitcoinDepositor .connect(thirdParty) - .initializeStake( + .initializeDeposit( tbtcDepositData.fundingTxInfo, tbtcDepositData.reveal, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, 0, ), ).to.be.not.reverted @@ -197,49 +200,49 @@ describe("BitcoinDepositor", () => { }) }) - describe("when stake is already in progress", () => { + describe("when deposit is already in progress", () => { beforeAfterSnapshotWrapper() before(async () => { - await initializeStake() + await initializeDeposit() }) it("should revert", async () => { await expect( bitcoinDepositor .connect(thirdParty) - .initializeStake( + .initializeDeposit( tbtcDepositData.fundingTxInfo, tbtcDepositData.reveal, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, tbtcDepositData.referral, ), ).to.be.revertedWith("Deposit already revealed") }) }) - describe("when stake is already finalized", () => { + describe("when deposit is already finalized", () => { beforeAfterSnapshotWrapper() before(async () => { - await initializeStake() + await initializeDeposit() // Simulate deposit request finalization. await finalizeMinting(tbtcDepositData.depositKey) await bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey) + .finalizeDeposit(tbtcDepositData.depositKey) }) it("should revert", async () => { await expect( bitcoinDepositor .connect(thirdParty) - .initializeStake( + .initializeDeposit( tbtcDepositData.fundingTxInfo, tbtcDepositData.reveal, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, tbtcDepositData.referral, ), ).to.be.revertedWith("Deposit already revealed") @@ -248,29 +251,29 @@ describe("BitcoinDepositor", () => { }) }) - describe("finalizeStake", () => { + describe("finalizeDeposit", () => { beforeAfterSnapshotWrapper() - describe("when stake has not been initialized", () => { + describe("when deposit has not been initialized", () => { it("should revert", async () => { await expect( bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey), + .finalizeDeposit(tbtcDepositData.depositKey), ) .to.be.revertedWithCustomError( bitcoinDepositor, - "UnexpectedStakeRequestState", + "UnexpectedDepositState", ) - .withArgs(StakeRequestState.Unknown, StakeRequestState.Initialized) + .withArgs(DepositState.Unknown, DepositState.Initialized) }) }) - describe("when stake has been initialized", () => { + describe("when deposit has been initialized", () => { beforeAfterSnapshotWrapper() before(async () => { - await initializeStake() + await initializeDeposit() }) describe("when deposit was not bridged", () => { @@ -278,13 +281,13 @@ describe("BitcoinDepositor", () => { await expect( bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey), + .finalizeDeposit(tbtcDepositData.depositKey), ).to.be.revertedWith("Deposit not finalized by the bridge") }) }) describe("when deposit was bridged", () => { - describe("when stake has not been finalized", () => { + describe("when deposit has not been finalized", () => { describe("when depositor contract balance is lower than bridged amount", () => { beforeAfterSnapshotWrapper() @@ -300,7 +303,7 @@ describe("BitcoinDepositor", () => { await expect( bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey), + .finalizeDeposit(tbtcDepositData.depositKey), ) .to.be.revertedWithCustomError( stbtc, @@ -309,7 +312,7 @@ describe("BitcoinDepositor", () => { .withArgs( await bitcoinDepositor.getAddress(), mintedAmount - depositorFee, - amountToStake, + amountToDeposit, ) }) }) @@ -325,15 +328,15 @@ describe("BitcoinDepositor", () => { describe("when depositor fee divisor is not zero", () => { beforeAfterSnapshotWrapper() - const expectedAssetsAmount = amountToStake - const expectedReceivedSharesAmount = amountToStake + const expectedAssetsAmount = amountToDeposit + const expectedReceivedSharesAmount = amountToDeposit let tx: ContractTransactionResponse before(async () => { tx = await bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey) + .finalizeDeposit(tbtcDepositData.depositKey) }) it("should transfer depositor fee", async () => { @@ -344,17 +347,17 @@ describe("BitcoinDepositor", () => { ) }) - it("should update stake state", async () => { - const stakeRequest = await bitcoinDepositor.stakeRequests( + it("should update deposit state", async () => { + const depositState = await bitcoinDepositor.deposits( tbtcDepositData.depositKey, ) - expect(stakeRequest).to.be.equal(StakeRequestState.Finalized) + expect(depositState).to.be.equal(DepositState.Finalized) }) - it("should emit StakeRequestFinalized event", async () => { + it("should emit DepositFinalized event", async () => { await expect(tx) - .to.emit(bitcoinDepositor, "StakeRequestFinalized") + .to.emit(bitcoinDepositor, "DepositFinalized") .withArgs( tbtcDepositData.depositKey, thirdParty.address, @@ -370,25 +373,25 @@ describe("BitcoinDepositor", () => { .to.emit(stbtc, "Deposit") .withArgs( await bitcoinDepositor.getAddress(), - tbtcDepositData.staker, + tbtcDepositData.depositOwner, expectedAssetsAmount, expectedReceivedSharesAmount, ) }) - it("should stake in Acre contract", async () => { + it("should deposit in Acre contract", async () => { await expect( tx, "invalid minted stBTC amount", ).to.changeTokenBalances( stbtc, - [tbtcDepositData.staker], + [tbtcDepositData.depositOwner], [expectedReceivedSharesAmount], ) await expect( tx, - "invalid staked tBTC amount", + "invalid deposited tBTC amount", ).to.changeTokenBalances(tbtc, [stbtc], [expectedAssetsAmount]) }) }) @@ -396,8 +399,9 @@ describe("BitcoinDepositor", () => { describe("when depositor fee divisor is zero", () => { beforeAfterSnapshotWrapper() - const expectedAssetsAmount = amountToStake + depositorFee - const expectedReceivedSharesAmount = amountToStake + depositorFee + const expectedAssetsAmount = amountToDeposit + depositorFee + const expectedReceivedSharesAmount = + amountToDeposit + depositorFee let tx: ContractTransactionResponse @@ -408,24 +412,24 @@ describe("BitcoinDepositor", () => { tx = await bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey) + .finalizeDeposit(tbtcDepositData.depositKey) }) it("should not transfer depositor fee", async () => { await expect(tx).to.changeTokenBalances(tbtc, [treasury], [0]) }) - it("should update stake state", async () => { - const stakeRequest = await bitcoinDepositor.stakeRequests( + it("should update deposit state", async () => { + const deposit = await bitcoinDepositor.deposits( tbtcDepositData.depositKey, ) - expect(stakeRequest).to.be.equal(StakeRequestState.Finalized) + expect(deposit).to.be.equal(DepositState.Finalized) }) - it("should emit StakeRequestFinalized event", async () => { + it("should emit DepositFinalized event", async () => { await expect(tx) - .to.emit(bitcoinDepositor, "StakeRequestFinalized") + .to.emit(bitcoinDepositor, "DepositFinalized") .withArgs( tbtcDepositData.depositKey, thirdParty.address, @@ -441,25 +445,25 @@ describe("BitcoinDepositor", () => { .to.emit(stbtc, "Deposit") .withArgs( await bitcoinDepositor.getAddress(), - tbtcDepositData.staker, + tbtcDepositData.depositOwner, expectedAssetsAmount, expectedReceivedSharesAmount, ) }) - it("should stake in Acre contract", async () => { + it("should deposit in Acre contract", async () => { await expect( tx, "invalid minted stBTC amount", ).to.changeTokenBalances( stbtc, - [tbtcDepositData.staker], + [tbtcDepositData.depositOwner], [expectedReceivedSharesAmount], ) await expect( tx, - "invalid staked tBTC amount", + "invalid deposited tBTC amount", ).to.changeTokenBalances(tbtc, [stbtc], [expectedAssetsAmount]) }) }) @@ -477,7 +481,7 @@ describe("BitcoinDepositor", () => { await expect( bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey), + .finalizeDeposit(tbtcDepositData.depositKey), ) .to.be.revertedWithCustomError( bitcoinDepositor, @@ -489,40 +493,37 @@ describe("BitcoinDepositor", () => { }) }) - describe("when stake has been finalized", () => { + describe("when deposit has been finalized", () => { beforeAfterSnapshotWrapper() before(async () => { // Simulate deposit request finalization. await finalizeMinting(tbtcDepositData.depositKey) - // Finalize stake. + // Finalize deposit. await bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey) + .finalizeDeposit(tbtcDepositData.depositKey) }) it("should revert", async () => { await expect( bitcoinDepositor .connect(thirdParty) - .finalizeStake(tbtcDepositData.depositKey), + .finalizeDeposit(tbtcDepositData.depositKey), ) .to.be.revertedWithCustomError( bitcoinDepositor, - "UnexpectedStakeRequestState", - ) - .withArgs( - StakeRequestState.Finalized, - StakeRequestState.Initialized, + "UnexpectedDepositState", ) + .withArgs(DepositState.Finalized, DepositState.Initialized) }) }) }) }) }) - describe("updateMinStakeAmount", () => { + describe("updateMinDepositAmount", () => { beforeAfterSnapshotWrapper() describe("when caller is not governance", () => { @@ -530,7 +531,7 @@ describe("BitcoinDepositor", () => { it("should revert", async () => { await expect( - bitcoinDepositor.connect(thirdParty).updateMinStakeAmount(1234), + bitcoinDepositor.connect(thirdParty).updateMinDepositAmount(1234), ) .to.be.revertedWithCustomError( bitcoinDepositor, @@ -541,7 +542,7 @@ describe("BitcoinDepositor", () => { }) describe("when caller is governance", () => { - const testUpdateMinStakeAmount = (newValue: bigint) => + const testupdateMinDepositAmount = (newValue: bigint) => function () { beforeAfterSnapshotWrapper() @@ -550,17 +551,17 @@ describe("BitcoinDepositor", () => { before(async () => { tx = await bitcoinDepositor .connect(governance) - .updateMinStakeAmount(newValue) + .updateMinDepositAmount(newValue) }) - it("should emit MinStakeAmountUpdated event", async () => { + it("should emit MinDepositAmountUpdated event", async () => { await expect(tx) - .to.emit(bitcoinDepositor, "MinStakeAmountUpdated") + .to.emit(bitcoinDepositor, "MinDepositAmountUpdated") .withArgs(newValue) }) it("should update value correctly", async () => { - expect(await bitcoinDepositor.minStakeAmount()).to.be.eq(newValue) + expect(await bitcoinDepositor.minDepositAmount()).to.be.eq(newValue) }) } @@ -568,44 +569,44 @@ describe("BitcoinDepositor", () => { // Deposit dust threshold: 1000000 satoshi = 0.01 BTC // tBTC Bridge stores the dust threshold in satoshi precision, - // we need to convert it to the tBTC token precision as `updateMinStakeAmount` + // we need to convert it to the tBTC token precision as `updateMinDepositAmount` // function expects this precision. const bridgeDepositDustThreshold = to1ePrecision( defaultDepositDustThreshold, 10, ) - describe("when new stake amount is less than bridge deposit dust threshold", () => { + describe("when new deposit amount is less than bridge deposit dust threshold", () => { beforeAfterSnapshotWrapper() - const newMinStakeAmount = bridgeDepositDustThreshold - 1n + const newMinDepositAmount = bridgeDepositDustThreshold - 1n it("should revert", async () => { await expect( bitcoinDepositor .connect(governance) - .updateMinStakeAmount(newMinStakeAmount), + .updateMinDepositAmount(newMinDepositAmount), ) .to.be.revertedWithCustomError( bitcoinDepositor, - "MinStakeAmountLowerThanBridgeMinDeposit", + "MinDepositAmountLowerThanBridgeMinDeposit", ) - .withArgs(newMinStakeAmount, bridgeDepositDustThreshold) + .withArgs(newMinDepositAmount, bridgeDepositDustThreshold) }) }) describe( - "when new stake amount is equal to bridge deposit dust threshold", - testUpdateMinStakeAmount(bridgeDepositDustThreshold), + "when new deposit amount is equal to bridge deposit dust threshold", + testupdateMinDepositAmount(bridgeDepositDustThreshold), ) describe( - "when new stake amount is greater than bridge deposit dust threshold", - testUpdateMinStakeAmount(bridgeDepositDustThreshold + 1n), + "when new deposit amount is greater than bridge deposit dust threshold", + testupdateMinDepositAmount(bridgeDepositDustThreshold + 1n), ) describe( - "when new stake amount is equal max uint256", - testUpdateMinStakeAmount(MaxUint256), + "when new deposit amount is equal max uint256", + testupdateMinDepositAmount(MaxUint256), ) }) }) @@ -669,24 +670,24 @@ describe("BitcoinDepositor", () => { const extraDataValidTestData = new Map< string, { - staker: string + depositOwner: string referral: number extraData: string } >([ [ - "staker has leading zeros", + "depositOwner has leading zeros", { - staker: "0x000055d85E80A49B5930C4a77975d44f012D86C1", + depositOwner: "0x000055d85E80A49B5930C4a77975d44f012D86C1", referral: 6851, // hex: 0x1ac3 extraData: "0x000055d85e80a49b5930c4a77975d44f012d86c11ac300000000000000000000", }, ], [ - "staker has trailing zeros", + "depositOwner has trailing zeros", { - staker: "0x2d2F8BC7923F7F806Dc9bb2e17F950b42CfE0000", + depositOwner: "0x2d2F8BC7923F7F806Dc9bb2e17F950b42CfE0000", referral: 6851, // hex: 0x1ac3 extraData: "0x2d2f8bc7923f7f806dc9bb2e17f950b42cfe00001ac300000000000000000000", @@ -695,7 +696,7 @@ describe("BitcoinDepositor", () => { [ "referral is zero", { - staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + depositOwner: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", referral: 0, extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e000000000000000000000000", @@ -704,7 +705,7 @@ describe("BitcoinDepositor", () => { [ "referral has leading zeros", { - staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + depositOwner: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", referral: 31, // hex: 0x001f extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e001f00000000000000000000", @@ -713,7 +714,7 @@ describe("BitcoinDepositor", () => { [ "referral has trailing zeros", { - staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + depositOwner: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", referral: 19712, // hex: 0x4d00 extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e4d0000000000000000000000", @@ -722,7 +723,7 @@ describe("BitcoinDepositor", () => { [ "referral is maximum value", { - staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + depositOwner: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", referral: 65535, // max uint16 extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89effff00000000000000000000", @@ -733,10 +734,10 @@ describe("BitcoinDepositor", () => { describe("encodeExtraData", () => { extraDataValidTestData.forEach( // eslint-disable-next-line @typescript-eslint/no-shadow - ({ staker, referral, extraData: expectedExtraData }, testName) => { + ({ depositOwner, referral, extraData: expectedExtraData }, testName) => { it(testName, async () => { expect( - await bitcoinDepositor.encodeExtraData(staker, referral), + await bitcoinDepositor.encodeExtraData(depositOwner, referral), ).to.be.equal(expectedExtraData) }) }, @@ -746,14 +747,20 @@ describe("BitcoinDepositor", () => { describe("decodeExtraData", () => { extraDataValidTestData.forEach( ( - { staker: expectedStaker, referral: expectedReferral, extraData }, + { + depositOwner: expoectedDepositOwner, + referral: expectedReferral, + extraData, + }, testName, ) => { it(testName, async () => { - const [actualStaker, actualReferral] = + const [actualDepositOwner, actualReferral] = await bitcoinDepositor.decodeExtraData(extraData) - expect(actualStaker, "invalid staker").to.be.equal(expectedStaker) + expect(actualDepositOwner, "invalid depositOwner").to.be.equal( + expoectedDepositOwner, + ) expect(actualReferral, "invalid referral").to.be.equal( expectedReferral, ) @@ -767,24 +774,26 @@ describe("BitcoinDepositor", () => { // value. const extraData = "0xeb098d6cde6a202981316b24b19e64d82721e89e1ac3105f9919321ea7d75f58" - const expectedStaker = "0xeb098d6cDE6A202981316b24B19e64D82721e89E" + const expectedDepositOwner = "0xeb098d6cDE6A202981316b24B19e64D82721e89E" const expectedReferral = 6851 // hex: 0x1ac3 - const [actualStaker, actualReferral] = + const [actualDepositOwner, actualReferral] = await bitcoinDepositor.decodeExtraData(extraData) - expect(actualStaker, "invalid staker").to.be.equal(expectedStaker) + expect(actualDepositOwner, "invalid depositOwner").to.be.equal( + expectedDepositOwner, + ) expect(actualReferral, "invalid referral").to.be.equal(expectedReferral) }) }) - async function initializeStake() { + async function initializeDeposit() { await bitcoinDepositor .connect(thirdParty) - .initializeStake( + .initializeDeposit( tbtcDepositData.fundingTxInfo, tbtcDepositData.reveal, - tbtcDepositData.staker, + tbtcDepositData.depositOwner, tbtcDepositData.referral, ) } diff --git a/core/test/BitcoinDepositor.upgrade.test.ts b/core/test/BitcoinDepositor.upgrade.test.ts index 46ea0155c..6194712d5 100644 --- a/core/test/BitcoinDepositor.upgrade.test.ts +++ b/core/test/BitcoinDepositor.upgrade.test.ts @@ -42,19 +42,19 @@ describe("BitcoinDepositor contract upgrade", () => { const newVariable = 1n let bitcoinDepositorV2: BitcoinDepositorV2 let v1InitialParameters: { - minStakeAmount: bigint + minDepositAmount: bigint depositorFeeDivisor: bigint } beforeAfterSnapshotWrapper() before(async () => { - const minStakeAmount = await bitcoinDepositor.minStakeAmount() + const minDepositAmount = await bitcoinDepositor.minDepositAmount() const depositorFeeDivisor = await bitcoinDepositor.depositorFeeDivisor() v1InitialParameters = { - minStakeAmount, + minDepositAmount, depositorFeeDivisor, } @@ -98,8 +98,8 @@ describe("BitcoinDepositor contract upgrade", () => { ) expect(await bitcoinDepositorV2.stbtc()).to.eq(await stbtc.getAddress()) - expect(await bitcoinDepositorV2.minStakeAmount()).to.eq( - v1InitialParameters.minStakeAmount, + expect(await bitcoinDepositorV2.minDepositAmount()).to.eq( + v1InitialParameters.minDepositAmount, ) expect(await bitcoinDepositorV2.depositorFeeDivisor()).to.eq( v1InitialParameters.depositorFeeDivisor, @@ -107,14 +107,14 @@ describe("BitcoinDepositor contract upgrade", () => { }) }) - describe("upgraded `updateMinStakeAmount` function", () => { - const newMinStakeAmount: bigint = to1e18(1000) + describe("upgraded `updateMinDepositAmount` function", () => { + const newMinDepositAmount: bigint = to1e18(1000) let tx: ContractTransactionResponse before(async () => { tx = await bitcoinDepositorV2 .connect(governance) - .updateMinStakeAmount(newMinStakeAmount) + .updateMinDepositAmount(newMinDepositAmount) }) it("should emit `NewEvent` event", async () => { diff --git a/core/test/data/tbtc.ts b/core/test/data/tbtc.ts index fcf97c50d..923635e4f 100644 --- a/core/test/data/tbtc.ts +++ b/core/test/data/tbtc.ts @@ -35,7 +35,7 @@ export const tbtcDepositData = { vault: "0x594cfd89700040163727828AE20B52099C58F02C", }, // 20-bytes of extraData - staker: "0xa9B38eA6435c8941d6eDa6a46b68E3e211719699", + depositOwner: "0xa9B38eA6435c8941d6eDa6a46b68E3e211719699", // 2-bytes of extraData referral: "0x5bd1", extraData: diff --git a/core/test/stBTC.test.ts b/core/test/stBTC.test.ts index 4f5bec95a..729cdcafe 100644 --- a/core/test/stBTC.test.ts +++ b/core/test/stBTC.test.ts @@ -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], diff --git a/core/test/stBTC.upgrade.test.ts b/core/test/stBTC.upgrade.test.ts index 3bf77cbc1..f22c3f997 100644 --- a/core/test/stBTC.upgrade.test.ts +++ b/core/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/types/index.ts b/core/types/index.ts index e84cb597b..19e2cf0f5 100644 --- a/core/types/index.ts +++ b/core/types/index.ts @@ -1,5 +1,5 @@ /* eslint-disable import/prefer-default-export */ -export enum StakeRequestState { +export enum DepositState { Unknown, Initialized, Finalized, From b4552b1a8fa4410b108940e22c257566447705a5 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 9 Apr 2024 17:42:29 +0200 Subject: [PATCH 099/123] Update stake mentions in docs --- core/contracts/BitcoinRedeemer.sol | 2 +- core/contracts/Dispatcher.sol | 2 +- core/contracts/stBTC.sol | 7 +++---- core/contracts/test/upgrades/stBTCV2.sol | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/core/contracts/BitcoinRedeemer.sol b/core/contracts/BitcoinRedeemer.sol index b3c51ba18..159a81dfd 100644 --- a/core/contracts/BitcoinRedeemer.sol +++ b/core/contracts/BitcoinRedeemer.sol @@ -138,7 +138,7 @@ contract BitcoinRedeemer is Ownable2StepUpgradeable, IReceiveApproval { /// setup. This contract remains upgradable to have flexibility to handle /// adjustments to tBTC Bridge changes. /// @dev Redemption data should include a `redeemer` address matching the - /// address of the staker who is redeeming the shares. In case anything + /// address of the deposit owner who is redeeming the shares. In case anything /// goes wrong during the tBTC unminting process, the redeemer will be /// able to claim the tBTC tokens back from the tBTC Bank contract. /// @param owner The owner of the stBTC tokens. diff --git a/core/contracts/Dispatcher.sol b/core/contracts/Dispatcher.sol index ec1696239..075edadc2 100644 --- a/core/contracts/Dispatcher.sol +++ b/core/contracts/Dispatcher.sol @@ -19,7 +19,7 @@ contract Dispatcher is Router, Ownable2Step { bool authorized; } - /// The main stBTC contract holding tBTC deposited by stakers. + /// The main stBTC contract holding tBTC deposits. stBTC public immutable stbtc; /// tBTC token contract. IERC20 public immutable tbtc; diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 857ff19a4..17a78ce62 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -13,10 +13,9 @@ 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 @@ -126,7 +125,7 @@ contract stBTC 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 { if (address(newDispatcher) == address(0)) { diff --git a/core/contracts/test/upgrades/stBTCV2.sol b/core/contracts/test/upgrades/stBTCV2.sol index 85c29e96e..2ae996baf 100644 --- a/core/contracts/test/upgrades/stBTCV2.sol +++ b/core/contracts/test/upgrades/stBTCV2.sol @@ -117,7 +117,7 @@ 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 { if (address(newDispatcher) == address(0)) { From 4e813640c57dbe97f1f0e3365d21622661fe025a Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 9 Apr 2024 17:52:59 +0200 Subject: [PATCH 100/123] Update SDK to to replace staker with depositOwner --- dapp/src/web3/relayer-depositor-proxy.ts | 4 +-- sdk/src/lib/contracts/bitcoin-depositor.ts | 10 +++--- sdk/src/lib/ethereum/bitcoin-depositor.ts | 26 ++++++++------- .../modules/staking/stake-initialization.ts | 1 + sdk/test/lib/ethereum/data.ts | 30 +++++++++++------ sdk/test/lib/ethereum/tbtc-depositor.test.ts | 32 +++++++++++-------- 6 files changed, 61 insertions(+), 42 deletions(-) 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/sdk/src/lib/contracts/bitcoin-depositor.ts b/sdk/src/lib/contracts/bitcoin-depositor.ts index 749f78b73..f221f39ce 100644 --- a/sdk/src/lib/contracts/bitcoin-depositor.ts +++ b/sdk/src/lib/contracts/bitcoin-depositor.ts @@ -5,7 +5,7 @@ import { DepositorProxy } from "./depositor-proxy" export { DepositReceipt } from "@keep-network/tbtc-v2.ts" export type DecodedExtraData = { - staker: ChainIdentifier + depositOwner: ChainIdentifier referral: number } @@ -24,14 +24,14 @@ export interface BitcoinDepositor extends DepositorProxy { getTbtcVaultChainIdentifier(): Promise /** - * Encodes staker address and referral as extra data. - * @param staker The address to which the stBTC shares will be minted. + * Encodes deposit owner address and referral as extra data. + * @param depositOwner The address to which the stBTC shares will be minted. * @param referral Data used for referral program. */ - encodeExtraData(staker: ChainIdentifier, referral: number): Hex + encodeExtraData(depositOwner: ChainIdentifier, referral: number): Hex /** - * Decodes staker address and referral from extra data. + * Decodes depositOwner address and referral from extra data. * @param extraData Encoded extra data. */ decodeExtraData(extraData: string): DecodedExtraData diff --git a/sdk/src/lib/ethereum/bitcoin-depositor.ts b/sdk/src/lib/ethereum/bitcoin-depositor.ts index d968ee5b6..60e868680 100644 --- a/sdk/src/lib/ethereum/bitcoin-depositor.ts +++ b/sdk/src/lib/ethereum/bitcoin-depositor.ts @@ -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/modules/staking/stake-initialization.ts b/sdk/src/modules/staking/stake-initialization.ts index 869de4452..9ed9b31a5 100644 --- a/sdk/src/modules/staking/stake-initialization.ts +++ b/sdk/src/modules/staking/stake-initialization.ts @@ -27,6 +27,7 @@ type StakeOptions = { backoffStepMs: BackoffRetrierParameters[1] } +// TODO: Rename to `DepositInitialization` to be consistent with the naming. /** * Represents an instance of the staking flow. Staking flow requires a few steps * which should be done to stake BTC. diff --git a/sdk/test/lib/ethereum/data.ts b/sdk/test/lib/ethereum/data.ts index 597364a21..b4deab283 100644 --- a/sdk/test/lib/ethereum/data.ts +++ b/sdk/test/lib/ethereum/data.ts @@ -3,48 +3,60 @@ import { EthereumAddress } from "../../../src" // eslint-disable-next-line import/prefer-default-export export const extraDataValidTestData: { testDescription: string - staker: EthereumAddress + depositOwner: EthereumAddress referral: number extraData: string }[] = [ { - testDescription: "staker has leading zeros", - staker: EthereumAddress.from("0x000055d85E80A49B5930C4a77975d44f012D86C1"), + testDescription: "depositOwner has leading zeros", + depositOwner: EthereumAddress.from( + "0x000055d85E80A49B5930C4a77975d44f012D86C1", + ), referral: 6851, // hex: 0x1ac3 extraData: "0x000055d85e80a49b5930c4a77975d44f012d86c11ac300000000000000000000", }, { - testDescription: "staker has trailing zeros", - staker: EthereumAddress.from("0x2d2F8BC7923F7F806Dc9bb2e17F950b42CfE0000"), + testDescription: "depositOwner has trailing zeros", + depositOwner: EthereumAddress.from( + "0x2d2F8BC7923F7F806Dc9bb2e17F950b42CfE0000", + ), referral: 6851, // hex: 0x1ac3 extraData: "0x2d2f8bc7923f7f806dc9bb2e17f950b42cfe00001ac300000000000000000000", }, { testDescription: "referral is zero", - staker: EthereumAddress.from("0xeb098d6cDE6A202981316b24B19e64D82721e89E"), + depositOwner: EthereumAddress.from( + "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + ), referral: 0, extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e000000000000000000000000", }, { testDescription: "referral has leading zeros", - staker: EthereumAddress.from("0xeb098d6cDE6A202981316b24B19e64D82721e89E"), + depositOwner: EthereumAddress.from( + "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + ), referral: 31, // hex: 0x001f extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e001f00000000000000000000", }, { testDescription: "referral has trailing zeros", - staker: EthereumAddress.from("0xeb098d6cDE6A202981316b24B19e64D82721e89E"), + depositOwner: EthereumAddress.from( + "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + ), referral: 19712, // hex: 0x4d00 extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89e4d0000000000000000000000", }, { testDescription: "referral is maximum value", - staker: EthereumAddress.from("0xeb098d6cDE6A202981316b24B19e64D82721e89E"), + depositOwner: EthereumAddress.from( + "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + ), referral: 65535, // max uint16 extraData: "0xeb098d6cde6a202981316b24b19e64d82721e89effff00000000000000000000", diff --git a/sdk/test/lib/ethereum/tbtc-depositor.test.ts b/sdk/test/lib/ethereum/tbtc-depositor.test.ts index 7368af30a..5941d0442 100644 --- a/sdk/test/lib/ethereum/tbtc-depositor.test.ts +++ b/sdk/test/lib/ethereum/tbtc-depositor.test.ts @@ -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", @@ -157,7 +157,7 @@ describe("BitcoinDepositor", () => { expect(mockedContractInstance.initializeStake).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) }, ) From 8fc4ab974e4e27d03fb1a6d7da034110a5b16c92 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 9 Apr 2024 17:54:37 +0200 Subject: [PATCH 101/123] Remove TODOs for govern parameters There is no point introducing a govern upgrade process as the contracts are upgradable anyway. --- core/contracts/PausableOwnable.sol | 1 - core/contracts/stBTC.sol | 8 -------- 2 files changed, 9 deletions(-) diff --git a/core/contracts/PausableOwnable.sol b/core/contracts/PausableOwnable.sol index 257b9d222..7e888475b 100644 --- a/core/contracts/PausableOwnable.sol +++ b/core/contracts/PausableOwnable.sol @@ -92,7 +92,6 @@ abstract contract PausableOwnable is /// @param newPauseAdmin New account that can trigger emergency /// stop mechanism. function updatePauseAdmin(address newPauseAdmin) external onlyOwner { - // TODO: Introduce a parameters update process. if (newPauseAdmin == address(0)) { revert ZeroAddress(); } diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 17a78ce62..004fd06f9 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -97,7 +97,6 @@ contract stBTC is ERC4626Fees, PausableOwnable { /// @notice Updates treasury wallet address. /// @param newTreasury New treasury wallet address. function updateTreasury(address newTreasury) external onlyOwner { - // TODO: Introduce a parameters update process. if (newTreasury == address(0)) { revert ZeroAddress(); } @@ -116,14 +115,11 @@ 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 deposited tBTC. /// @param newDispatcher Address of the new dispatcher contract. @@ -150,8 +146,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( @@ -162,8 +156,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( From 89feb846d13af65adcf21a7f87e1cf2f2b5e7876 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 9 Apr 2024 17:55:36 +0200 Subject: [PATCH 102/123] Remove TODO from updateDispatcher We no longer expect the vaults and tokens to be owned by the dispatcher. --- core/contracts/stBTC.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 004fd06f9..d456735a0 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -133,10 +133,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); From c6acdcea3ba13ac382fa094960d31071be4cf46a Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 9 Apr 2024 18:02:41 +0200 Subject: [PATCH 103/123] Fix BitcoinDepositor tests in SDK --- sdk/test/lib/ethereum/tbtc-depositor.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/test/lib/ethereum/tbtc-depositor.test.ts b/sdk/test/lib/ethereum/tbtc-depositor.test.ts index 5941d0442..11aab167c 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 @@ -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,7 +154,7 @@ describe("BitcoinDepositor", () => { vault: `0x${vaultAddress.identifierHex}`, } - expect(mockedContractInstance.initializeStake).toHaveBeenCalledWith( + expect(mockedContractInstance.initializeDeposit).toHaveBeenCalledWith( btcTxInfo, revealInfo, `0x${depositOwner.identifierHex}`, From 7a0962d4704bdf7e2d1eb503923dda684eda559d Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 9 Apr 2024 18:10:38 +0200 Subject: [PATCH 104/123] Udpate MezoAllocator test upgrade contract --- .../test/upgrades/MezoAllocatorV2.sol | 252 +++++++++++------- core/test/MezoAllocator.upgrade.test.ts | 6 +- 2 files changed, 153 insertions(+), 105 deletions(-) diff --git a/core/contracts/test/upgrades/MezoAllocatorV2.sol b/core/contracts/test/upgrades/MezoAllocatorV2.sol index e5e862e07..d809cf194 100644 --- a/core/contracts/test/upgrades/MezoAllocatorV2.sol +++ b/core/contracts/test/upgrades/MezoAllocatorV2.sol @@ -4,69 +4,107 @@ 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); } -/// @dev This is a contract used to test stBTC upgradeability. It is a copy of -/// stBTC contract with some differences marked with `TEST:` comments. -contract MezoAllocatorV2 is Ownable2StepUpgradeable { +/// @notice MezoAllocator routes tBTC to/from MezoPortal. +contract MezoAllocatorV2 is IDispatcher, Ownable2StepUpgradeable { using SafeERC20 for IERC20; - /// @notice DepositInfo keeps track of the deposit Id, deposit balance, - /// creation time, and unlock time. - struct DepositInfo { - uint256 id; - uint96 balance; - uint32 createdAt; - uint32 unlockAt; - } - - /// Address of the MezoPortal contract. - address public mezoPortal; - /// tBTC token contract. + /// @notice Address of the MezoPortal contract. + IMezoPortal public mezoPortal; + /// @notice tBTC token contract. IERC20 public tbtc; - /// Contract holding tBTC deposited by stakers. - address public tbtcStorage; - - /// @notice Maintainer address which can trigger deposit flow. - address public maintainer; - - /// @notice keeps track of the deposit info. - DepositInfo public depositInfo; + /// @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; - /// Emitted when tBTC is deposited to MezoPortal. + /// @notice Emitted when tBTC is deposited to MezoPortal. event DepositAllocated( uint256 indexed oldDepositId, uint256 indexed newDepositId, - uint256 amount + uint256 addedAmount, + uint256 newDepositAmount ); - - /// @notice Emitted when the tBTC storage address is updated. - event TbtcStorageUpdated(address indexed tbtcStorage); - + /// @notice Emitted when tBTC is withdrawn from MezoPortal. + event DepositWithdrawn(uint256 indexed depositId, uint256 amount); /// @notice Emitted when the maintainer address is updated. - event MaintainerUpdated(address indexed maintainer); - + event MaintainerAdded(address indexed maintainer); + /// @notice Emitted when the maintainer address is updated. + event MaintainerRemoved(address indexed maintainer); // TEST: New event. event NewEvent(); - /// @notice Reverts if the caller is not an authorized account. error NotAuthorized(); + /// @notice Reverts if the caller is not a maintainer. + error MaintainerNotRegistered(); + /// @notice Reverts if the caller is already a maintainer. + error MaintainerAlreadyRegistered(); - /// @notice Reverts if the address is 0. - error ZeroAddress(); - - modifier onlyMaintainerAndOwner() { - if (msg.sender != maintainer && owner() != msg.sender) { + modifier onlyMaintainer() { + if (!isMaintainer[msg.sender]) { revert NotAuthorized(); } _; @@ -80,7 +118,11 @@ contract MezoAllocatorV2 is Ownable2StepUpgradeable { /// @notice Initializes the MezoAllocator contract. /// @param _mezoPortal Address of the MezoPortal contract. /// @param _tbtc Address of the tBTC token contract. - function initialize(address _mezoPortal, IERC20 _tbtc) public initializer { + 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. @@ -95,88 +137,94 @@ contract MezoAllocatorV2 is Ownable2StepUpgradeable { /// 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 used. - /// @dev This function can be invoked periodically by a bot. - /// @param amount Amount of tBTC to deposit to Mezo Portal. - function allocate(uint96 amount) external onlyMaintainerAndOwner { - // Free all Acre's tBTC from MezoPortal before creating a new deposit. - free(); - // slither-disable-next-line arbitrary-send-erc20 - IERC20(tbtc).safeTransferFrom(tbtcStorage, address(this), amount); - - // Add freed tBTC from the previous deposit and add the new amount. - uint96 newBalance = depositInfo.balance + amount; + /// 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); + } - IERC20(tbtc).forceApprove(mezoPortal, newBalance); - // 0 denotes no lock period for this deposit. The zero lock time is - // hardcoded as of biz decision. - IMezoPortal(mezoPortal).deposit(address(tbtc), newBalance, 0); + // 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 assignes depositId to the current deposit. - uint256 newDepositId = IMezoPortal(mezoPortal).depositCount(); - // slither-disable-next-line reentrancy-benign - uint256 oldDepositId = depositInfo.id; - depositInfo.id = newDepositId; - depositInfo.balance = newBalance; - // solhint-disable-next-line not-rely-on-time - depositInfo.createdAt = uint32(block.timestamp); - // solhint-disable-next-line not-rely-on-time - depositInfo.unlockAt = uint32(block.timestamp); + // which assigns depositId to the current deposit. + depositId = mezoPortal.depositCount(); // slither-disable-next-line reentrancy-events - emit DepositAllocated(oldDepositId, newDepositId, amount); + emit DepositAllocated( + oldDepositId, + depositId, + addedAmount, + depositBalance + ); } - /// @notice Updates the tBTC storage address. - /// @dev At first this is going to be the stBTC contract. Once Acre - /// works with more destinations for tBTC, this will be updated to - /// the new storage contract like AcreDispatcher. - /// @param _tbtcStorage Address of the new tBTC storage. + /// @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 NotAuthorized(); + + 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 Updates the maintainer address. + /// @param maintainerToAdd Address of the new maintainer. // TEST: Modified function. - function updateTbtcStorage(address _tbtcStorage) external onlyOwner { - if (_tbtcStorage == address(0)) { + function addMaintainer(address maintainerToAdd) external onlyOwner { + if (maintainerToAdd == address(0)) { revert ZeroAddress(); } - tbtcStorage = _tbtcStorage; + if (isMaintainer[maintainerToAdd]) { + revert MaintainerAlreadyRegistered(); + } + maintainers.push(maintainerToAdd); + isMaintainer[maintainerToAdd] = true; - emit TbtcStorageUpdated(_tbtcStorage); + emit MaintainerAdded(maintainerToAdd); // TEST: Emit new event. emit NewEvent(); } - /// @notice Updates the maintainer address. - /// @param _maintainer Address of the new maintainer. - function updateMaintainer(address _maintainer) external onlyOwner { - if (_maintainer == address(0)) { - revert ZeroAddress(); + /// @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; + } } - maintainer = _maintainer; - - emit MaintainerUpdated(_maintainer); - } - // TODO: add updatable withdrawer and onlyWithdrawer modifier (stBTC or AcreDispatcher). - /// @notice Withdraws tBTC from MezoPortal and transfers it to stBTC. - function withdraw(uint96 amount) external { - // TODO: Take the last deposit and pull the funds from it (FIFO). - // If not enough funds, take everything from that deposit and - // take the rest from the next deposit id until the amount is - // reached. Delete deposit ids that are empty. - // IMezoPortal(mezoPortal).withdraw(address(tbtc), depositId, amount); - // TODO: update depositsById and deposits data structures. - // IERC20(tbtc).safeTransfer(address(tbtcStorage), amount); + emit MaintainerRemoved(maintainerToRemove); } - /// @notice Withdraw all Acre's tBTC from MezoPortal. - function free() private { - if (depositInfo.balance > 0) { - // slither-disable-next-line reentrancy-no-eth - IMezoPortal(mezoPortal).withdraw( - address(tbtc), - depositInfo.id, - depositInfo.balance - ); - } + /// @notice Returns the total amount of tBTC allocated to MezoPortal. + function totalAssets() external view returns (uint256 totalAmount) { + return depositBalance; } } diff --git a/core/test/MezoAllocator.upgrade.test.ts b/core/test/MezoAllocator.upgrade.test.ts index 4c891e127..b547df11e 100644 --- a/core/test/MezoAllocator.upgrade.test.ts +++ b/core/test/MezoAllocator.upgrade.test.ts @@ -70,17 +70,17 @@ describe("MezoAllocator contract upgrade", () => { await mezoPortal.getAddress(), ) expect(await allocatorV2.tbtc()).to.eq(await tbtc.getAddress()) - expect(await allocatorV2.tbtcStorage()).to.eq(await stbtc.getAddress()) + expect(await allocatorV2.stbtc()).to.eq(await stbtc.getAddress()) }) }) - describe("upgraded `updateTbtcStorage` function", () => { + describe("upgraded `addMaintainer` function", () => { let tx: ContractTransactionResponse before(async () => { const newAddress = await ethers.Wallet.createRandom().getAddress() - tx = await allocatorV2.connect(governance).updateTbtcStorage(newAddress) + tx = await allocatorV2.connect(governance).addMaintainer(newAddress) }) it("should emit `NewEvent` event", async () => { From ca0891ba44c8d162da60315e5131fbcb05d2fbea Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 10 Apr 2024 00:15:03 +0200 Subject: [PATCH 105/123] Adding more tests for Mezo Allocator --- core/contracts/MezoAllocator.sol | 5 + core/test/MezoAllocator.test.ts | 248 ++++++++++++++++++++++++++++++- 2 files changed, 249 insertions(+), 4 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index ebc46b677..edf078132 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -217,4 +217,9 @@ contract MezoAllocator is IDispatcher, Ownable2Step { function totalAssets() external view returns (uint256 totalAmount) { return depositBalance; } + + /// @notice Returns the list of maintainers. + function getMaintainers() external view returns (address[] memory) { + return maintainers; + } } diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index e12c90eeb..33e2057d5 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -3,7 +3,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { expect } from "chai" import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" -import { ContractTransactionResponse } from "ethers" +import { ContractTransactionResponse, ZeroAddress } from "ethers" import { beforeAfterSnapshotWrapper, deployment } from "./helpers" import { @@ -20,11 +20,12 @@ const { getNamedSigners, getUnnamedSigners } = helpers.signers async function fixture() { const { tbtc, stbtc, mezoAllocator, mezoPortal } = await deployment() const { governance, maintainer } = await getNamedSigners() - const [thirdParty] = await getUnnamedSigners() + const [depositor, thirdParty] = await getUnnamedSigners() return { governance, thirdParty, + depositor, maintainer, tbtc, stbtc, @@ -40,11 +41,21 @@ describe("MezoAllocator", () => { let mezoPortal: IMezoPortal let thirdParty: HardhatEthersSigner + let depositor: HardhatEthersSigner let maintainer: HardhatEthersSigner + let governance: HardhatEthersSigner before(async () => { - ;({ thirdParty, maintainer, tbtc, stbtc, mezoAllocator, mezoPortal } = - await loadFixture(fixture)) + ;({ + thirdParty, + depositor, + maintainer, + governance, + tbtc, + stbtc, + mezoAllocator, + mezoPortal, + } = await loadFixture(fixture)) }) describe("allocate", () => { @@ -121,6 +132,235 @@ describe("MezoAllocator", () => { 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, "NotAuthorized") + }) + }) + + 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 () => { + expect( + await tbtc.balanceOf(await mezoPortal.getAddress()), + ).to.equal(to1e18(3)) + }) + }) + + 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 () => { + expect( + await tbtc.balanceOf(await mezoPortal.getAddress()), + ).to.equal(0) + }) + }) + }) + }) + }) + + 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") + }) }) }) }) From 74c8cc8a5f56e62713b30a249470f9de6b45dd4e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 10 Apr 2024 00:30:37 +0200 Subject: [PATCH 106/123] Linting --- core/test/MezoAllocator.test.ts | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index 33e2057d5..5d012d664 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -271,7 +271,10 @@ describe("MezoAllocator", () => { it("should revert", async () => { await expect( mezoAllocator.connect(thirdParty).addMaintainer(depositor.address), - ).to.be.revertedWithCustomError(mezoAllocator, "OwnableUnauthorizedAccount") + ).to.be.revertedWithCustomError( + mezoAllocator, + "OwnableUnauthorizedAccount", + ) }) }) @@ -299,13 +302,19 @@ describe("MezoAllocator", () => { it("should add a new maintainer to the list", async () => { const maintainers = await mezoAllocator.getMaintainers() - expect(maintainers).to.deep.equal([maintainer.address, thirdParty.address]) + 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") + ).to.be.revertedWithCustomError( + mezoAllocator, + "MaintainerAlreadyRegistered", + ) }) it("should not allow to add a zero address as a maintainer", async () => { @@ -324,7 +333,10 @@ describe("MezoAllocator", () => { it("should revert", async () => { await expect( mezoAllocator.connect(thirdParty).removeMaintainer(depositor.address), - ).to.be.revertedWithCustomError(mezoAllocator, "OwnableUnauthorizedAccount") + ).to.be.revertedWithCustomError( + mezoAllocator, + "OwnableUnauthorizedAccount", + ) }) }) @@ -333,7 +345,9 @@ describe("MezoAllocator", () => { let tx: ContractTransactionResponse before(async () => { - await mezoAllocator.connect(governance).addMaintainer(thirdParty.address) + await mezoAllocator + .connect(governance) + .addMaintainer(thirdParty.address) tx = await mezoAllocator .connect(governance) .removeMaintainer(thirdParty.address) @@ -358,8 +372,13 @@ describe("MezoAllocator", () => { it("should not allow to remove a maintainer twice", async () => { await expect( - mezoAllocator.connect(governance).removeMaintainer(thirdParty.address), - ).to.be.revertedWithCustomError(mezoAllocator, "MaintainerNotRegistered") + mezoAllocator + .connect(governance) + .removeMaintainer(thirdParty.address), + ).to.be.revertedWithCustomError( + mezoAllocator, + "MaintainerNotRegistered", + ) }) }) }) From 1121527e7750d613962790a6e9608e3ac92146ee Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 10 Apr 2024 00:31:27 +0200 Subject: [PATCH 107/123] Deleting Dispatcher The current implementation of Dispatcher is no longer valid. We need to remove all the related code. --- core/contracts/Dispatcher.sol | 174 --------- core/contracts/Router.sol | 37 -- core/contracts/stBTC.sol | 1 - core/contracts/test/upgrades/stBTCV2.sol | 6 +- core/deploy/02_deploy_dispatcher.ts | 29 -- .../12_mezo_allocator_update_maintainer.ts | 2 +- .../22_transfer_ownership_dispatcher.ts | 31 -- core/test/Dispatcher.test.ts | 342 ------------------ core/test/helpers/context.ts | 4 - 9 files changed, 4 insertions(+), 622 deletions(-) delete mode 100644 core/contracts/Dispatcher.sol delete mode 100644 core/contracts/Router.sol delete mode 100644 core/deploy/02_deploy_dispatcher.ts delete mode 100644 core/deploy/22_transfer_ownership_dispatcher.ts delete mode 100644 core/test/Dispatcher.test.ts 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/stBTC.sol b/core/contracts/stBTC.sol index 0b3e27a05..976b67813 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -5,7 +5,6 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol"; -import "./Dispatcher.sol"; import "./PausableOwnable.sol"; import "./lib/ERC4626Fees.sol"; import "./interfaces/IDispatcher.sol"; diff --git a/core/contracts/test/upgrades/stBTCV2.sol b/core/contracts/test/upgrades/stBTCV2.sol index 85c29e96e..cb5d655f7 100644 --- a/core/contracts/test/upgrades/stBTCV2.sol +++ b/core/contracts/test/upgrades/stBTCV2.sol @@ -5,9 +5,9 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol"; -import "../../Dispatcher.sol"; import "../../PausableOwnable.sol"; import "../../lib/ERC4626Fees.sol"; +import "../../interfaces/IDispatcher.sol"; import {ZeroAddress} from "../../utils/Errors.sol"; /// @title stBTCV2 @@ -17,7 +17,7 @@ contract stBTCV2 is ERC4626Fees, PausableOwnable { using SafeERC20 for IERC20; /// Dispatcher contract that routes tBTC from stBTC to a given vault and back. - Dispatcher public dispatcher; + IDispatcher public dispatcher; /// Address of the treasury wallet, where fees should be transferred to. address public treasury; @@ -119,7 +119,7 @@ contract stBTCV2 is ERC4626Fees, PausableOwnable { /// @notice Updates the dispatcher contract and gives it an unlimited /// allowance to transfer staked tBTC. /// @param newDispatcher Address of the new dispatcher contract. - function updateDispatcher(Dispatcher newDispatcher) external onlyOwner { + function updateDispatcher(IDispatcher newDispatcher) external onlyOwner { if (address(newDispatcher) == address(0)) { revert ZeroAddress(); } 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/deploy/12_mezo_allocator_update_maintainer.ts b/core/deploy/12_mezo_allocator_update_maintainer.ts index aa7cec484..1be91a2a9 100644 --- a/core/deploy/12_mezo_allocator_update_maintainer.ts +++ b/core/deploy/12_mezo_allocator_update_maintainer.ts @@ -21,4 +21,4 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func func.tags = ["MezoAllocatorUpdateMaintainer"] -func.dependencies = ["Dispatcher"] +func.dependencies = ["MezoAllocator"] diff --git a/core/deploy/22_transfer_ownership_dispatcher.ts b/core/deploy/22_transfer_ownership_dispatcher.ts deleted file mode 100644 index 427cf388e..000000000 --- a/core/deploy/22_transfer_ownership_dispatcher.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { HardhatRuntimeEnvironment } from "hardhat/types" -import type { DeployFunction } from "hardhat-deploy/types" - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { getNamedAccounts, deployments } = hre - const { deployer, governance } = await getNamedAccounts() - const { log } = deployments - - log(`transferring ownership of Dispatcher contract to ${governance}`) - - await deployments.execute( - "Dispatcher", - { from: deployer, log: true, waitConfirmations: 1 }, - "transferOwnership", - governance, - ) - - if (hre.network.name !== "mainnet") { - await deployments.execute( - "Dispatcher", - { from: governance, log: true, waitConfirmations: 1 }, - "acceptOwnership", - ) - } -} - -export default func - -func.tags = ["TransferOwnershipDispatcher"] -func.dependencies = ["Dispatcher"] -func.runAtTheEnd = true diff --git a/core/test/Dispatcher.test.ts b/core/test/Dispatcher.test.ts deleted file mode 100644 index dfe7dba92..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 } -} -// TODO: Remove these tests once Distpather contract is removed from the project. -describe.skip("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/core/test/helpers/context.ts b/core/test/helpers/context.ts index 68584f5ae..a9471f738 100644 --- a/core/test/helpers/context.ts +++ b/core/test/helpers/context.ts @@ -3,7 +3,6 @@ import { getDeployedContract } from "./contract" import type { StBTC as stBTC, - Dispatcher, BridgeStub, TestERC4626, TBTCVaultStub, @@ -28,8 +27,6 @@ export async function deployment() { const tbtcBridge: BridgeStub = await getDeployedContract("Bridge") const tbtcVault: TBTCVaultStub = await getDeployedContract("TBTCVault") - const dispatcher: Dispatcher = await getDeployedContract("Dispatcher") - const vault: TestERC4626 = await getDeployedContract("Vault") const mezoAllocator: MezoAllocator = await getDeployedContract("MezoAllocator") @@ -42,7 +39,6 @@ export async function deployment() { bitcoinRedeemer, tbtcBridge, tbtcVault, - dispatcher, vault, mezoAllocator, mezoPortal, From 9b45a73a99b879e6fd362553f60de79405abad98 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 10 Apr 2024 11:08:24 +0200 Subject: [PATCH 108/123] Move core/ to solidity/ In the current approach we don't exect external contracts modules to be defined so we replace the core/ directory with solidity/. This is consistent with other mono-repos we have. --- .github/workflows/reusable-sdk-build.yaml | 12 ++-- ...uild.yaml => reusable-solidity-build.yaml} | 16 ++--- .github/workflows/sdk.yaml | 12 ++-- .../workflows/{core.yaml => solidity.yaml} | 62 +++++++++---------- .pre-commit-config.yaml | 29 +++++---- .prettierignore | 2 +- README.md | 4 +- netlify.toml | 8 +-- pnpm-workspace.yaml | 2 +- sdk/package.json | 2 +- sdk/src/lib/ethereum/bitcoin-depositor.ts | 2 +- sdk/src/lib/ethereum/stbtc.ts | 2 +- sdk/test/lib/ethereum/tbtc-depositor.test.ts | 2 +- {core => solidity}/.env | 0 {core => solidity}/.env.example | 0 {core => solidity}/.eslintignore | 0 {core => solidity}/.eslintrc | 0 {core => solidity}/.gitignore | 0 {core => solidity}/.mocharc.json | 0 {core => solidity}/.nvmrc | 0 {core => solidity}/.prettierignore | 0 {core => solidity}/.prettierrc.js | 0 {core => solidity}/.solhint.json | 0 {core => solidity}/.solhintignore | 0 {core => solidity}/.tsconfig-eslint.json | 0 {core => solidity}/README.md | 0 .../contracts/BitcoinDepositor.sol | 0 .../contracts/BitcoinRedeemer.sol | 0 .../contracts/MezoAllocator.sol | 0 .../contracts/PausableOwnable.sol | 0 .../contracts/bridge/ITBTCToken.sol | 0 .../contracts/interfaces/IDispatcher.sol | 0 .../contracts/lib/ERC4626Fees.sol | 0 {core => solidity}/contracts/stBTC.sol | 0 .../test/BitcoinDepositorHarness.sol | 0 .../contracts/test/MezoPortalStub.sol | 0 .../contracts/test/TestERC20.sol | 0 .../contracts/test/TestERC4626.sol | 0 .../contracts/test/TestTBTC.sol | 0 .../test/upgrades/BitcoinDepositorV2.sol | 0 .../contracts/test/upgrades/stBTCV2.sol | 0 {core => solidity}/contracts/utils/Errors.sol | 0 .../deploy/00_resolve_mezo_portal.ts | 0 .../deploy/00_resolve_tbtc_bridge.ts | 0 .../deploy/00_resolve_tbtc_token.ts | 0 .../deploy/00_resolve_tbtc_vault.ts | 0 .../deploy/00_resolve_testing_erc4626.ts | 0 {core => solidity}/deploy/01_deploy_stbtc.ts | 0 .../deploy/02_deploy_mezo_allocator.ts | 0 .../deploy/03_deploy_bitcoin_depositor.ts | 0 .../deploy/04_deploy_bitcoin_redeemer.ts | 0 .../deploy/11_stbtc_update_dispatcher.ts | 0 .../12_mezo_allocator_update_maintainer.ts | 0 .../13_stbtc_update_minimum_deposit_amount.ts | 0 .../deploy/14_update_pause_admin_stbtc.ts | 0 .../deploy/21_transfer_ownership_stbtc.ts | 0 ...23_transfer_ownership_bitcoin_depositor.ts | 0 .../24_transfer_ownership_bitcoin_redeemer.ts | 0 .../24_transfer_ownership_mezo_allocator.ts | 0 .../deployments/sepolia/.chainId | 0 .../deployments/sepolia/BitcoinDepositor.json | 0 .../49f0432287e96a47d66ba17ae7bf5d96.json | 0 .../b22c277b248ba02f9ec5bf62d176f9ce.json | 0 .../deployments/sepolia/stBTC.json | 0 .../external/mainnet/Bridge.json | 0 .../external/mainnet/MezoPortal.json | 0 {core => solidity}/external/mainnet/TBTC.json | 0 .../external/mainnet/TBTCVault.json | 0 .../external/sepolia/Bridge.json | 0 .../external/sepolia/MezoPortal.json | 0 {core => solidity}/external/sepolia/TBTC.json | 0 .../external/sepolia/TBTCVault.json | 0 {core => solidity}/hardhat.config.ts | 0 {core => solidity}/helpers/address.ts | 0 {core => solidity}/helpers/deployment.ts | 0 {core => solidity}/package.json | 2 +- .../scripts/fetch_external_artifacts.sh | 0 {core => solidity}/slither.config.json | 0 .../test/BitcoinDepositor.test.ts | 0 .../test/BitcoinDepositor.upgrade.test.ts | 0 .../test/BitcoinRedeemer.test.ts | 0 {core => solidity}/test/Deployment.test.ts | 0 {core => solidity}/test/MezoAllocator.test.ts | 0 {core => solidity}/test/data/tbtc.ts | 0 {core => solidity}/test/helpers.test.ts | 0 {core => solidity}/test/helpers/context.ts | 0 {core => solidity}/test/helpers/contract.ts | 0 {core => solidity}/test/helpers/index.ts | 0 {core => solidity}/test/helpers/snapshot.ts | 0 {core => solidity}/test/stBTC.test.ts | 0 {core => solidity}/test/stBTC.upgrade.test.ts | 0 {core => solidity}/test/utils/index.ts | 0 {core => solidity}/test/utils/number.ts | 0 {core => solidity}/tsconfig.export.json | 0 {core => solidity}/tsconfig.json | 0 {core => solidity}/types/index.ts | 0 96 files changed, 78 insertions(+), 79 deletions(-) rename .github/workflows/{reusable-core-build.yaml => reusable-solidity-build.yaml} (69%) rename .github/workflows/{core.yaml => solidity.yaml} (79%) rename {core => solidity}/.env (100%) rename {core => solidity}/.env.example (100%) rename {core => solidity}/.eslintignore (100%) rename {core => solidity}/.eslintrc (100%) rename {core => solidity}/.gitignore (100%) rename {core => solidity}/.mocharc.json (100%) rename {core => solidity}/.nvmrc (100%) rename {core => solidity}/.prettierignore (100%) rename {core => solidity}/.prettierrc.js (100%) rename {core => solidity}/.solhint.json (100%) rename {core => solidity}/.solhintignore (100%) rename {core => solidity}/.tsconfig-eslint.json (100%) rename {core => solidity}/README.md (100%) rename {core => solidity}/contracts/BitcoinDepositor.sol (100%) rename {core => solidity}/contracts/BitcoinRedeemer.sol (100%) rename {core => solidity}/contracts/MezoAllocator.sol (100%) rename {core => solidity}/contracts/PausableOwnable.sol (100%) rename {core => solidity}/contracts/bridge/ITBTCToken.sol (100%) rename {core => solidity}/contracts/interfaces/IDispatcher.sol (100%) rename {core => solidity}/contracts/lib/ERC4626Fees.sol (100%) rename {core => solidity}/contracts/stBTC.sol (100%) rename {core => solidity}/contracts/test/BitcoinDepositorHarness.sol (100%) rename {core => solidity}/contracts/test/MezoPortalStub.sol (100%) rename {core => solidity}/contracts/test/TestERC20.sol (100%) rename {core => solidity}/contracts/test/TestERC4626.sol (100%) rename {core => solidity}/contracts/test/TestTBTC.sol (100%) rename {core => solidity}/contracts/test/upgrades/BitcoinDepositorV2.sol (100%) rename {core => solidity}/contracts/test/upgrades/stBTCV2.sol (100%) rename {core => solidity}/contracts/utils/Errors.sol (100%) rename {core => solidity}/deploy/00_resolve_mezo_portal.ts (100%) rename {core => solidity}/deploy/00_resolve_tbtc_bridge.ts (100%) rename {core => solidity}/deploy/00_resolve_tbtc_token.ts (100%) rename {core => solidity}/deploy/00_resolve_tbtc_vault.ts (100%) rename {core => solidity}/deploy/00_resolve_testing_erc4626.ts (100%) rename {core => solidity}/deploy/01_deploy_stbtc.ts (100%) rename {core => solidity}/deploy/02_deploy_mezo_allocator.ts (100%) rename {core => solidity}/deploy/03_deploy_bitcoin_depositor.ts (100%) rename {core => solidity}/deploy/04_deploy_bitcoin_redeemer.ts (100%) rename {core => solidity}/deploy/11_stbtc_update_dispatcher.ts (100%) rename {core => solidity}/deploy/12_mezo_allocator_update_maintainer.ts (100%) rename {core => solidity}/deploy/13_stbtc_update_minimum_deposit_amount.ts (100%) rename {core => solidity}/deploy/14_update_pause_admin_stbtc.ts (100%) rename {core => solidity}/deploy/21_transfer_ownership_stbtc.ts (100%) rename {core => solidity}/deploy/23_transfer_ownership_bitcoin_depositor.ts (100%) rename {core => solidity}/deploy/24_transfer_ownership_bitcoin_redeemer.ts (100%) rename {core => solidity}/deploy/24_transfer_ownership_mezo_allocator.ts (100%) rename {core => solidity}/deployments/sepolia/.chainId (100%) rename {core => solidity}/deployments/sepolia/BitcoinDepositor.json (100%) rename {core => solidity}/deployments/sepolia/solcInputs/49f0432287e96a47d66ba17ae7bf5d96.json (100%) rename {core => solidity}/deployments/sepolia/solcInputs/b22c277b248ba02f9ec5bf62d176f9ce.json (100%) rename {core => solidity}/deployments/sepolia/stBTC.json (100%) rename {core => solidity}/external/mainnet/Bridge.json (100%) rename {core => solidity}/external/mainnet/MezoPortal.json (100%) rename {core => solidity}/external/mainnet/TBTC.json (100%) rename {core => solidity}/external/mainnet/TBTCVault.json (100%) rename {core => solidity}/external/sepolia/Bridge.json (100%) rename {core => solidity}/external/sepolia/MezoPortal.json (100%) rename {core => solidity}/external/sepolia/TBTC.json (100%) rename {core => solidity}/external/sepolia/TBTCVault.json (100%) rename {core => solidity}/hardhat.config.ts (100%) rename {core => solidity}/helpers/address.ts (100%) rename {core => solidity}/helpers/deployment.ts (100%) rename {core => solidity}/package.json (98%) rename {core => solidity}/scripts/fetch_external_artifacts.sh (100%) rename {core => solidity}/slither.config.json (100%) rename {core => solidity}/test/BitcoinDepositor.test.ts (100%) rename {core => solidity}/test/BitcoinDepositor.upgrade.test.ts (100%) rename {core => solidity}/test/BitcoinRedeemer.test.ts (100%) rename {core => solidity}/test/Deployment.test.ts (100%) rename {core => solidity}/test/MezoAllocator.test.ts (100%) rename {core => solidity}/test/data/tbtc.ts (100%) rename {core => solidity}/test/helpers.test.ts (100%) rename {core => solidity}/test/helpers/context.ts (100%) rename {core => solidity}/test/helpers/contract.ts (100%) rename {core => solidity}/test/helpers/index.ts (100%) rename {core => solidity}/test/helpers/snapshot.ts (100%) rename {core => solidity}/test/stBTC.test.ts (100%) rename {core => solidity}/test/stBTC.upgrade.test.ts (100%) rename {core => solidity}/test/utils/index.ts (100%) rename {core => solidity}/test/utils/number.ts (100%) rename {core => solidity}/tsconfig.export.json (100%) rename {core => solidity}/tsconfig.json (100%) rename {core => solidity}/types/index.ts (100%) 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/.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..dc7b0f048 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 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) +[![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 @@ -51,7 +51,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/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-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..fbec48325 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/solidity": "workspace:*", "ethers": "^6.10.0" } } diff --git a/sdk/src/lib/ethereum/bitcoin-depositor.ts b/sdk/src/lib/ethereum/bitcoin-depositor.ts index d968ee5b6..a3f74f22c 100644 --- a/sdk/src/lib/ethereum/bitcoin-depositor.ts +++ b/sdk/src/lib/ethereum/bitcoin-depositor.ts @@ -1,5 +1,5 @@ import { packRevealDepositParameters } from "@keep-network/tbtc-v2.ts" -import { BitcoinDepositor as BitcoinDepositorTypechain } from "@acre-btc/core/typechain/contracts/BitcoinDepositor" +import { BitcoinDepositor as BitcoinDepositorTypechain } from "@acre-btc/solidity/typechain/contracts/BitcoinDepositor" import { ZeroAddress, dataSlice, diff --git a/sdk/src/lib/ethereum/stbtc.ts b/sdk/src/lib/ethereum/stbtc.ts index a5087f2a6..8ef5f7187 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/solidity/typechain/contracts/StBTC" import stBTC from "./artifacts/sepolia/stBTC.json" import { EthersContractConfig, diff --git a/sdk/test/lib/ethereum/tbtc-depositor.test.ts b/sdk/test/lib/ethereum/tbtc-depositor.test.ts index 7368af30a..1c37cef98 100644 --- a/sdk/test/lib/ethereum/tbtc-depositor.test.ts +++ b/sdk/test/lib/ethereum/tbtc-depositor.test.ts @@ -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(), ) 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/core/.solhintignore b/solidity/.solhintignore similarity index 100% rename from core/.solhintignore rename to solidity/.solhintignore 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/core/README.md b/solidity/README.md similarity index 100% rename from core/README.md rename to solidity/README.md diff --git a/core/contracts/BitcoinDepositor.sol b/solidity/contracts/BitcoinDepositor.sol similarity index 100% rename from core/contracts/BitcoinDepositor.sol rename to solidity/contracts/BitcoinDepositor.sol diff --git a/core/contracts/BitcoinRedeemer.sol b/solidity/contracts/BitcoinRedeemer.sol similarity index 100% rename from core/contracts/BitcoinRedeemer.sol rename to solidity/contracts/BitcoinRedeemer.sol diff --git a/core/contracts/MezoAllocator.sol b/solidity/contracts/MezoAllocator.sol similarity index 100% rename from core/contracts/MezoAllocator.sol rename to solidity/contracts/MezoAllocator.sol diff --git a/core/contracts/PausableOwnable.sol b/solidity/contracts/PausableOwnable.sol similarity index 100% rename from core/contracts/PausableOwnable.sol rename to solidity/contracts/PausableOwnable.sol diff --git a/core/contracts/bridge/ITBTCToken.sol b/solidity/contracts/bridge/ITBTCToken.sol similarity index 100% rename from core/contracts/bridge/ITBTCToken.sol rename to solidity/contracts/bridge/ITBTCToken.sol diff --git a/core/contracts/interfaces/IDispatcher.sol b/solidity/contracts/interfaces/IDispatcher.sol similarity index 100% rename from core/contracts/interfaces/IDispatcher.sol rename to solidity/contracts/interfaces/IDispatcher.sol 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 100% rename from core/contracts/stBTC.sol rename to solidity/contracts/stBTC.sol diff --git a/core/contracts/test/BitcoinDepositorHarness.sol b/solidity/contracts/test/BitcoinDepositorHarness.sol similarity index 100% rename from core/contracts/test/BitcoinDepositorHarness.sol rename to solidity/contracts/test/BitcoinDepositorHarness.sol diff --git a/core/contracts/test/MezoPortalStub.sol b/solidity/contracts/test/MezoPortalStub.sol similarity index 100% rename from core/contracts/test/MezoPortalStub.sol rename to solidity/contracts/test/MezoPortalStub.sol diff --git a/core/contracts/test/TestERC20.sol b/solidity/contracts/test/TestERC20.sol similarity index 100% rename from core/contracts/test/TestERC20.sol rename to solidity/contracts/test/TestERC20.sol diff --git a/core/contracts/test/TestERC4626.sol b/solidity/contracts/test/TestERC4626.sol similarity index 100% rename from core/contracts/test/TestERC4626.sol rename to solidity/contracts/test/TestERC4626.sol diff --git a/core/contracts/test/TestTBTC.sol b/solidity/contracts/test/TestTBTC.sol similarity index 100% rename from core/contracts/test/TestTBTC.sol rename to solidity/contracts/test/TestTBTC.sol diff --git a/core/contracts/test/upgrades/BitcoinDepositorV2.sol b/solidity/contracts/test/upgrades/BitcoinDepositorV2.sol similarity index 100% rename from core/contracts/test/upgrades/BitcoinDepositorV2.sol rename to solidity/contracts/test/upgrades/BitcoinDepositorV2.sol diff --git a/core/contracts/test/upgrades/stBTCV2.sol b/solidity/contracts/test/upgrades/stBTCV2.sol similarity index 100% rename from core/contracts/test/upgrades/stBTCV2.sol rename to solidity/contracts/test/upgrades/stBTCV2.sol 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/core/deploy/00_resolve_mezo_portal.ts b/solidity/deploy/00_resolve_mezo_portal.ts similarity index 100% rename from core/deploy/00_resolve_mezo_portal.ts rename to solidity/deploy/00_resolve_mezo_portal.ts diff --git a/core/deploy/00_resolve_tbtc_bridge.ts b/solidity/deploy/00_resolve_tbtc_bridge.ts similarity index 100% rename from core/deploy/00_resolve_tbtc_bridge.ts rename to solidity/deploy/00_resolve_tbtc_bridge.ts diff --git a/core/deploy/00_resolve_tbtc_token.ts b/solidity/deploy/00_resolve_tbtc_token.ts similarity index 100% rename from core/deploy/00_resolve_tbtc_token.ts rename to solidity/deploy/00_resolve_tbtc_token.ts diff --git a/core/deploy/00_resolve_tbtc_vault.ts b/solidity/deploy/00_resolve_tbtc_vault.ts similarity index 100% rename from core/deploy/00_resolve_tbtc_vault.ts rename to solidity/deploy/00_resolve_tbtc_vault.ts diff --git a/core/deploy/00_resolve_testing_erc4626.ts b/solidity/deploy/00_resolve_testing_erc4626.ts similarity index 100% rename from core/deploy/00_resolve_testing_erc4626.ts rename to solidity/deploy/00_resolve_testing_erc4626.ts diff --git a/core/deploy/01_deploy_stbtc.ts b/solidity/deploy/01_deploy_stbtc.ts similarity index 100% rename from core/deploy/01_deploy_stbtc.ts rename to solidity/deploy/01_deploy_stbtc.ts diff --git a/core/deploy/02_deploy_mezo_allocator.ts b/solidity/deploy/02_deploy_mezo_allocator.ts similarity index 100% rename from core/deploy/02_deploy_mezo_allocator.ts rename to solidity/deploy/02_deploy_mezo_allocator.ts diff --git a/core/deploy/03_deploy_bitcoin_depositor.ts b/solidity/deploy/03_deploy_bitcoin_depositor.ts similarity index 100% rename from core/deploy/03_deploy_bitcoin_depositor.ts rename to solidity/deploy/03_deploy_bitcoin_depositor.ts diff --git a/core/deploy/04_deploy_bitcoin_redeemer.ts b/solidity/deploy/04_deploy_bitcoin_redeemer.ts similarity index 100% rename from core/deploy/04_deploy_bitcoin_redeemer.ts rename to solidity/deploy/04_deploy_bitcoin_redeemer.ts diff --git a/core/deploy/11_stbtc_update_dispatcher.ts b/solidity/deploy/11_stbtc_update_dispatcher.ts similarity index 100% rename from core/deploy/11_stbtc_update_dispatcher.ts rename to solidity/deploy/11_stbtc_update_dispatcher.ts diff --git a/core/deploy/12_mezo_allocator_update_maintainer.ts b/solidity/deploy/12_mezo_allocator_update_maintainer.ts similarity index 100% rename from core/deploy/12_mezo_allocator_update_maintainer.ts rename to solidity/deploy/12_mezo_allocator_update_maintainer.ts diff --git a/core/deploy/13_stbtc_update_minimum_deposit_amount.ts b/solidity/deploy/13_stbtc_update_minimum_deposit_amount.ts similarity index 100% rename from core/deploy/13_stbtc_update_minimum_deposit_amount.ts rename to solidity/deploy/13_stbtc_update_minimum_deposit_amount.ts diff --git a/core/deploy/14_update_pause_admin_stbtc.ts b/solidity/deploy/14_update_pause_admin_stbtc.ts similarity index 100% rename from core/deploy/14_update_pause_admin_stbtc.ts rename to solidity/deploy/14_update_pause_admin_stbtc.ts diff --git a/core/deploy/21_transfer_ownership_stbtc.ts b/solidity/deploy/21_transfer_ownership_stbtc.ts similarity index 100% rename from core/deploy/21_transfer_ownership_stbtc.ts rename to solidity/deploy/21_transfer_ownership_stbtc.ts diff --git a/core/deploy/23_transfer_ownership_bitcoin_depositor.ts b/solidity/deploy/23_transfer_ownership_bitcoin_depositor.ts similarity index 100% rename from core/deploy/23_transfer_ownership_bitcoin_depositor.ts rename to solidity/deploy/23_transfer_ownership_bitcoin_depositor.ts diff --git a/core/deploy/24_transfer_ownership_bitcoin_redeemer.ts b/solidity/deploy/24_transfer_ownership_bitcoin_redeemer.ts similarity index 100% rename from core/deploy/24_transfer_ownership_bitcoin_redeemer.ts rename to solidity/deploy/24_transfer_ownership_bitcoin_redeemer.ts diff --git a/core/deploy/24_transfer_ownership_mezo_allocator.ts b/solidity/deploy/24_transfer_ownership_mezo_allocator.ts similarity index 100% rename from core/deploy/24_transfer_ownership_mezo_allocator.ts rename to solidity/deploy/24_transfer_ownership_mezo_allocator.ts diff --git a/core/deployments/sepolia/.chainId b/solidity/deployments/sepolia/.chainId similarity index 100% rename from core/deployments/sepolia/.chainId rename to solidity/deployments/sepolia/.chainId diff --git a/core/deployments/sepolia/BitcoinDepositor.json b/solidity/deployments/sepolia/BitcoinDepositor.json similarity index 100% rename from core/deployments/sepolia/BitcoinDepositor.json rename to solidity/deployments/sepolia/BitcoinDepositor.json diff --git a/core/deployments/sepolia/solcInputs/49f0432287e96a47d66ba17ae7bf5d96.json b/solidity/deployments/sepolia/solcInputs/49f0432287e96a47d66ba17ae7bf5d96.json similarity index 100% rename from core/deployments/sepolia/solcInputs/49f0432287e96a47d66ba17ae7bf5d96.json rename to solidity/deployments/sepolia/solcInputs/49f0432287e96a47d66ba17ae7bf5d96.json diff --git a/core/deployments/sepolia/solcInputs/b22c277b248ba02f9ec5bf62d176f9ce.json b/solidity/deployments/sepolia/solcInputs/b22c277b248ba02f9ec5bf62d176f9ce.json similarity index 100% rename from core/deployments/sepolia/solcInputs/b22c277b248ba02f9ec5bf62d176f9ce.json rename to solidity/deployments/sepolia/solcInputs/b22c277b248ba02f9ec5bf62d176f9ce.json diff --git a/core/deployments/sepolia/stBTC.json b/solidity/deployments/sepolia/stBTC.json similarity index 100% rename from core/deployments/sepolia/stBTC.json rename to solidity/deployments/sepolia/stBTC.json diff --git a/core/external/mainnet/Bridge.json b/solidity/external/mainnet/Bridge.json similarity index 100% rename from core/external/mainnet/Bridge.json rename to solidity/external/mainnet/Bridge.json diff --git a/core/external/mainnet/MezoPortal.json b/solidity/external/mainnet/MezoPortal.json similarity index 100% rename from core/external/mainnet/MezoPortal.json rename to solidity/external/mainnet/MezoPortal.json 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/core/external/sepolia/MezoPortal.json b/solidity/external/sepolia/MezoPortal.json similarity index 100% rename from core/external/sepolia/MezoPortal.json rename to solidity/external/sepolia/MezoPortal.json diff --git a/core/external/sepolia/TBTC.json b/solidity/external/sepolia/TBTC.json similarity index 100% rename from core/external/sepolia/TBTC.json rename to solidity/external/sepolia/TBTC.json diff --git a/core/external/sepolia/TBTCVault.json b/solidity/external/sepolia/TBTCVault.json similarity index 100% rename from core/external/sepolia/TBTCVault.json rename to solidity/external/sepolia/TBTCVault.json diff --git a/core/hardhat.config.ts b/solidity/hardhat.config.ts similarity index 100% rename from core/hardhat.config.ts rename to solidity/hardhat.config.ts diff --git a/core/helpers/address.ts b/solidity/helpers/address.ts similarity index 100% rename from core/helpers/address.ts rename to solidity/helpers/address.ts diff --git a/core/helpers/deployment.ts b/solidity/helpers/deployment.ts similarity index 100% rename from core/helpers/deployment.ts rename to solidity/helpers/deployment.ts diff --git a/core/package.json b/solidity/package.json similarity index 98% rename from core/package.json rename to solidity/package.json index 4ed08daac..ec2e9e3c3 100644 --- a/core/package.json +++ b/solidity/package.json @@ -1,5 +1,5 @@ { - "name": "@acre-btc/core", + "name": "@acre-btc/solidity", "version": "0.0.1", "description": "Bitcoin Liquid Staking", "license": "GPL-3.0-only", diff --git a/core/scripts/fetch_external_artifacts.sh b/solidity/scripts/fetch_external_artifacts.sh similarity index 100% rename from core/scripts/fetch_external_artifacts.sh rename to solidity/scripts/fetch_external_artifacts.sh diff --git a/core/slither.config.json b/solidity/slither.config.json similarity index 100% rename from core/slither.config.json rename to solidity/slither.config.json diff --git a/core/test/BitcoinDepositor.test.ts b/solidity/test/BitcoinDepositor.test.ts similarity index 100% rename from core/test/BitcoinDepositor.test.ts rename to solidity/test/BitcoinDepositor.test.ts diff --git a/core/test/BitcoinDepositor.upgrade.test.ts b/solidity/test/BitcoinDepositor.upgrade.test.ts similarity index 100% rename from core/test/BitcoinDepositor.upgrade.test.ts rename to solidity/test/BitcoinDepositor.upgrade.test.ts diff --git a/core/test/BitcoinRedeemer.test.ts b/solidity/test/BitcoinRedeemer.test.ts similarity index 100% rename from core/test/BitcoinRedeemer.test.ts rename to solidity/test/BitcoinRedeemer.test.ts diff --git a/core/test/Deployment.test.ts b/solidity/test/Deployment.test.ts similarity index 100% rename from core/test/Deployment.test.ts rename to solidity/test/Deployment.test.ts diff --git a/core/test/MezoAllocator.test.ts b/solidity/test/MezoAllocator.test.ts similarity index 100% rename from core/test/MezoAllocator.test.ts rename to solidity/test/MezoAllocator.test.ts diff --git a/core/test/data/tbtc.ts b/solidity/test/data/tbtc.ts similarity index 100% rename from core/test/data/tbtc.ts rename to solidity/test/data/tbtc.ts 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 100% rename from core/test/helpers/context.ts rename to solidity/test/helpers/context.ts 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 100% rename from core/test/stBTC.test.ts rename to solidity/test/stBTC.test.ts diff --git a/core/test/stBTC.upgrade.test.ts b/solidity/test/stBTC.upgrade.test.ts similarity index 100% rename from core/test/stBTC.upgrade.test.ts rename to solidity/test/stBTC.upgrade.test.ts 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 100% rename from core/types/index.ts rename to solidity/types/index.ts From db83064d0ddadc26c856e375523575dc3a841199 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 10 Apr 2024 11:12:28 +0200 Subject: [PATCH 109/123] Rename @acre-btc/solidity to @acre-btc/contracts --- sdk/package.json | 2 +- sdk/src/lib/ethereum/bitcoin-depositor.ts | 2 +- sdk/src/lib/ethereum/stbtc.ts | 2 +- solidity/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/package.json b/sdk/package.json index fbec48325..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/solidity": "workspace:*", + "@acre-btc/contracts": "workspace:*", "ethers": "^6.10.0" } } diff --git a/sdk/src/lib/ethereum/bitcoin-depositor.ts b/sdk/src/lib/ethereum/bitcoin-depositor.ts index a3f74f22c..0facc9bba 100644 --- a/sdk/src/lib/ethereum/bitcoin-depositor.ts +++ b/sdk/src/lib/ethereum/bitcoin-depositor.ts @@ -1,5 +1,5 @@ import { packRevealDepositParameters } from "@keep-network/tbtc-v2.ts" -import { BitcoinDepositor as BitcoinDepositorTypechain } from "@acre-btc/solidity/typechain/contracts/BitcoinDepositor" +import { BitcoinDepositor as BitcoinDepositorTypechain } from "@acre-btc/contracts/typechain/contracts/BitcoinDepositor" import { ZeroAddress, dataSlice, diff --git a/sdk/src/lib/ethereum/stbtc.ts b/sdk/src/lib/ethereum/stbtc.ts index 8ef5f7187..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/solidity/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/solidity/package.json b/solidity/package.json index ec2e9e3c3..6f867cbaf 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -1,5 +1,5 @@ { - "name": "@acre-btc/solidity", + "name": "@acre-btc/contracts", "version": "0.0.1", "description": "Bitcoin Liquid Staking", "license": "GPL-3.0-only", From 537e79469128b408c5981099540a957970561cd3 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 10 Apr 2024 11:12:50 +0200 Subject: [PATCH 110/123] Fix linting --- .github/workflows/subgraph.yaml | 1 - 1 file changed, 1 deletion(-) 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 - From f9172fc1e9e9bd1cb710a00e20f15fd29990367a Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 10 Apr 2024 11:19:46 +0200 Subject: [PATCH 111/123] Update pnpm-lock.yaml --- pnpm-lock.yaml | 371 +++++++++++++++++++++---------------------------- 1 file changed, 156 insertions(+), 215 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 122c0166f..a58ff6a75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,121 +18,6 @@ importers: specifier: ^11.2.1 version: 11.2.1 - core: - dependencies: - '@keep-network/bitcoin-spv-sol': - specifier: 3.4.0-solc-0.8 - version: 3.4.0-solc-0.8 - '@keep-network/tbtc-v2': - specifier: development - version: 1.6.0-dev.24(@keep-network/keep-core@1.8.1-dev.0) - '@openzeppelin/contracts': - specifier: ^5.0.0 - version: 5.0.0 - '@openzeppelin/contracts-upgradeable': - specifier: ^5.0.2 - version: 5.0.2(@openzeppelin/contracts@5.0.0) - '@thesis-co/solidity-contracts': - specifier: github:thesis/solidity-contracts#c315b9d - version: github.com/thesis/solidity-contracts/c315b9d - '@types/chai-as-promised': - specifier: ^7.1.8 - version: 7.1.8 - chai-as-promised: - specifier: ^7.1.1 - version: 7.1.1(chai@4.3.10) - devDependencies: - '@keep-network/hardhat-helpers': - specifier: ^0.7.1 - version: 0.7.1(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-verify@2.0.1)(@openzeppelin/hardhat-upgrades@3.0.5)(ethers@6.8.1)(hardhat-deploy@0.11.43)(hardhat@2.19.1) - '@nomicfoundation/hardhat-chai-matchers': - specifier: ^2.0.2 - version: 2.0.2(@nomicfoundation/hardhat-ethers@3.0.5)(chai@4.3.10)(ethers@6.8.1)(hardhat@2.19.1) - '@nomicfoundation/hardhat-ethers': - specifier: ^3.0.5 - version: 3.0.5(ethers@6.8.1)(hardhat@2.19.1) - '@nomicfoundation/hardhat-network-helpers': - specifier: ^1.0.9 - version: 1.0.9(hardhat@2.19.1) - '@nomicfoundation/hardhat-toolbox': - specifier: ^4.0.0 - version: 4.0.0(@nomicfoundation/hardhat-chai-matchers@2.0.2)(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-network-helpers@1.0.9)(@nomicfoundation/hardhat-verify@2.0.1)(@typechain/ethers-v6@0.5.1)(@typechain/hardhat@9.1.0)(@types/chai@4.3.11)(@types/mocha@10.0.6)(@types/node@20.9.4)(chai@4.3.10)(ethers@6.8.1)(hardhat-gas-reporter@1.0.9)(hardhat@2.19.1)(solidity-coverage@0.8.5)(ts-node@10.9.1)(typechain@8.3.2)(typescript@5.3.2) - '@nomicfoundation/hardhat-verify': - specifier: ^2.0.1 - version: 2.0.1(hardhat@2.19.1) - '@nomiclabs/hardhat-etherscan': - specifier: ^3.1.7 - version: 3.1.7(hardhat@2.19.1) - '@openzeppelin/hardhat-upgrades': - specifier: ^3.0.5 - version: 3.0.5(@nomicfoundation/hardhat-ethers@3.0.5)(@nomicfoundation/hardhat-verify@2.0.1)(ethers@6.8.1)(hardhat@2.19.1) - '@thesis-co/eslint-config': - specifier: github:thesis/eslint-config#7b9bc8c - version: github.com/thesis/eslint-config/7b9bc8c(eslint@8.54.0)(prettier@3.1.0)(typescript@5.3.2) - '@typechain/ethers-v6': - specifier: ^0.5.1 - version: 0.5.1(ethers@6.8.1)(typechain@8.3.2)(typescript@5.3.2) - '@typechain/hardhat': - specifier: ^9.1.0 - version: 9.1.0(@typechain/ethers-v6@0.5.1)(ethers@6.8.1)(hardhat@2.19.1)(typechain@8.3.2) - '@types/chai': - specifier: ^4.3.11 - version: 4.3.11 - '@types/mocha': - specifier: ^10.0.6 - version: 10.0.6 - '@types/node': - specifier: ^20.9.4 - version: 20.9.4 - chai: - specifier: ^4.3.10 - version: 4.3.10 - eslint: - specifier: ^8.54.0 - version: 8.54.0 - ethers: - specifier: ^6.8.1 - version: 6.8.1 - hardhat: - specifier: ^2.19.1 - version: 2.19.1(ts-node@10.9.1)(typescript@5.3.2) - hardhat-contract-sizer: - specifier: ^2.10.0 - version: 2.10.0(hardhat@2.19.1) - hardhat-deploy: - specifier: ^0.11.43 - version: 0.11.43 - hardhat-gas-reporter: - specifier: ^1.0.9 - version: 1.0.9(hardhat@2.19.1) - prettier: - specifier: ^3.1.0 - version: 3.1.0 - prettier-plugin-solidity: - specifier: ^1.2.0 - version: 1.2.0(prettier@3.1.0) - solhint: - specifier: ^4.0.0 - version: 4.0.0 - solhint-config-thesis: - specifier: github:thesis/solhint-config - version: github.com/thesis/solhint-config/266de12d96d58f01171e20858b855ec80520de8d(solhint@4.0.0) - solidity-coverage: - specifier: ^0.8.5 - version: 0.8.5(hardhat@2.19.1) - solidity-docgen: - specifier: 0.6.0-beta.36 - version: 0.6.0-beta.36(hardhat@2.19.1) - ts-node: - specifier: ^10.9.1 - version: 10.9.1(@types/node@20.9.4)(typescript@5.3.2) - typechain: - specifier: ^8.3.2 - version: 8.3.2(typescript@5.3.2) - typescript: - specifier: ^5.3.2 - version: 5.3.2 - dapp: dependencies: '@acre-btc/sdk': @@ -170,7 +55,7 @@ importers: version: 0.23.13 axios: specifier: ^1.6.7 - version: 1.6.7 + version: 1.6.7(debug@4.3.4) ethers: specifier: ^6.10.0 version: 6.10.0 @@ -244,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) @@ -285,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': @@ -480,7 +480,7 @@ packages: '@babel/traverse': 7.23.4 '@babel/types': 7.23.4 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1779,7 +1779,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.4 '@babel/types': 7.23.4 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3503,7 +3503,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) espree: 9.6.1 globals: 13.23.0 ignore: 5.3.0 @@ -4158,7 +4158,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4500,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 @@ -4510,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 @@ -4635,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 @@ -5128,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 @@ -5136,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: @@ -5169,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 @@ -5190,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) @@ -5336,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: @@ -5559,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: @@ -5571,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) @@ -5582,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 @@ -6815,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 @@ -6837,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) @@ -7341,7 +7342,7 @@ packages: '@typescript-eslint/type-utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) '@typescript-eslint/utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.54.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -7386,7 +7387,7 @@ packages: '@typescript-eslint/types': 6.12.0 '@typescript-eslint/typescript-estree': 6.12.0(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.54.0 typescript: 5.3.2 transitivePeerDependencies: @@ -7437,7 +7438,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.12.0(typescript@5.3.2) '@typescript-eslint/utils': 6.12.0(eslint@8.54.0)(typescript@5.3.2) - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.54.0 ts-api-utils: 1.0.3(typescript@5.3.2) typescript: 5.3.2 @@ -7484,7 +7485,7 @@ packages: dependencies: '@typescript-eslint/types': 6.12.0 '@typescript-eslint/visitor-keys': 6.12.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -8302,19 +8303,9 @@ packages: /axios@0.21.4(debug@4.3.4): resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: - follow-redirects: 1.15.3(debug@4.3.4) - transitivePeerDependencies: - - debug - - /axios@1.6.7: - resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} - dependencies: - follow-redirects: 1.15.5 - form-data: 4.0.0 - proxy-from-env: 1.1.0 + follow-redirects: 1.15.5(debug@4.3.4) transitivePeerDependencies: - debug - dev: false /axios@1.6.7(debug@4.3.4): resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} @@ -8324,7 +8315,6 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: true /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} @@ -10263,17 +10253,6 @@ packages: dependencies: ms: 2.1.3 - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -11526,7 +11505,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -11860,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==} @@ -12325,27 +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: - resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dev: false - /follow-redirects@1.15.5(debug@4.3.4): resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} engines: {node: '>=4.0'} @@ -12356,7 +12297,6 @@ packages: optional: true dependencies: debug: 4.3.4(supports-color@8.1.1) - dev: true /follow-redirects@1.5.10: resolution: {integrity: sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==} @@ -19044,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 @@ -22116,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: From c15fe460307345c740b76c95c75d51c5cbc5cea1 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 10 Apr 2024 11:20:34 +0200 Subject: [PATCH 112/123] Add commit to blame ignore --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) 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 From 89585dcfee1a55207136eed34be7bb6d4ccb84d0 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 10 Apr 2024 12:18:01 +0200 Subject: [PATCH 113/123] Releasing deposit in full from Mezo In case there will be a need of pulling all the funds from Mezo we add a releaseDeposit function that can be called by the owner only. --- core/contracts/MezoAllocator.sol | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index edf078132..e47fed42f 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -91,6 +91,8 @@ contract MezoAllocator is IDispatcher, Ownable2Step { 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 an authorized account. error NotAuthorized(); /// @notice Reverts if the caller is not a maintainer. @@ -178,6 +180,18 @@ contract MezoAllocator is IDispatcher, Ownable2Step { tbtc.safeTransfer(address(stbtc), amount); } + /// @notice Releases deposit in full from MezoPortal. + 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 { From 7a2b52b7a32d0caf15ccae6aafa90fe11b0ca625 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 10 Apr 2024 12:26:14 +0200 Subject: [PATCH 114/123] Adding two additional custom errors Added NotMaintainer and NotStbtc to better understand the error if occurs. --- core/contracts/MezoAllocator.sol | 8 ++++++-- core/test/MezoAllocator.test.ts | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index e47fed42f..2a0aef8d8 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -96,13 +96,17 @@ contract MezoAllocator is IDispatcher, Ownable2Step { /// @notice Reverts if the caller is not an authorized account. error NotAuthorized(); /// @notice Reverts if the caller is not a maintainer. + error NotMaintainer(); + /// @notice Reverts if the caller is not the stBTC contract. + error NotStbtc(); + /// @notice Reverts if the caller is not a maintainer. error MaintainerNotRegistered(); /// @notice Reverts if the caller is already a maintainer. error MaintainerAlreadyRegistered(); modifier onlyMaintainer() { if (!isMaintainer[msg.sender]) { - revert NotAuthorized(); + revert NotMaintainer(); } _; } @@ -171,7 +175,7 @@ contract MezoAllocator is IDispatcher, Ownable2Step { /// MezoPortal for a given deposit id. /// @param amount Amount of tBTC to withdraw. function withdraw(uint256 amount) external { - if (msg.sender != address(stbtc)) revert NotAuthorized(); + if (msg.sender != address(stbtc)) revert NotStbtc(); emit DepositWithdrawn(depositId, amount); mezoPortal.withdraw(address(tbtc), depositId, uint96(amount)); diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index 5d012d664..08c51aa67 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -65,7 +65,7 @@ describe("MezoAllocator", () => { it("should revert", async () => { await expect( mezoAllocator.connect(thirdParty).allocate(), - ).to.be.revertedWithCustomError(mezoAllocator, "NotAuthorized") + ).to.be.revertedWithCustomError(mezoAllocator, "NotMaintainer") }) }) @@ -153,7 +153,7 @@ describe("MezoAllocator", () => { it("should revert", async () => { await expect( mezoAllocator.connect(thirdParty).withdraw(1n), - ).to.be.revertedWithCustomError(mezoAllocator, "NotAuthorized") + ).to.be.revertedWithCustomError(mezoAllocator, "NotStbtc") }) }) From 0cb112380b243d0a30e9bb83aaa3a1a051b13178 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 10 Apr 2024 12:30:04 +0200 Subject: [PATCH 115/123] Renaming tag to MezoAllocatorAddMaintainer --- core/deploy/12_mezo_allocator_update_maintainer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/deploy/12_mezo_allocator_update_maintainer.ts b/core/deploy/12_mezo_allocator_update_maintainer.ts index aa7cec484..5eb82de37 100644 --- a/core/deploy/12_mezo_allocator_update_maintainer.ts +++ b/core/deploy/12_mezo_allocator_update_maintainer.ts @@ -20,5 +20,5 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func -func.tags = ["MezoAllocatorUpdateMaintainer"] +func.tags = ["MezoAllocatorAddMaintainer"] func.dependencies = ["Dispatcher"] From 59c3d6fbd618e7322d3c9b1145c21f276a737617 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 10 Apr 2024 13:14:47 +0200 Subject: [PATCH 116/123] Adding tests for releaseDeposit --- core/test/MezoAllocator.test.ts | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index 08c51aa67..c2759123f 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -383,4 +383,48 @@ describe("MezoAllocator", () => { }) }) }) + + describe("releaseDeposit", () => { + beforeAfterSnapshotWrapper() + + context("when a caller is not a maintainer", () => { + 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 () => { + expect(await tbtc.balanceOf(await mezoPortal.getAddress())).to.equal( + 0, + ) + }) + }) + }) + }) }) From b7e40dcfcd3a8fb8191ba5da6f8b768815237c55 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 10 Apr 2024 13:40:14 +0200 Subject: [PATCH 117/123] Removing test erc4626 as it is no longer used --- core/contracts/test/TestERC4626.sol | 12 --------- core/deploy/00_resolve_testing_erc4626.ts | 30 ----------------------- core/test/helpers/context.ts | 3 --- 3 files changed, 45 deletions(-) delete mode 100644 core/contracts/test/TestERC4626.sol delete mode 100644 core/deploy/00_resolve_testing_erc4626.ts 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/test/helpers/context.ts b/core/test/helpers/context.ts index a9471f738..b4d864fcb 100644 --- a/core/test/helpers/context.ts +++ b/core/test/helpers/context.ts @@ -4,7 +4,6 @@ import { getDeployedContract } from "./contract" import type { StBTC as stBTC, BridgeStub, - TestERC4626, TBTCVaultStub, MezoAllocator, MezoPortalStub, @@ -27,7 +26,6 @@ export async function deployment() { const tbtcBridge: BridgeStub = await getDeployedContract("Bridge") const tbtcVault: TBTCVaultStub = await getDeployedContract("TBTCVault") - const vault: TestERC4626 = await getDeployedContract("Vault") const mezoAllocator: MezoAllocator = await getDeployedContract("MezoAllocator") const mezoPortal: MezoPortalStub = await getDeployedContract("MezoPortal") @@ -39,7 +37,6 @@ export async function deployment() { bitcoinRedeemer, tbtcBridge, tbtcVault, - vault, mezoAllocator, mezoPortal, } From c52a2da3f3f5a6435aadfec266a9ed9fc5814d43 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 10 Apr 2024 13:45:05 +0200 Subject: [PATCH 118/123] Refactoring ReleaseDeposit tests --- core/test/MezoAllocator.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index c2759123f..26d6fdc69 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -387,7 +387,7 @@ describe("MezoAllocator", () => { describe("releaseDeposit", () => { beforeAfterSnapshotWrapper() - context("when a caller is not a maintainer", () => { + context("when a caller is not governance", () => { it("should revert", async () => { await expect( mezoAllocator.connect(thirdParty).releaseDeposit(), @@ -420,8 +420,10 @@ describe("MezoAllocator", () => { }) it("should decrease Mezo Portal balance", async () => { - expect(await tbtc.balanceOf(await mezoPortal.getAddress())).to.equal( - 0, + await expect(tx).to.changeTokenBalances( + tbtc, + [mezoPortal, stbtc], + [-to1e18(5), to1e18(5)], ) }) }) From 6ca6e0e42e104a3c1c9ec3193e4759b7f6eb7207 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 10 Apr 2024 14:22:23 +0200 Subject: [PATCH 119/123] Smaller refactorings and cleanups --- core/contracts/MezoAllocator.sol | 16 ++++++++-------- core/test/MezoAllocator.test.ts | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 2a0aef8d8..1511bfbd4 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -93,20 +93,18 @@ contract MezoAllocator is IDispatcher, Ownable2Step { 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 an authorized account. - error NotAuthorized(); /// @notice Reverts if the caller is not a maintainer. - error NotMaintainer(); + error CallerNotMaintainer(); /// @notice Reverts if the caller is not the stBTC contract. - error NotStbtc(); - /// @notice Reverts if the caller is not a maintainer. + 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 NotMaintainer(); + revert CallerNotMaintainer(); } _; } @@ -175,7 +173,7 @@ contract MezoAllocator is IDispatcher, Ownable2Step { /// MezoPortal for a given deposit id. /// @param amount Amount of tBTC to withdraw. function withdraw(uint256 amount) external { - if (msg.sender != address(stbtc)) revert NotStbtc(); + if (msg.sender != address(stbtc)) revert CallerNotStbtc(); emit DepositWithdrawn(depositId, amount); mezoPortal.withdraw(address(tbtc), depositId, uint96(amount)); @@ -185,6 +183,8 @@ contract MezoAllocator is IDispatcher, Ownable2Step { } /// @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) @@ -232,7 +232,7 @@ contract MezoAllocator is IDispatcher, Ownable2Step { } /// @notice Returns the total amount of tBTC allocated to MezoPortal. - function totalAssets() external view returns (uint256 totalAmount) { + function totalAssets() external view returns (uint256) { return depositBalance; } diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index 26d6fdc69..d088fcee1 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -65,7 +65,7 @@ describe("MezoAllocator", () => { it("should revert", async () => { await expect( mezoAllocator.connect(thirdParty).allocate(), - ).to.be.revertedWithCustomError(mezoAllocator, "NotMaintainer") + ).to.be.revertedWithCustomError(mezoAllocator, "CallerNotMaintainer") }) }) @@ -153,7 +153,7 @@ describe("MezoAllocator", () => { it("should revert", async () => { await expect( mezoAllocator.connect(thirdParty).withdraw(1n), - ).to.be.revertedWithCustomError(mezoAllocator, "NotStbtc") + ).to.be.revertedWithCustomError(mezoAllocator, "CallerNotStbtc") }) }) From 860b16cd203a0a150660741335d34a51e233732a Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 10 Apr 2024 14:40:15 +0200 Subject: [PATCH 120/123] Refactoring tests around MezoAllocator --- core/test/MezoAllocator.test.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index d088fcee1..23467a497 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -79,8 +79,10 @@ describe("MezoAllocator", () => { }) it("should deposit and transfer tBTC to Mezo Portal", async () => { - expect(await tbtc.balanceOf(await mezoPortal.getAddress())).to.equal( - to1e18(6), + await expect(tx).to.changeTokenBalances( + tbtc, + [await mezoPortal.getAddress()], + [to1e18(6)], ) }) @@ -95,6 +97,11 @@ describe("MezoAllocator", () => { 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") @@ -201,9 +208,11 @@ describe("MezoAllocator", () => { }) it("should decrease Mezo Portal balance", async () => { - expect( - await tbtc.balanceOf(await mezoPortal.getAddress()), - ).to.equal(to1e18(3)) + await expect(tx).to.changeTokenBalances( + tbtc, + [await mezoPortal.getAddress()], + [-to1e18(2)], + ) }) }) @@ -232,9 +241,11 @@ describe("MezoAllocator", () => { }) it("should decrease Mezo Portal balance", async () => { - expect( - await tbtc.balanceOf(await mezoPortal.getAddress()), - ).to.equal(0) + await expect(tx).to.changeTokenBalances( + tbtc, + [await mezoPortal.getAddress()], + [-to1e18(3)], + ) }) }) }) From 0d54124573b385a8f0174f65b0ae50125b1ccca9 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 10 Apr 2024 15:06:11 +0200 Subject: [PATCH 121/123] Update MezoAllocator upgrade test contract --- .../test/upgrades/MezoAllocatorV2.sol | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/core/contracts/test/upgrades/MezoAllocatorV2.sol b/core/contracts/test/upgrades/MezoAllocatorV2.sol index d809cf194..2457510a4 100644 --- a/core/contracts/test/upgrades/MezoAllocatorV2.sol +++ b/core/contracts/test/upgrades/MezoAllocatorV2.sol @@ -94,18 +94,22 @@ contract MezoAllocatorV2 is IDispatcher, Ownable2StepUpgradeable { 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 an authorized account. - error NotAuthorized(); /// @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 NotAuthorized(); + revert CallerNotMaintainer(); } _; } @@ -115,9 +119,6 @@ contract MezoAllocatorV2 is IDispatcher, Ownable2StepUpgradeable { _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, @@ -175,7 +176,7 @@ contract MezoAllocatorV2 is IDispatcher, Ownable2StepUpgradeable { /// MezoPortal for a given deposit id. /// @param amount Amount of tBTC to withdraw. function withdraw(uint256 amount) external { - if (msg.sender != address(stbtc)) revert NotAuthorized(); + if (msg.sender != address(stbtc)) revert CallerNotStbtc(); emit DepositWithdrawn(depositId, amount); mezoPortal.withdraw(address(tbtc), depositId, uint96(amount)); @@ -184,6 +185,20 @@ contract MezoAllocatorV2 is IDispatcher, Ownable2StepUpgradeable { 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. @@ -224,7 +239,12 @@ contract MezoAllocatorV2 is IDispatcher, Ownable2StepUpgradeable { } /// @notice Returns the total amount of tBTC allocated to MezoPortal. - function totalAssets() external view returns (uint256 totalAmount) { + function totalAssets() external view returns (uint256) { return depositBalance; } + + /// @notice Returns the list of maintainers. + function getMaintainers() external view returns (address[] memory) { + return maintainers; + } } From ba01b0787d1a2e72c166652b43508819448ef33d Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 10 Apr 2024 15:20:29 +0200 Subject: [PATCH 122/123] Refactorring error comments --- core/contracts/MezoAllocator.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index 1511bfbd4..02d194388 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -97,9 +97,9 @@ contract MezoAllocator is IDispatcher, Ownable2Step { error CallerNotMaintainer(); /// @notice Reverts if the caller is not the stBTC contract. error CallerNotStbtc(); - /// @notice Reverts if the maintainer is already registered. + /// @notice Reverts if the maintainer is not registered. error MaintainerNotRegistered(); - /// @notice Reverts if the caller is already a maintainer. + /// @notice Reverts if the maintainer has been already registered. error MaintainerAlreadyRegistered(); modifier onlyMaintainer() { From e85b23152e9b549bf58865cae5a12abebfa962fe Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 10 Apr 2024 16:20:45 +0200 Subject: [PATCH 123/123] Update Solidity readme --- README.md | 2 -- solidity/README.md | 49 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dc7b0f048..3dbae51c2 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ Bitcoin Liquid Staking -[![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 ### pnpm diff --git a/solidity/README.md b/solidity/README.md index b90952ee8..651236aa9 100644 --- a/solidity/README.md +++ b/solidity/README.md @@ -1,3 +1,48 @@ -# Acre +# Acre Contracts -Acre is a “liquid staking” solution that allows people to earn yield on their Bitcoin via yield farming on Ethereum. +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