瀏覽代碼

Merge branch 'main' of github.com:pyth-network/pyth-crosschain into scaling

keyvan 1 年之前
父節點
當前提交
da1f0315eb
共有 48 個文件被更改,包括 2171 次插入561 次删除
  1. 2 2
      apps/staking/src/api.ts
  2. 95 0
      apps/staking/src/app/api/stake-accounts/route.ts
  3. 42 0
      apps/staking/src/app/api/supply/route.ts
  4. 二進制
      apps/staking/src/app/opengraph-image.jpg
  5. 二進制
      apps/staking/src/app/opengraph-image.png
  6. 1 0
      apps/staking/src/app/terms-of-service/page.tsx
  7. 1 1
      apps/staking/src/components/Blocked/index.tsx
  8. 4 4
      apps/staking/src/components/Dashboard/index.tsx
  9. 42 14
      apps/staking/src/components/Footer/index.tsx
  10. 5 3
      apps/staking/src/components/GeneralFaq/index.tsx
  11. 2 2
      apps/staking/src/components/GovernanceGuide/index.tsx
  12. 1 1
      apps/staking/src/components/Header/help-menu.tsx
  13. 8 2
      apps/staking/src/components/Header/index.tsx
  14. 181 107
      apps/staking/src/components/OracleIntegrityStaking/index.tsx
  15. 36 31
      apps/staking/src/components/OracleIntegrityStakingGuide/index.tsx
  16. 40 23
      apps/staking/src/components/PublisherFaq/index.tsx
  17. 1079 0
      apps/staking/src/components/TermsOfService/index.tsx
  18. 1 4
      apps/staking/src/config/server.ts
  19. 22 17
      apps/staking/src/middleware.ts
  20. 6 1
      contract_manager/store/chains/EvmChains.yaml
  21. 0 3
      contract_manager/store/contracts/EvmEntropyContracts.yaml
  22. 6 3
      contract_manager/store/contracts/EvmPriceFeedContracts.yaml
  23. 6 3
      contract_manager/store/contracts/EvmWormholeContracts.yaml
  24. 1 0
      governance/pyth_staking_sdk/src/constants.ts
  25. 5 3
      governance/pyth_staking_sdk/src/index.ts
  26. 76 12
      governance/pyth_staking_sdk/src/pyth-staking-client.ts
  27. 6 3
      governance/pyth_staking_sdk/src/types.ts
  28. 33 19
      governance/pyth_staking_sdk/src/utils/vesting.ts
  29. 1 0
      governance/xc_admin/packages/xc_admin_common/src/chains.ts
  30. 139 281
      pnpm-lock.yaml
  31. 1 1
      target_chains/ethereum/sdk/js/package.json
  32. 3 3
      target_chains/ethereum/sdk/js/src/index.ts
  33. 6 4
      target_chains/ton/contracts/contracts/Main.fc
  34. 21 4
      target_chains/ton/contracts/contracts/Pyth.fc
  35. 2 1
      target_chains/ton/contracts/contracts/common/errors.fc
  36. 1 1
      target_chains/ton/contracts/contracts/common/governance_actions.fc
  37. 1 0
      target_chains/ton/contracts/contracts/common/op.fc
  38. 3 0
      target_chains/ton/contracts/contracts/common/storage.fc
  39. 6 3
      target_chains/ton/contracts/contracts/tests/PythTest.fc
  40. 78 0
      target_chains/ton/contracts/contracts/tests/PythTestUpgraded.fc
  41. 2 1
      target_chains/ton/contracts/contracts/tests/WormholeTest.fc
  42. 1 0
      target_chains/ton/contracts/package.json
  43. 154 2
      target_chains/ton/contracts/tests/PythTest.spec.ts
  44. 16 0
      target_chains/ton/contracts/tests/utils.ts
  45. 3 0
      target_chains/ton/contracts/tests/utils/pyth.ts
  46. 25 2
      target_chains/ton/contracts/wrappers/PythTest.ts
  47. 6 0
      target_chains/ton/contracts/wrappers/PythTestUpgraded.compile.ts
  48. 1 0
      target_chains/ton/contracts/wrappers/WormholeTest.ts

+ 2 - 2
apps/staking/src/api.ts

@@ -194,7 +194,7 @@ const loadDataForStakeAccount = async (
   client: PythStakingClient,
   hermesClient: HermesClient,
   stakeAccount: PublicKey,
-) => {
+): Promise<Data> => {
   const [
     { publishers, ...baseInfo },
     stakeAccountCustody,
@@ -240,7 +240,7 @@ const loadDataForStakeAccount = async (
       cooldown: filterGovernancePositions(PositionState.PREUNLOCKING),
       cooldown2: filterGovernancePositions(PositionState.UNLOCKING),
     },
-    unlockSchedule,
+    unlockSchedule: unlockSchedule.schedule,
     integrityStakingPublishers: publishers.map((publisher) => ({
       ...publisher,
       positions: {

+ 95 - 0
apps/staking/src/app/api/stake-accounts/route.ts

@@ -0,0 +1,95 @@
+import { PythStakingClient } from "@pythnetwork/staking-sdk";
+import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
+import { clusterApiUrl, Connection, PublicKey } from "@solana/web3.js";
+import type { NextRequest } from "next/server";
+import { z } from "zod";
+
+import { IS_MAINNET, RPC } from "../../../config/server";
+
+const UnlockScheduleSchema = z.object({
+  date: z.date(),
+  amount: z.number(),
+});
+
+const LockSchema = z.object({
+  type: z.string(),
+  schedule: z.array(UnlockScheduleSchema),
+});
+
+const ResponseSchema = z.array(
+  z.object({
+    custodyAccount: z.string(),
+    actualAmount: z.number(),
+    lock: LockSchema,
+  }),
+);
+
+const stakingClient = new PythStakingClient({
+  connection: new Connection(
+    RPC ??
+      clusterApiUrl(
+        IS_MAINNET ? WalletAdapterNetwork.Mainnet : WalletAdapterNetwork.Devnet,
+      ),
+  ),
+});
+
+const isValidPublicKey = (publicKey: string) => {
+  try {
+    new PublicKey(publicKey);
+    return true;
+  } catch {
+    return false;
+  }
+};
+
+export async function GET(req: NextRequest) {
+  const owner = req.nextUrl.searchParams.get("owner");
+
+  if (owner === null || !isValidPublicKey(owner)) {
+    return Response.json(
+      {
+        error:
+          "Must provide the 'owner' query parameters as a valid base58 public key",
+      },
+      {
+        status: 400,
+      },
+    );
+  }
+
+  const positions = await stakingClient.getAllStakeAccountPositions(
+    new PublicKey(owner),
+  );
+
+  const responseRaw = await Promise.all(
+    positions.map(async (position) => {
+      const custodyAccount =
+        await stakingClient.getStakeAccountCustody(position);
+      const lock = await stakingClient.getUnlockSchedule(position, true);
+      return {
+        custodyAccount: custodyAccount.address.toBase58(),
+        actualAmount: Number(custodyAccount.amount),
+        lock: {
+          type: lock.type,
+          schedule: lock.schedule.map((unlock) => ({
+            date: unlock.date,
+            amount: Number(unlock.amount),
+          })),
+        },
+      };
+    }),
+  );
+
+  const response = ResponseSchema.safeParse(responseRaw);
+
+  return response.success
+    ? Response.json(response.data)
+    : Response.json(
+        {
+          error: "Internal server error",
+        },
+        {
+          status: 500,
+        },
+      );
+}

+ 42 - 0
apps/staking/src/app/api/supply/route.ts

@@ -0,0 +1,42 @@
+import { PythStakingClient } from "@pythnetwork/staking-sdk";
+import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
+import { clusterApiUrl, Connection } from "@solana/web3.js";
+import type { NextRequest } from "next/server";
+import { z } from "zod";
+
+import { IS_MAINNET, RPC } from "../../../config/server";
+
+const stakingClient = new PythStakingClient({
+  connection: new Connection(
+    RPC ??
+      clusterApiUrl(
+        IS_MAINNET ? WalletAdapterNetwork.Mainnet : WalletAdapterNetwork.Devnet,
+      ),
+  ),
+});
+
+const querySchema = z.enum(["totalSupply", "circulatingSupply"]);
+
+export async function GET(req: NextRequest) {
+  const query = querySchema.safeParse(req.nextUrl.searchParams.get("q"));
+  if (!query.success) {
+    return Response.json(
+      {
+        error:
+          "The 'q' query parameter must be one of 'totalSupply' or 'circulatingSupply'.",
+      },
+      {
+        status: 400,
+      },
+    );
+  }
+  const q = query.data;
+
+  if (q === "circulatingSupply") {
+    const circulatingSupply = await stakingClient.getCirculatingSupply();
+    return Response.json(Number(circulatingSupply));
+  } else {
+    const pythMint = await stakingClient.getPythTokenMint();
+    return Response.json(Number(pythMint.supply));
+  }
+}

二進制
apps/staking/src/app/opengraph-image.jpg


二進制
apps/staking/src/app/opengraph-image.png


+ 1 - 0
apps/staking/src/app/terms-of-service/page.tsx

@@ -0,0 +1 @@
+export { TermsOfService as default } from "../../components/TermsOfService";

+ 1 - 1
apps/staking/src/components/Blocked/index.tsx

@@ -19,7 +19,7 @@ const Blocked = ({ reason }: Props) => (
     </h1>
     <p className="mb-20 text-lg">{reason}</p>
     <LinkButton
-      className="place-self-center px-24 py-3"
+      className="w-full max-w-96 place-self-center px-8 py-3"
       href="https://www.pyth.network"
       target="_blank"
     >

+ 4 - 4
apps/staking/src/components/Dashboard/index.tsx

@@ -144,10 +144,10 @@ export const Dashboard = ({
         className="group border-neutral-600/50 data-[empty]:my-[5dvh] data-[empty]:border data-[empty]:bg-white/10 data-[empty]:p-4 sm:p-4 data-[empty]:sm:my-0 data-[empty]:sm:border-0 data-[empty]:sm:bg-transparent data-[empty]:sm:p-0"
         {...(tab === TabIds.Empty && { "data-empty": true })}
       >
-        <h1 className="my-4 hidden text-center text-xl/tight font-light group-data-[empty]:block sm:mb-6 sm:text-3xl lg:my-14 lg:text-5xl">
+        <h1 className="my-4 hidden text-center text-xl/tight font-light group-data-[empty]:mb-10 group-data-[empty]:block sm:mb-6 sm:text-3xl group-data-[empty]:sm:mb-6 lg:my-14 lg:text-5xl">
           Choose Your Journey
         </h1>
-        <TabList className="sticky top-header-height z-10 flex flex-row items-stretch justify-center group-data-[empty]:mx-auto group-data-[empty]:max-w-7xl group-data-[empty]:flex-col group-data-[empty]:gap-2 group-data-[empty]:sm:flex-row">
+        <TabList className="sticky top-header-height z-10 flex flex-row items-stretch justify-center group-data-[empty]:mx-auto group-data-[empty]:max-w-7xl group-data-[empty]:flex-col group-data-[empty]:gap-8 group-data-[empty]:sm:flex-row group-data-[empty]:sm:gap-2">
           <Tab id={TabIds.Empty} className="hidden" />
           <Journey
             longText="Oracle Integrity Staking (OIS)"
@@ -241,13 +241,13 @@ const Journey = ({
     )}
     {...props}
   >
-    <div className="grid size-full flex-none basis-0 place-content-center border border-neutral-600/50 bg-pythpurple-800 p-2 text-center font-semibold transition group-hover/tab:bg-pythpurple-600/30 group-selected/tab:border-pythpurple-400/60 group-selected/tab:bg-pythpurple-600/60 group-hover/tab:group-selected/tab:bg-pythpurple-600/60 sm:py-4 sm:text-lg">
+    <div className="grid size-full flex-none basis-0 place-content-center border border-neutral-600/50 bg-pythpurple-800 p-2 text-center font-semibold transition group-data-[empty]:py-8 group-hover/tab:bg-pythpurple-600/30 group-selected/tab:border-pythpurple-400/60 group-selected/tab:bg-pythpurple-600/60 group-hover/tab:group-selected/tab:bg-pythpurple-600/60 sm:py-4 sm:text-lg group-data-[empty]:sm:py-2">
       <span className="hidden group-data-[empty]:inline sm:inline">
         {longText}
       </span>
       <span className="group-data-[empty]:hidden sm:hidden">{shortText}</span>
     </div>
-    <div className="relative hidden max-h-[40dvh] w-4/5 flex-none overflow-hidden opacity-30 transition group-hover/tab:opacity-100 group-data-[empty]:sm:block">
+    <div className="relative hidden w-4/5 flex-none overflow-hidden opacity-30 transition group-hover/tab:opacity-100 group-data-[empty]:sm:block">
       <div className="absolute inset-0 bg-[#E6DAFE] mix-blend-color" />
       <Image src={image} alt="" className="size-full object-cover object-top" />
       <div className="absolute inset-0 top-16 text-center text-xl text-pythpurple-800 md:text-2xl lg:text-3xl">

+ 42 - 14
apps/staking/src/components/Footer/index.tsx

@@ -38,33 +38,61 @@ export const Footer = ({
 }: Omit<HTMLAttributes<HTMLElement>, "children">) => (
   <footer
     className={clsx(
-      "text-xs font-light sm:sticky sm:bottom-0 lg:px-4",
+      "text-xs font-light lg:sticky lg:bottom-0 lg:px-4",
       className,
     )}
     {...props}
   >
     <div className="border-t border-neutral-600/50 bg-pythpurple-800 lg:border-x">
-      <MaxWidth className="flex h-48 flex-col items-center justify-between overflow-hidden py-8 sm:h-16 sm:flex-row sm:gap-10 lg:-mx-4">
-        <div className="flex flex-col items-center gap-2 sm:flex-row sm:gap-4 md:gap-8">
-          <Link href="https://www.pyth.network" target="_blank">
-            <Logo className="h-10 sm:h-8" />
+      <MaxWidth className="flex h-48 flex-col items-center justify-between overflow-hidden pb-4 pt-8 text-center lg:-mx-4 lg:h-16 lg:flex-row lg:gap-10 lg:py-0">
+        <div className="flex flex-col items-center gap-2 lg:flex-row lg:gap-8">
+          <Link
+            href="https://www.pyth.network"
+            target="_blank"
+            className="focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400"
+          >
+            <Logo className="h-10 lg:h-8" />
             <span className="sr-only">Pyth homepage</span>
           </Link>
           <div>© 2024 Pyth Data Association</div>
         </div>
-        <div className="relative flex h-full items-center sm:-right-3">
-          {SOCIAL_LINKS.map(({ name, icon: Icon, href }) => (
+        <div className="flex flex-col items-center gap-6 lg:flex-row-reverse lg:gap-8 xl:gap-16">
+          <div className="relative flex h-full items-center lg:-right-3">
+            {SOCIAL_LINKS.map(({ name, icon: Icon, href }) => (
+              <Link
+                target="_blank"
+                href={href}
+                key={name}
+                className="grid h-full place-content-center px-3 transition hover:text-pythpurple-400 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400"
+                rel="noreferrer"
+              >
+                <Icon className="size-4" />
+                <span className="sr-only">{name}</span>
+              </Link>
+            ))}
+          </div>
+          <div className="flex flex-row gap-1 xl:gap-4">
             <Link
+              className="-my-1 px-2 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400"
               target="_blank"
-              href={href}
-              key={name}
-              className="grid h-full place-content-center px-3 hover:text-pythpurple-400"
-              rel="noreferrer"
+              href="https://pythdataassociation.com/privacy-policy"
             >
-              <Icon className="size-4" />
-              <span className="sr-only">{name}</span>
+              Privacy Policy
             </Link>
-          ))}
+            <Link
+              className="-my-1 px-2 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400"
+              target="_blank"
+              href="https://pythdataassociation.com/terms-of-use"
+            >
+              Terms of Use
+            </Link>
+            <Link
+              className="-my-1 px-2 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400"
+              href="/terms-of-service"
+            >
+              Terms of Service
+            </Link>
+          </div>
         </div>
       </MaxWidth>
     </div>

+ 5 - 3
apps/staking/src/components/GeneralFaq/index.tsx

@@ -105,7 +105,7 @@ export const GeneralFaq = (
         {
           question: "Does participating in OIS affect my participation in PG?",
           answer:
-            "No. The two programs are separate. Staking in OIS does not affect your participation in PG. For example, staking in OIs does not increase your voting power in PG. Staking to a publisher’s stake pool does not give that publisher additional voting power in PG.",
+            "No. The two programs are separate. Staking in OIS does not affect your participation in PG. For example, staking in OIS does not increase your voting power in PG. Staking to a publisher’s stake pool does not give that publisher additional voting power in PG.",
         },
         {
           question: "Does slashing reduce voting weights?",
@@ -171,8 +171,10 @@ export const GeneralFaq = (
               <p>
                 The Cooldown Period has two phases: from the time you click{" "}
                 <strong>Unstake</strong> until the end of the current epoch,
-                followed by a full epoch. Tokens in the first phase are subject
-                to rewards and slashing. Tokens in the second phase are not.
+                followed by a full epoch. Tokens in the first phase are eligible
+                for rewards. Tokens in both phases are subject to slashing if an
+                issue is identified in an epoch in which they were eligible for
+                rewards.
               </p>
             </>
           ),

+ 2 - 2
apps/staking/src/components/GovernanceGuide/index.tsx

@@ -351,7 +351,7 @@ export const GovernanceGuide = (
                   <Link
                     className="underline"
                     target="_blank"
-                    href="https://forum.pyth.network/t/about-the-pyth-dao/14"
+                    href="https://ipfs.io/ipfs/QmP2GmL1n2WbHd7AtHqyXVWFyyHH36aZLfVZbNoqhommJi"
                   >
                     Pyth DAO LLC Operating Agreement
                   </Link>{" "}
@@ -359,7 +359,7 @@ export const GovernanceGuide = (
                   <Link
                     className="underline"
                     target="_blank"
-                    href="https://forum.pyth.network/t/about-the-pyth-dao/14"
+                    href="https://github.com/pyth-network/governance/blob/main/docs/constitution/pyth-dao-constitution.md"
                   >
                     Pyth DAO Constitution
                   </Link>

+ 1 - 1
apps/staking/src/components/Header/help-menu.tsx

@@ -37,7 +37,7 @@ export const HelpMenu = () => {
   return (
     <>
       <MenuTrigger>
-        <Button className="group -mx-2 flex flex-row items-center gap-2 rounded-sm px-2 transition hover:bg-white/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400 pressed:bg-white/10 sm:-mx-4 sm:px-4">
+        <Button className="group -mx-2 flex flex-row items-center gap-2 rounded-sm p-2 transition hover:bg-white/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400 pressed:bg-white/10 sm:-mx-4 sm:px-4">
           <QuestionMarkCircleIcon className="size-6 flex-none" />
           <span className="sr-only xs:not-sr-only">Help</span>
           <ChevronDownIcon className="size-4 flex-none opacity-60 transition duration-300 group-data-[pressed]:-rotate-180" />

+ 8 - 2
apps/staking/src/components/Header/index.tsx

@@ -5,6 +5,7 @@ import { CurrentStakeAccount } from "./current-stake-account";
 import { HelpMenu } from "./help-menu";
 import Logo from "./logo.svg";
 import Logomark from "./logomark.svg";
+import { Link } from "../Link";
 import { MaxWidth } from "../MaxWidth";
 import { WalletButton } from "../WalletButton";
 
@@ -15,8 +16,13 @@ export const Header = ({
   <header className={clsx("sticky top-0 w-full lg:px-4", className)} {...props}>
     <div className="border-b border-neutral-600/50 bg-pythpurple-800 lg:border-x">
       <MaxWidth className="flex h-header items-center justify-between gap-2 lg:-mx-4">
-        <Logo className="hidden max-h-full py-4 text-pythpurple-100 sm:block" />
-        <Logomark className="max-h-full py-4 text-pythpurple-100 sm:hidden" />
+        <Link
+          href="/"
+          className="-mx-2 h-[calc(var(--header-height)_-_0.5rem)] rounded-sm p-2 text-pythpurple-100 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400"
+        >
+          <Logo className="hidden h-full sm:block" />
+          <Logomark className="h-full sm:hidden" />
+        </Link>
         <div className="flex flex-none flex-row items-stretch gap-4 sm:gap-8">
           <CurrentStakeAccount />
           <WalletButton className="flex-none" />

+ 181 - 107
apps/staking/src/components/OracleIntegrityStaking/index.tsx

@@ -582,12 +582,9 @@ const PublisherList = ({
   const scrollTarget = useRef<HTMLDivElement | null>(null);
   const [search, setSearch] = useState("");
   const [yoursFirst, setYoursFirst] = useState(true);
-  const [sort, setSort] = useState({
-    field: SortField.PoolUtilization,
-    descending: true,
-  });
+  const [sort, setSort] = useState(SortOption.RemainingPoolDescending);
   const filter = useFilter({ sensitivity: "base", usage: "search" });
-  const [currentPage, setPage] = useState(0);
+  const [currentPage, setPage] = useState(1);
   const filteredSortedPublishers = useMemo(
     () =>
       publishers
@@ -607,25 +604,16 @@ const PublisherList = ({
               return 1;
             }
           }
-          const sortResult = doSort(a, b, yieldRate, sort.field);
-          return sort.descending ? sortResult * -1 : sortResult;
+          return doSort(a, b, yieldRate, sort);
         }),
-    [
-      publishers,
-      search,
-      sort.field,
-      sort.descending,
-      filter,
-      yieldRate,
-      yoursFirst,
-    ],
+    [publishers, search, sort, filter, yieldRate, yoursFirst],
   );
 
   const paginatedPublishers = useMemo(
     () =>
       filteredSortedPublishers.slice(
+        (currentPage - 1) * PAGE_SIZE,
         currentPage * PAGE_SIZE,
-        (currentPage + 1) * PAGE_SIZE,
       ),
     [filteredSortedPublishers, currentPage],
   );
@@ -643,7 +631,7 @@ const PublisherList = ({
   const updateSearch = useCallback<typeof setSearch>(
     (newSearch) => {
       setSearch(newSearch);
-      updatePage(0);
+      updatePage(1);
     },
     [setSearch, updatePage],
   );
@@ -651,7 +639,7 @@ const PublisherList = ({
   const updateSort = useCallback<typeof setSort>(
     (newSort) => {
       setSort(newSort);
-      updatePage(0);
+      updatePage(1);
     },
     [setSort, updatePage],
   );
@@ -659,7 +647,7 @@ const PublisherList = ({
   const updateYoursFirst = useCallback<typeof setYoursFirst>(
     (newYoursFirst) => {
       setYoursFirst(newYoursFirst);
-      updatePage(0);
+      updatePage(1);
     },
     [setYoursFirst, updatePage],
   );
@@ -697,20 +685,13 @@ const PublisherList = ({
           </SearchField>
           <Select
             className="flex flex-row items-center gap-2 2xl:hidden"
-            selectedKey={sort.field}
-            onSelectionChange={(field) => {
-              updateSort({
-                field: field as SortField,
-                descending:
-                  field === SortField.NumberOfFeeds ||
-                  field === SortField.APY ||
-                  field === SortField.SelfStake,
-              });
-            }}
+            selectedKey={sort}
+            // @ts-expect-error react-aria coerces everything to Key for some reason...
+            onSelectionChange={updateSort}
           >
             <Label className="whitespace-nowrap opacity-80">Sort by</Label>
             <Button className="group flex flex-row items-center gap-2 text-xs transition">
-              {SORT_FIELD_TO_NAME[sort.field]}
+              {getSortName(sort)}
               <ChevronDownIcon className="size-4 flex-none opacity-60 transition duration-300 group-data-[pressed]:-rotate-180" />
             </Button>
             <Popover
@@ -720,17 +701,23 @@ const PublisherList = ({
               <ListBox
                 className="flex origin-top-right flex-col border border-neutral-400 bg-pythpurple-100 py-2 text-sm text-pythpurple-950 shadow shadow-neutral-400 outline-none"
                 items={[
-                  { id: SortField.PublisherName },
-                  { id: SortField.PoolUtilization },
-                  { id: SortField.APY },
-                  { id: SortField.SelfStake },
-                  { id: SortField.NumberOfFeeds },
-                  { id: SortField.QualityRanking },
-                ]}
+                  SortOption.PublisherNameDescending,
+                  SortOption.PublisherNameAscending,
+                  SortOption.RemainingPoolDescending,
+                  SortOption.RemainingPoolAscending,
+                  SortOption.ApyDescending,
+                  SortOption.ApyAscending,
+                  SortOption.SelfStakeDescending,
+                  SortOption.SelfStakeAscending,
+                  SortOption.NumberOfFeedsDescending,
+                  SortOption.NumberOfFeedsAscending,
+                  SortOption.QualityRankingDescending,
+                  SortOption.QualityRankingAscending,
+                ].map((id) => ({ id }))}
               >
                 {({ id }) => (
                   <ListBoxItem className="flex cursor-pointer items-center gap-2 whitespace-nowrap px-4 py-2 text-left data-[disabled]:cursor-default data-[focused]:bg-pythpurple-800/20 data-[has-submenu]:data-[open]:bg-pythpurple-800/10 data-[has-submenu]:data-[open]:data-[focused]:bg-pythpurple-800/20 focus:outline-none focus-visible:outline-none">
-                    {SORT_FIELD_TO_NAME[id]}
+                    {getSortName(id)}
                   </ListBoxItem>
                 )}
               </ListBox>
@@ -772,7 +759,8 @@ const PublisherList = ({
             <thead className="bg-pythpurple-100/30 font-light">
               <tr>
                 <SortablePublisherTableHeader
-                  field={SortField.PublisherName}
+                  asc={SortOption.PublisherNameAscending}
+                  desc={SortOption.PublisherNameDescending}
                   sort={sort}
                   setSort={updateSort}
                   alignment="left"
@@ -781,21 +769,24 @@ const PublisherList = ({
                   Publisher
                 </SortablePublisherTableHeader>
                 <SortablePublisherTableHeader
-                  field={SortField.SelfStake}
+                  asc={SortOption.SelfStakeAscending}
+                  desc={SortOption.SelfStakeDescending}
                   sort={sort}
                   setSort={updateSort}
                 >
                   {"Publisher's stake"}
                 </SortablePublisherTableHeader>
                 <SortablePublisherTableHeader
-                  field={SortField.PoolUtilization}
+                  asc={SortOption.RemainingPoolAscending}
+                  desc={SortOption.RemainingPoolDescending}
                   sort={sort}
                   setSort={updateSort}
                 >
                   Pool
                 </SortablePublisherTableHeader>
                 <SortablePublisherTableHeader
-                  field={SortField.APY}
+                  asc={SortOption.ApyAscending}
+                  desc={SortOption.ApyDescending}
                   sort={sort}
                   setSort={updateSort}
                 >
@@ -803,14 +794,16 @@ const PublisherList = ({
                 </SortablePublisherTableHeader>
                 <PublisherTableHeader>Historical APY</PublisherTableHeader>
                 <SortablePublisherTableHeader
-                  field={SortField.NumberOfFeeds}
+                  asc={SortOption.NumberOfFeedsAscending}
+                  desc={SortOption.NumberOfFeedsDescending}
                   sort={sort}
                   setSort={updateSort}
                 >
                   Number of feeds
                 </SortablePublisherTableHeader>
                 <SortablePublisherTableHeader
-                  field={SortField.QualityRanking}
+                  asc={SortOption.QualityRankingAscending}
+                  desc={SortOption.QualityRankingDescending}
                   sort={sort}
                   setSort={updateSort}
                 >
@@ -896,6 +889,7 @@ const Paginator = ({ currentPage, numPages, onPageChange }: PaginatorProps) => {
                 onPageChange(page);
               }}
               size="nopad"
+              variant="secondary"
               className="grid size-8 place-content-center"
             >
               {page}
@@ -936,16 +930,19 @@ const doSort = (
   a: PublisherProps["publisher"],
   b: PublisherProps["publisher"],
   yieldRate: bigint,
-  sortField: SortField,
+  sort: SortOption,
 ): number => {
-  switch (sortField) {
-    case SortField.PublisherName: {
-      return (a.name ?? a.publicKey.toBase58()).localeCompare(
+  switch (sort) {
+    case SortOption.PublisherNameAscending:
+    case SortOption.PublisherNameDescending: {
+      const value = (a.name ?? a.publicKey.toBase58()).localeCompare(
         b.name ?? b.publicKey.toBase58(),
       );
+      return sort === SortOption.PublisherNameAscending ? -1 * value : value;
     }
-    case SortField.APY: {
-      return (
+    case SortOption.ApyAscending:
+    case SortOption.ApyDescending: {
+      const value =
         calculateApy({
           isSelf: false,
           selfStake: a.selfStake + a.selfStakeDelta,
@@ -959,20 +956,34 @@ const doSort = (
           poolCapacity: b.poolCapacity,
           poolUtilization: b.poolUtilization + b.poolUtilizationDelta,
           yieldRate,
-        })
-      );
+        });
+      return sort === SortOption.ApyDescending ? -1 * value : value;
     }
-    case SortField.NumberOfFeeds: {
+    case SortOption.NumberOfFeedsAscending: {
       return Number(a.numFeeds - b.numFeeds);
     }
-    case SortField.PoolUtilization: {
-      const value = Number(
-        (a.poolUtilization + a.poolUtilizationDelta) * b.poolCapacity -
-          (b.poolUtilization + b.poolUtilizationDelta) * a.poolCapacity,
-      );
-      return value === 0 ? Number(a.poolCapacity - b.poolCapacity) : value;
+    case SortOption.NumberOfFeedsDescending: {
+      return Number(b.numFeeds - a.numFeeds);
+    }
+    case SortOption.RemainingPoolAscending:
+    case SortOption.RemainingPoolDescending: {
+      if (a.poolCapacity === 0n && b.poolCapacity === 0n) {
+        return 0;
+      } else if (a.poolCapacity === 0n) {
+        return 1;
+      } else if (b.poolCapacity === 0n) {
+        return -1;
+      } else {
+        const remainingPoolA =
+          a.poolCapacity - a.poolUtilization - a.poolUtilizationDelta;
+        const remainingPoolB =
+          b.poolCapacity - b.poolUtilization - b.poolUtilizationDelta;
+        const value = Number(remainingPoolA - remainingPoolB);
+        return sort === SortOption.RemainingPoolDescending ? -1 * value : value;
+      }
     }
-    case SortField.QualityRanking: {
+    case SortOption.QualityRankingDescending:
+    case SortOption.QualityRankingAscending: {
       if (a.qualityRanking === 0 && b.qualityRanking === 0) {
         return 0;
       } else if (a.qualityRanking === 0) {
@@ -980,12 +991,16 @@ const doSort = (
       } else if (b.qualityRanking === 0) {
         return -1;
       } else {
-        return Number(a.qualityRanking - b.qualityRanking);
+        const value = Number(a.qualityRanking - b.qualityRanking);
+        return sort === SortOption.QualityRankingAscending ? -1 * value : value;
       }
     }
-    case SortField.SelfStake: {
+    case SortOption.SelfStakeAscending: {
       return Number(a.selfStake - b.selfStake);
     }
+    case SortOption.SelfStakeDescending: {
+      return Number(b.selfStake - a.selfStake);
+    }
   }
 };
 
@@ -994,14 +1009,16 @@ type SortablePublisherTableHeaderProps = Omit<
   "children"
 > & {
   children: string;
-  field: SortField;
-  sort: { field: SortField; descending: boolean };
-  setSort: Dispatch<SetStateAction<{ field: SortField; descending: boolean }>>;
+  asc: SortOption;
+  desc: SortOption;
+  sort: SortOption;
+  setSort: Dispatch<SetStateAction<SortOption>>;
   alignment?: "left" | "right";
 };
 
 const SortablePublisherTableHeader = ({
-  field,
+  asc,
+  desc,
   sort,
   setSort,
   children,
@@ -1010,11 +1027,8 @@ const SortablePublisherTableHeader = ({
   ...props
 }: SortablePublisherTableHeaderProps) => {
   const updateSort = useCallback(() => {
-    setSort((cur) => ({
-      field,
-      descending: cur.field === field ? !cur.descending : false,
-    }));
-  }, [setSort, field]);
+    setSort((cur) => (cur === desc ? asc : desc));
+  }, [setSort, asc, desc]);
 
   return (
     <th>
@@ -1025,8 +1039,8 @@ const SortablePublisherTableHeader = ({
           className,
         )}
         onPress={updateSort}
-        {...(sort.field === field && { "data-sorted": true })}
-        {...(sort.descending && { "data-descending": true })}
+        {...((sort === asc || sort === desc) && { "data-sorted": true })}
+        {...(sort === desc && { "data-descending": true })}
         data-alignment={alignment ?? "center"}
         {...props}
       >
@@ -1133,13 +1147,13 @@ const Publisher = ({
   );
 
   return compact ? (
-    <div className="border-t border-neutral-600/50 p-4 sm:px-10">
+    <div className="border-t border-neutral-600/50 p-4 sm:px-10 md:pt-8">
       {!isSelf && (
         <div className="flex flex-row items-center justify-between">
           <PublisherName
             className="font-semibold"
-            truncatedClassName="sm:hidden"
-            fullClassName="hidden sm:inline"
+            truncatedClassName="md:hidden"
+            fullClassName="hidden md:inline"
           >
             {publisher}
           </PublisherName>
@@ -1153,24 +1167,40 @@ const Publisher = ({
           />
         </div>
       )}
-      <div className="gap-8 xs:flex xs:flex-row-reverse xs:items-center xs:justify-between">
-        <div className="flex grow flex-col gap-2 xs:items-end">
-          {isSelf && (
-            <StakeToPublisherButton
-              api={api}
-              currentEpoch={currentEpoch}
-              availableToStake={availableToStake}
+      <div
+        className={clsx(
+          "gap-8",
+          isSelf
+            ? "flex flex-row-reverse items-center justify-between"
+            : "xs:flex xs:flex-row-reverse xs:items-center xs:justify-between",
+        )}
+      >
+        {!isSelf && (
+          <div className="flex grow flex-col gap-2 xs:items-end">
+            <UtilizationMeter
               publisher={publisher}
-              yieldRate={yieldRate}
-              isSelf
+              className="mx-auto my-4 w-full grow xs:mx-0 sm:w-auto sm:flex-none"
             />
-          )}
-          <UtilizationMeter
+          </div>
+        )}
+        {isSelf && (
+          <StakeToPublisherButton
+            api={api}
+            currentEpoch={currentEpoch}
+            availableToStake={availableToStake}
             publisher={publisher}
-            className="mx-auto my-4 w-full grow xs:mx-0 sm:w-auto sm:flex-none"
+            yieldRate={yieldRate}
+            isSelf
           />
-        </div>
-        <dl className="flex-none text-xs">
+        )}
+        <dl
+          className={clsx(
+            "flex-none text-xs",
+            isSelf
+              ? "lg:flex lg:flex-row lg:gap-6"
+              : "md:grid md:grid-cols-2 lg:gap-x-10 xl:flex xl:flex-row xl:gap-8",
+          )}
+        >
           {!isSelf && (
             <div className="flex flex-row items-center gap-2">
               <dt className="font-semibold">{"Publisher's Stake:"}</dt>
@@ -1195,6 +1225,12 @@ const Publisher = ({
           </div>
         </dl>
       </div>
+      {isSelf && (
+        <UtilizationMeter
+          publisher={publisher}
+          className="mx-auto my-4 w-full grow xs:mx-0"
+        />
+      )}
       {(warmup !== undefined || staked !== undefined) && (
         <YourPositionsTable
           publisher={publisher}
@@ -1311,7 +1347,7 @@ const UtilizationMeter = ({ publisher, ...props }: UtilizationMeterProps) => {
               }}
               className={clsx(
                 "absolute inset-0 max-w-full",
-                percentage < 100 ? "bg-pythpurple-400" : "bg-fuchsia-900",
+                percentage < 100 ? "bg-pythpurple-400" : "bg-red-800",
               )}
             />
             <div
@@ -1600,23 +1636,61 @@ const hasAnyPositions = ({ positions }: PublisherProps["publisher"]) =>
     positions.cooldown2,
   ].some((value) => value !== undefined && value > 0n);
 
-enum SortField {
-  PublisherName,
-  PoolUtilization,
-  APY,
-  SelfStake,
-  NumberOfFeeds,
-  QualityRanking,
+enum SortOption {
+  PublisherNameDescending,
+  PublisherNameAscending,
+  RemainingPoolDescending,
+  RemainingPoolAscending,
+  ApyDescending,
+  ApyAscending,
+  SelfStakeDescending,
+  SelfStakeAscending,
+  NumberOfFeedsDescending,
+  NumberOfFeedsAscending,
+  QualityRankingDescending,
+  QualityRankingAscending,
 }
 
-const SORT_FIELD_TO_NAME: Record<SortField, string> = {
-  [SortField.PublisherName]: "Publisher Name",
-  [SortField.PoolUtilization]: "Pool Utilization",
-  [SortField.APY]: "Estimated Next APY",
-  [SortField.SelfStake]: "Publisher's Stake",
-  [SortField.NumberOfFeeds]: "Number of Feeds",
-  [SortField.QualityRanking]: "Quality Ranking",
-} as const;
+const getSortName = (sortOption: SortOption) => {
+  switch (sortOption) {
+    case SortOption.PublisherNameDescending: {
+      return "Publisher Name (A-Z)";
+    }
+    case SortOption.PublisherNameAscending: {
+      return "Publisher Name (Z-A)";
+    }
+    case SortOption.RemainingPoolDescending: {
+      return "Most remaining pool";
+    }
+    case SortOption.RemainingPoolAscending: {
+      return "Least remaining pool";
+    }
+    case SortOption.ApyDescending: {
+      return "Highest estimated next APY";
+    }
+    case SortOption.ApyAscending: {
+      return "Lowest estimated next APY";
+    }
+    case SortOption.SelfStakeDescending: {
+      return "Highest publisher's stake";
+    }
+    case SortOption.SelfStakeAscending: {
+      return "Lowest publisher's stake";
+    }
+    case SortOption.NumberOfFeedsDescending: {
+      return "Most feeds";
+    }
+    case SortOption.NumberOfFeedsAscending: {
+      return "Least feeds";
+    }
+    case SortOption.QualityRankingDescending: {
+      return "Best quality ranking";
+    }
+    case SortOption.QualityRankingAscending: {
+      return "Worst quality ranking";
+    }
+  }
+};
 
 class InvalidKeyError extends Error {
   constructor() {

+ 36 - 31
apps/staking/src/components/OracleIntegrityStakingGuide/index.tsx

@@ -126,6 +126,11 @@ export const OracleIntegrityStakingGuide = (
         faq: {
           title: "Adding Tokens FAQ",
           questions: [
+            {
+              question: "Why do I need to add my tokens?",
+              answer:
+                "Adding tokens to the Pyth Staking Dashboard transfers them to your SPL wallet’s staking account. Your tokens will remain under your control on-chain through the Pyth Staking Dashboard.",
+            },
             {
               question: "Where are my added tokens stored?",
               answer:
@@ -233,22 +238,17 @@ export const OracleIntegrityStakingGuide = (
         description: (
           <>
             <p>
-              Navigate to the Oracle Integrity Staking tab to begin staking your
-              tokens to publishers to help secure Pyth Price Feeds.
-            </p>
-
-            <p>
-              Each publisher is assigned a stake pool that typically includes
-              the publisher’s self-stake and delegated stakes from other
-              participants. The rewards distribution protocol programmatically
-              shares rewards first to publishers, and then to stakers supporting
-              them.
+              Once you confirm your choice to stake to a publisher, your tokens
+              will first enter a Warmup Period, which lasts until the end of the
+              current epoch. An epoch is a one-week period starting every
+              Thursday at 00:00 UTC.
             </p>
 
             <p>
-              You can sort publishers by their stake pool details, quality
-              ranking, and more. Once you have chosen a publisher, click Stake
-              and specify the number of tokens you wish to stake to their pool.
+              Tokens in the Warmup Period do not contribute to oracle security
+              and are not eligible for sharing in publisher rewards or
+              penalties. Once the Warmup Period ends, these tokens become staked
+              and will play an active role in strengthening oracle integrity.
             </p>
           </>
         ),
@@ -409,12 +409,12 @@ export const OracleIntegrityStakingGuide = (
                 </p>
                 <p>
                   The Pyth DAO sets a maximum reward rate for stake pools,
-                  currently set at 9%. This rate is achieved for a pool when the
-                  total stake is below the stake cap. If the stake cap is
+                  currently set at 10%. This rate is achieved for a pool when
+                  the total stake is below the stake cap. If the stake cap is
                   exceeded, the reward rate for stakers is reduced.
                 </p>
                 <p>
-                  Publishers charge a fixed percentage (5%) of the rewards from
+                  Publishers charge a fixed percentage (20%) of the rewards from
                   stakers in their stake pool as a delegation fee (net of any
                   slashed amount). The Pyth DAO can vote to adjust this fee
                   structure. Learn more about staking rewards in the
@@ -436,7 +436,7 @@ export const OracleIntegrityStakingGuide = (
                   penalized.
                 </p>
                 <p>
-                  The current slashing rate is capped at 10% of publisher and
+                  The current slashing rate is capped at 5% of publisher and
                   delegated stakes, and this rate can be adjusted by the Pyth
                   DAO. The slashed amounts are sent to the DAO wallet. The Pyth
                   DAO can choose to vote on future decisions for these slashed
@@ -483,16 +483,17 @@ export const OracleIntegrityStakingGuide = (
                     tokens will first undergo a first phase of the Cooldown
                     Period from the time of clicking Unstake to the end of the
                     current epoch. These tokens still actively contribute to
-                    oracle integrity and remain subject to programmatic rewards
-                    and slashing.
+                    oracle integrity and remain eligible to programmatic
+                    rewards.
                   </p>
                   <p>
-                    After this first phase, these tokens will undergo second
+                    After this first phase, these tokens will undergo a second
                     phase in the Cooldown Period lasting one full epoch, during
-                    which the tokens are no longer subject to programmatic
-                    rewards or slashing. Once this phase concludes, your tokens
-                    will become unstaked and can be restaked or withdrawn to
-                    your wallet.
+                    which the tokens are no longer eligible to programmatic
+                    rewards. These tokens are subject to slashing if a misprint
+                    in the previous epoch is identified. Once this phase
+                    concludes, your tokens will become unstaked and can be
+                    restaked or withdrawn to your wallet.
                   </p>
                 </>
               ),
@@ -508,11 +509,15 @@ export const OracleIntegrityStakingGuide = (
               answer: (
                 <>
                   <p>
-                    In the first phase of the <strong>Cooldown Period</strong>
-                    an on-chain protocol consuming Pyth data can choose to raise
-                    a report for a plausible data misprint. The Pythian Council
-                    of the Pyth DAO will then review the reference data provided
-                    and compare against the Pyth data.
+                    Anyone can choose to raise a report for a plausible data
+                    misprint. The Pythian Council of the Pyth DAO will then
+                    review the reference data provided and compare against the
+                    Pyth data to determine whether a slashing event should
+                    occur. The council will have until the end of the epoch
+                    after the epoch of the reported incident to review the
+                    report. The tokens subject to slashing are the tokens
+                    eligible for rewards{" "}
+                    <i>during the epoch of the misprint incident</i>.
                   </p>
                   <p>
                     In the unlikely event that a published aggregate has been
@@ -520,8 +525,8 @@ export const OracleIntegrityStakingGuide = (
                     triggered. The stakes of the subset of publishers who
                     contributed to this incorrect aggregate are programmatically
                     slashed, along with the stakes of anyone who delegated
-                    tokens towards them. Such slashing event occurs during this
-                    same epoch.
+                    tokens towards them. Such slashing event occurs during the
+                    epoch after the epoch of the reported incident.
                   </p>
                   <p>
                     The slashed amounts are sent to the Pyth DAO’s wallet. The

+ 40 - 23
apps/staking/src/components/PublisherFaq/index.tsx

@@ -20,7 +20,7 @@ export const PublisherFaq = (
                 For a comprehensive walkthrough of the OIS program, publishers
                 can refer to the{" "}
                 <Link
-                  href="https://www.notion.so/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=21"
+                  href="https://pyth-network.notion.site/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=4"
                   className="underline"
                   target="_blank"
                 >
@@ -72,8 +72,9 @@ export const PublisherFaq = (
             <>
               <p>
                 Publishers that have locked tokens are{" "}
-                <strong>automatically opted-in</strong> with their existing
-                stake account with the most funds as their main stake account.
+                <strong>automatically opted-in</strong> with their main stake
+                account, which is the stake account in which they last received
+                locked tokens.
               </p>
               <p>
                 Publishers that have never received locked tokens will be
@@ -83,7 +84,7 @@ export const PublisherFaq = (
               <p>
                 If you wish to opt out of rewards, please follow the{" "}
                 <Link
-                  href="https://www.notion.so/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=21"
+                  href="https://pyth-network.notion.site/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=4"
                   className="underline"
                   target="_blank"
                 >
@@ -108,7 +109,7 @@ export const PublisherFaq = (
               <p>
                 Please follow the{" "}
                 <Link
-                  href="https://www.notion.so/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=21"
+                  href="https://pyth-network.notion.site/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=4"
                   className="underline"
                   target="_blank"
                 >
@@ -131,12 +132,16 @@ export const PublisherFaq = (
           answer: (
             <>
               <p>
-                Yes. Opting in makes you subject to OIS’ decentralized staking
-                rewards and slashing mechanisms. Programmatic rewards for
-                staking for publishers are determined by a number of stake pool
-                parameters. Programmatic slashing is capped at a 5% percentage
-                amount of the total stake within a publisher’s stake pool. The
-                Pyth DAO can vote to adjust these parameters.
+                Opting-in makes you subject to staking rewards from delegate
+                stakers but does not make you subject to slashing unless you
+                stake to your own stake pool.
+              </p>
+              <p>
+                Programmatic rewards for staking for publishers are determined
+                by a number of stake pool parameters. Programmatic slashing is
+                capped at a 5% percentage amount of the total stake within a
+                publisher’s stake pool. The Pyth DAO can vote to adjust these
+                parameters.
               </p>
               <p>
                 Please refer to the{" "}
@@ -176,7 +181,7 @@ export const PublisherFaq = (
                 It is important to understand the requirements,
                 responsibilities, and implications of participating in OIS. The{" "}
                 <Link
-                  href="https://www.notion.so/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=21"
+                  href="https://pyth-network.notion.site/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=4"
                   className="underline"
                   target="_blank"
                 >
@@ -311,7 +316,7 @@ export const PublisherFaq = (
               <p>
                 Please refer to the{" "}
                 <Link
-                  href="https://www.notion.so/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=21"
+                  href="https://pyth-network.notion.site/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=4"
                   className="underline"
                   target="_blank"
                 >
@@ -336,7 +341,7 @@ export const PublisherFaq = (
               <p>
                 Please refer to the{" "}
                 <Link
-                  href="https://www.notion.so/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=21"
+                  href="https://pyth-network.notion.site/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=4"
                   className="underline"
                   target="_blank"
                 >
@@ -369,7 +374,7 @@ export const PublisherFaq = (
               <p>
                 Please refer to the{" "}
                 <Link
-                  href="https://www.notion.so/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=21"
+                  href="https://pyth-network.notion.site/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=4"
                   className="underline"
                   target="_blank"
                 >
@@ -387,7 +392,7 @@ export const PublisherFaq = (
               <p>
                 Please refer to the{" "}
                 <Link
-                  href="https://www.notion.so/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=21"
+                  href="https://pyth-network.notion.site/Oracle-Integrity-Staking-OIS-Guide-for-Pyth-Network-MDPs-2755c872a7c44aefabfa9987ba7ec8ae?pvs=4"
                   className="underline"
                   target="_blank"
                 >
@@ -492,15 +497,27 @@ export const PublisherFaq = (
           answer: (
             <>
               <p>
-                An on-chain protocol can report a potential data error for the
-                previous epoch. If a report is raised, the Pythian Council
-                reviews it by comparing it with the reference data. If a
-                discrepancy is confirmed, a slashing event is triggered.
+                Anyone can choose to raise a report for a plausible data
+                misprint. The Pythian Council of the Pyth DAO will then review
+                the reference data provided and compare against the Pyth data to
+                determine whether a slashing event should occur.
+              </p>
+              <p>
+                The council will have until the end of the epoch after the epoch
+                of the reported incident to review the report. The tokens
+                subject to slashing are the tokens eligible for rewards{" "}
+                <em>during the epoch of the misprint incident</em>.
+              </p>
+              <p>
+                If a discrepancy is confirmed, a slashing event is triggered. In
+                this event, the stakes of publishers who contributed to the
+                incorrect aggregate will be programmatically slashed, along with
+                the stakes of anyone who delegated tokens towards their stake
+                pools.
               </p>
               <p>
-                During the same epoch, the stakes of the subset of publishers
-                involved in the error and their stakers are slashed. Slashed
-                amounts are sent to the DAO wallet for future decisions.
+                Slashed amounts are sent to the DAO wallet. The Pyth DAO can
+                choose to vote on future decisions for these slashed amounts.
               </p>
               <p>
                 Please refer to the{" "}

+ 1079 - 0
apps/staking/src/components/TermsOfService/index.tsx

@@ -0,0 +1,1079 @@
+import clsx from "clsx";
+import type { ReactNode, HTMLProps, ComponentProps } from "react";
+
+import { Link as BaseLink } from "../Link";
+
+export const TermsOfService = () => (
+  <main className="mx-auto flex max-w-prose flex-col gap-10 py-6 md:gap-16 md:py-20">
+    <div>
+      <h1 className="text-3xl font-light md:text-4xl">Terms of Service</h1>
+      <h2 className="text-sm opacity-60">Last updated: September 2024</h2>
+    </div>
+    <dl className="flex list-inside list-[upper-alpha] flex-col gap-10 md:gap-16">
+      <Section title="Scope">
+        <Paragraph>
+          These Terms of Service (“<strong>Terms</strong>”) govern your access
+          to and use of the website located at{" "}
+          <Link href="/">https://staking.pyth.network</Link> (“
+          <strong>Site</strong>”) operated by Pyth Data Association,
+          Grabenstrasse 25, 6340 Baar, Switzerland (“
+          <strong>Association</strong>
+          ”, “<strong>we</strong>”, “<strong>us</strong>”, or “
+          <strong>our</strong>”) and the use of our tools available therein (the
+          “<strong>Tools</strong>” and together with the Site, “
+          <strong>Services</strong>”).
+        </Paragraph>
+        <Paragraph>
+          “<strong>You</strong>”, “<strong>your</strong>” and “
+          <strong>User(s)</strong>” refers to anybody who accesses or in any way
+          uses the Services. If you are accessing or in any way using the
+          Services on behalf of a company (such as your employer) or other legal
+          entity, you represent and warrant that you have the authority to bind
+          that entity to these Terms and, in that case, “you”, “your” or
+          “User(s)” will refer to that entity.
+        </Paragraph>
+        <div>
+          <Paragraph>
+            Please read the Terms carefully before you start accessing or in any
+            way using the Services. By accessing or in any way using the
+            Services or by clicking to accept or agree to these Terms when this
+            option is made available to you, you accept and agree to be bound
+            and abide by these Terms in addition to:
+          </Paragraph>
+          <UnorderedList>
+            <li>
+              our{" "}
+              <Link
+                href="https://pythdataassociation.com/terms-of-use"
+                target="_blank"
+              >
+                Website Terms of Use
+              </Link>
+              , incorporated herein by reference; and
+            </li>
+            <li>
+              our{" "}
+              <Link
+                href="https://pythdataassociation.com/privacy-policy"
+                target="_blank"
+              >
+                Privacy Policy
+              </Link>
+              , incorporated herein by reference.
+            </li>
+          </UnorderedList>
+        </div>
+
+        <Paragraph>
+          If you do not agree to these Terms, you are not permitted access or in
+          any way use the Services. In the event of any conflict between these
+          Terms, the{" "}
+          <Link
+            href="https://pythdataassociation.com/terms-of-use"
+            target="_blank"
+          >
+            Website Terms of Use
+          </Link>{" "}
+          or the{" "}
+          <Link
+            href="https://pythdataassociation.com/privacy-policy"
+            target="_blank"
+          >
+            Privacy Policy
+          </Link>
+          , the terms of these Terms shall control.
+        </Paragraph>
+      </Section>
+      <Section title="External Content">
+        <Paragraph>
+          The Services provide links to third-party content (which may include
+          smart contracts and cryptographically secured protocols). We provide
+          such links only as a convenience and are not responsible for the
+          content, products or services on or available from those resources or
+          links displayed on such websites or items. USE OF ANY THIRD-PARTY
+          PROTOCOL OR SMART CONTRACT LINKED ON THE SERVICES IS AT YOUR OWN RISK.
+          THESE TERMS DO NOT GOVERN YOUR USE OF ANY PROTOCOL OR SMART CONTRACT
+          OTHER THAN THE SERVICES. PLEASE CONSULT APPLICABLE LICENSES AND USER
+          AGREEMENTS FOR INFORMATION REGARDING YOUR RIGHTS AND RISKS ASSOCIATED
+          WITH YOUR USE OF AND ACCESS TO THESE MATERIALS.
+        </Paragraph>
+      </Section>
+      <Section title="Acceptance of the Terms">
+        <Paragraph>
+          By accessing or in any way using the Services, you automatically agree
+          to these Terms. Your access and use of the Services is conditioned on
+          your acceptance of and/or compliance with these Terms, the{" "}
+          <Link
+            href="https://pythdataassociation.com/terms-of-use"
+            target="_blank"
+          >
+            Website Terms of Use
+          </Link>{" "}
+          and our{" "}
+          <Link
+            href="https://pythdataassociation.com/privacy-policy"
+            target="_blank"
+          >
+            Privacy Policy
+          </Link>
+          . These Terms shall exclusively apply; any of your terms and
+          conditions that contradict or deviate from these Terms shall only be
+          valid if and to the extent that we have expressly agreed to them. If
+          you do not agree with any part of these Terms, you may not access or
+          use the Services.
+        </Paragraph>
+      </Section>
+      <Section title="Changes to these Terms">
+        <Paragraph>
+          These Terms may be changed at any time at our sole discretion and
+          without prior notice. All changes are effective immediately upon
+          posting. By continuing to use the Services after revised Terms have
+          been posted, you signify your acceptance of and agreement to the
+          changes.
+        </Paragraph>
+        <Paragraph>
+          You are responsible to regularly review these Terms to stay informed
+          of any updates, as they are legally binding on you.
+        </Paragraph>
+      </Section>
+      <Section title="Accessing the Services">
+        <Paragraph>
+          We reserve the right to modify or discontinue the Services at our sole
+          discretion and without prior notice. We do not guarantee continuous
+          availability or uninterrupted access to the Services. We will not be
+          liable if all or any part of the Services is unavailable at any time
+          or for any duration.
+        </Paragraph>
+        <div>
+          <Paragraph>You are responsible to:</Paragraph>
+          <UnorderedList>
+            <li>
+              make all necessary arrangements to access and use the Services;
+              and
+            </li>
+            <li>
+              ensure that anyone who access or uses the Services through your
+              internet connection is aware of and complies with these Terms.
+            </li>
+          </UnorderedList>
+        </div>
+      </Section>
+      <Section title="Personal Restrictions">
+        <Paragraph>
+          The Services are only accessible to Users who are at least 18 years
+          old. The Services are not meant for individuals under 18. By accessing
+          or using the Services, you confirm and guarantee that you (i) are 18
+          years old or older, (ii) are not prohibited from accessing or using
+          the Services by any relevant laws. If you do not fulfill these
+          criteria, you are not permitted to access or use the Services.
+        </Paragraph>
+      </Section>
+      <Section title="Local Restrictions">
+        <Paragraph>
+          We are based in Switzerland and do not make any claims that the
+          Services are accessible or appropriate outside of Switzerland.
+          Accessing and/or using the Services may be illegal for certain
+          individuals or in certain countries. If you choose to access and/or
+          use the Services from outside Switzerland, you do so at your own risk
+          and are responsible for complying with local laws.
+        </Paragraph>
+        <Paragraph>
+          EXCEPT EXPLICTLY PROVIDED IN THESE TERMS, THE SERVICES ARE NOT
+          DEVELOPED FOR, AND ARE NOT AVAILABLE TO PERSONS OR ENTITIES WHO RESIDE
+          IN, ARE LOCATED IN, ARE INCORPORATED IN, OR HAVE A REGISTERED OFFICE
+          OR PRINCIPAL PLACE OF BUSINESS IN THE UNITED STATES OF AMERICA OR THE
+          UNITED KINGDOM (COLLECTIVELY, “<strong>BLOCKED PERSONS</strong>”).
+          MOREOVER, THE SERVICES ARE NOT OFFERED TO PERSONS OR ENTITIES WHO
+          RESIDE IN, ARE CITIZENS OF, ARE LOCATED IN, ARE INCORPORATED IN, OR
+          HAVE A REGISTERED OFFICE OR PRINCIPAL PLACE OF BUSINESS IN ANY
+          RESTRICTED JURISDICTION OR COUNTRY SUBJECT TO ANY SANCTIONS OR
+          RESTRICTIONS PURSUANT TO ANY APPLICABLE LAW, INCLUDING THE CRIMEA
+          REGION, CUBA, IRAN, NORTH KOREA, SYRIA, MYANMAR (BURMA, DONETSK,
+          LUHANSK, OR ANY OTHER COUNTRY TO WHICH THE UNITED STATES, THE UNITED
+          KINGDOM, THE EUROPEAN UNION, SWITZERLAND OR ANY OTHER JURISDICTIONS
+          EMBARGOES GOODS OR IMPOSES SIMILAR SANCTIONS, INCLUDING THOSE LISTED
+          ON OUR SERVICES (COLLECTIVELY, THE “
+          <strong>RESTRICTED JURISDICTIONS</strong>” AND EACH A “
+          <strong>RESTRICTED JURISDICTION</strong>”) OR ANY PERSON OWNED,
+          CONTROLLED, LOCATED IN OR ORGANIZED UNDER THE LAWS OF ANY JURISDICTION
+          UNDER EMBARGO OR CONNECTED OR AFFILIATED WITH ANY SUCH PERSON
+          (COLLECTIVELY, “<strong>RESTRICTED PERSONS</strong>”). THE SERVICES
+          WERE NOT SPECIFICALLY DEVELOPED FOR, AND IS NOT AIMED AT OR BEING
+          ACTIVELY MARKETED TO, PERSONS OR ENTITIES WHO RESIDE IN, ARE LOCATED
+          IN, ARE INCORPORATED IN, OR HAVE A REGISTERED OFFICE OR PRINCIPAL
+          PLACE OF BUSINESS IN THE EUROPEAN UNION. THERE ARE NO EXCEPTIONS. IF
+          YOU ARE A BLOCKED PERSON OR A RESTRICTED PERSON, THEN DO NOT USE OR
+          ATTEMPT TO ACCESS AND/OR USE THE SERVICES. USE OF ANY TECHNOLOGY OR
+          MECHANISM, SUCH AS A VIRTUAL PRIVATE NETWORK (“<strong>VPN</strong>”)
+          TO CIRCUMVENT THE RESTRICTIONS SET FORTH HEREIN IS PROHIBITED.
+        </Paragraph>
+      </Section>
+      <Section title="Services">
+        <Paragraph>
+          You access the Services by connecting a digital wallet holding digital
+          assets to smart contract systems offered by third parties that
+          communicate with the Services. You understand that we do not hold your
+          digital assets, have no power of disposal over your digital assets, or
+          take any custody of them. We have no access to your digital assets or
+          funds.
+        </Paragraph>
+        <Paragraph>
+          The Services allow you to participate in different non-custodial
+          staking mechanisms on the Pyth Network (“<strong>Pyth Staking</strong>
+          ”), an open-source oracle network (“<strong>Pyth Network</strong>”)
+          governed by the Pyth DAO that provides financial market data to
+          blockchain-based applications to provide high fidelity financial
+          market data to the blockchain industry powered by a blockchain
+          protocol on the Solana network (“<strong>Pyth Protocol</strong>”).
+        </Paragraph>
+        <Paragraph>
+          For the avoidance of doubt, the Association does not control the Pyth
+          Network and cannot control activity on the Pyth Network, including
+          Pyth Staking, the activities of persons who develop and use
+          applications on the Pyth Network, the production of data on the Pyth
+          Network, or use of the Pyth Network. The Pyth Network is an
+          open-source protocol that is governed by the Pyth DAO.
+        </Paragraph>
+        <Paragraph>
+          We are not responsible for the keys to any digital assets or your seed
+          phrase, or their loss or disclosure to others. The Association does
+          not maintain your keys or your seed phrase, and is not responsible for
+          their safe keeping. It is your responsibility at all times to ensure
+          you have such credentials and maintain them securely. ANY LOSSES YOU
+          SUFFER RELATING TO YOUR DIGITAL ASSET TRANSACTIONS, DIGITAL KEYS AND
+          WALLETS, AND EXCHANGES ARE YOUR SOLE RESPONSIBILITY, AND YOU HEREBY
+          INDEMNIFY US, AGREE TO DEFEND US, AND HOLD US HARMLESS AGAINST ANY
+          CLAIMS OR LOSSES THAT YOU OR ANYONE ELSE SUFFERS AS A RESULT OF YOUR
+          DIGITAL ASSET TRANSACTIONS, EVEN IF YOU INITIATED YOUR TRANSACTION BY
+          ACCESSING OUR SERVICES. If, once you use the Services and participate
+          in Pyth Staking, and your digital assets are somehow transferred to a
+          third party you didn’t intend to have them, it is your responsibility
+          to get them back. PLEASE KEEP YOUR SEED PHRASE AND DIGITAL KEYS SAFE,
+          AS THE ASSOCIATION DOES NOT HAVE THEM AND DOES NOT KNOW THEM. IF YOU
+          LOSE THE KEYS OR SEED PHRASE, YOU MAY LOSE ACCESS TO YOUR DIGITAL
+          ASSETS.
+        </Paragraph>
+        <Paragraph>
+          You also understand that we do not act as your transaction brokers,
+          financial intermediary, or financial advisors or give you any advice
+          of any kind with respect to what digital assets you choose to hold in
+          your wallet or any staking thereof. As with Pyth Staking you can
+          participate in via the Services, it is your responsibility and you are
+          solely responsible for the contents of your digital wallet, your
+          staking decisions, how and when you stake digital assets and with
+          whom. It is also your responsibility to ensure you understand digital
+          assets, how they work, what their value is, and about staking such
+          digital assets, as there are significant risks in doing so, all of
+          which you solely assume.
+        </Paragraph>
+        <Paragraph>
+          Also note that the digital wallet you use for the Services may not
+          connect or stake of all digital assets. It is your responsibility to
+          ensure compatibility with the Pyth Network.
+        </Paragraph>
+        <Paragraph>
+          We may suspend your access to or use of, or cancel your access to or
+          use of the Services for any reason, including if we believe you have
+          engaged in or are about to engage in any kind of fraud, if required
+          pursuant to applicable laws, or you violate these Terms. We may
+          provide you with notice of suspension, but do not undertake an
+          obligation to do so. We may change the functionality of the Services
+          at any time, which means some features could no longer be supported
+          after a time. You acknowledge that this is the case, and accept this
+          risk. Given that the digital wallets are non-custodial, we do not
+          perform any activities to vet Users prior to allowing them to create
+          their digital wallets or stake digital assets. You acknowledge that
+          this is a risk you accept when you interact with the wallet or other
+          users of the Services.
+        </Paragraph>
+        <Paragraph>
+          Our Services may at times make mistakes. You accept the risk that your
+          transactions may be improperly processed, or not processed at all. We
+          will not be liable for any such event. You hereby hold us harmless
+          from any such event. We offer no guarantees and shall not provide any
+          refunds for any services you paid for staking digital assets, even if
+          you lose such digital assets.
+        </Paragraph>
+      </Section>
+      <Section title="General Prohibitions">
+        <div>
+          <Paragraph>You agree not to do any of the following:</Paragraph>
+          <UnorderedList>
+            <li>
+              use the Services for the purpose of exploiting, harming, or
+              attempting to exploit or harm minors in any way by exposing them
+              to inappropriate content, asking for personally identifiable
+              information, or otherwise;
+            </li>
+            <li>
+              access, tamper with, or use non-public areas of the Services, our
+              computer systems, or the technical delivery systems of our
+              providers;{" "}
+            </li>
+            <li>
+              attempt to probe, scan or test the vlinerability of any
+              Association’s system or network or breach any security or
+              authentication measures;{" "}
+            </li>
+            <li>
+              avoid, bypass, remove, deactivate, impair, descramble or otherwise
+              circumvent any technological measure implemented by us or any of
+              our providers or any other third-party (including another User) to
+              protect the Services;{" "}
+            </li>
+            <li>
+              attempt to access or search the Services or download content from
+              the Services using any engine, software, tool, agent, device or
+              mechanism (including spiders, robots, crawlers, data mining tools
+              or the like) other than the software and/or search agents provided
+              by us or other generally available third-party web browsers;{" "}
+            </li>
+            <li>
+              use any manual process to monitor the Services or for any other
+              unauthorized purpose without our prior written consent;
+            </li>
+            <li>
+              send any unsolicited or unauthorized advertising, promotional
+              materials, email, junk mail, spam, chain letters or other form of
+              solicitation;{" "}
+            </li>
+            <li>
+              use any meta tags or other hidden text or metadata utilizing an
+              Association’s trademark, logo URL or product name without our
+              express written consent;{" "}
+            </li>
+            <li>
+              use the Services, or any portion thereof, in any manner not
+              permitted by these Terms;{" "}
+            </li>
+            <li>
+              forge any TCP/IP packet header or any part of the header
+              information in any email or newsgroup posting, or in any way use
+              the Services to send altered, deceptive or false
+              source-identifying information;{" "}
+            </li>
+            <li>
+              attempt to decipher, decompile, disassemble or reverse engineer
+              any of the software used to provide the Services;{" "}
+            </li>
+            <li>
+              use, transmit, introduce or install any code, files, scripts,
+              agents or programs intended to do harm or allow unauthorized
+              access, including, for example, viruses, worms, time bombs, back
+              doors and Trojan horses (collectively, “
+              <strong>Malicious Code</strong>”) on or through the Services, or
+              accessing or attempting to access the Services for the purpose of
+              infiltrating a computer or computing system or network, or
+              damaging the software components of the Services, or the systems
+              of the hosting provider, any other suppliers or service provider
+              involved in providing the Services, or another User;
+            </li>
+            <li>
+              distribute Malicious Code or other items of a destructive or
+              deceptive nature;
+            </li>
+            <li>
+              interfere with, or attempt to interfere with, the access of any
+              User, host or network, including, without limitation, sending a
+              virus, overloading, flooding, spamming, mail-bombing the Services,
+              or attacking the Services via a denial-of-service attack or a
+              distributed denial-of-service attack;
+            </li>
+            <li>
+              collect or store any personally identifiable information from the
+              Services from other Users of the Services without their express
+              permission;{" "}
+            </li>
+            <li>
+              impersonate or attempt to impersonate the Association, an
+              Association’s employee or representative, another User, or any
+              other person or entity (including, without limitation, by using
+              identifiers associated with any of the foregoing).;{" "}
+            </li>
+            <li>
+              reverse look-up, track or seek to track any information of any
+              other Users or visitors of the Services;{" "}
+            </li>
+            <li>
+              take any actions that imposes an unreasonable or
+              disproportionately large load on the infrastructure of systems or
+              networks of the Services, or the infrastructure of any systems or
+              networks connected to the Services;{" "}
+            </li>
+            <li>
+              use the Services, directly or indirectly, for or in connection
+              with money laundering, terrorist financing, or other illicit
+              financial activity;
+            </li>
+            <li>
+              use the Services for market manipliation, regardless of whether
+              prohibited by law);
+            </li>
+            <li>
+              use the Services to participate in fundraising for a business,
+              protocol, or platform; or fabricate in any way any transaction or
+              process related thereto;{" "}
+            </li>
+            <li>
+              disguise or interfere in any way with the IP address of the
+              computer you are using to access or use the Services or that
+              otherwise prevents us from correctly identifying the IP address of
+              the computer you are using to access the Services;{" "}
+            </li>
+            <li>
+              engage in any other conduct that restricts or inhibits anyone’s
+              use or enjoyment of the Services, or which, as determined by us,
+              may harm the Association or Users of the Services or expose them
+              to liability;
+            </li>
+            <li>use the Services in or from any Restricted Jurisdictions;</li>
+            <li>
+              use the Services if you if you are a Blocked or Restricted Person
+              (or on their behalf);
+            </li>
+            <li>
+              use the Services in any way that violates any applicable federal,
+              state, local, or international law or regliation (including,
+              without limitation, any laws regarding the export of data or
+              software to and from the United States, Canada, European Union,
+              Switzerland or other countries); or
+            </li>
+            <li>
+              encourage or enable any other individual to do any of the
+              foregoing.{" "}
+            </li>
+          </UnorderedList>
+        </div>
+        <Paragraph>
+          The Association is not obligated to monitor access to or use of the
+          Services or to review or edit any content. However, we reserve the
+          right to do so in our discretion, if we choose. We reserve the right,
+          but are not obligated, to remove or disable access to any content, at
+          any time and without notice, including, but not limited to, if we, at
+          our sole discretion, consider it objectionable or in violation of
+          these Terms. We have the right to investigate violations of these
+          Terms or conduct that affects the Services. We may also consult and
+          cooperate with law enforcement authorities to prosecute users who
+          violate the law.
+        </Paragraph>
+      </Section>
+      <Section title="Intellectual Property and Trademarks">
+        <Paragraph>
+          The Services and their entire contents, features, and functionality
+          (including but not limited to all information, software, text,
+          displays, images, video, and audio, and the design, selection, and
+          arrangement thereof), other than third-party content, are owned by the
+          Association, its licensors, or other providers of such material and
+          are protected by copyright, trademark, patent, trade secret, and other
+          intellectual property or proprietary rights laws.
+        </Paragraph>
+        <Paragraph>
+          Accessing or using the Services does not grant you any license or
+          rights to use the intellectual property or trademarks of the
+          Association, its licensors, its partners, or any third party, except
+          as expressly permitted by these Terms. Any unauthorized use of such
+          intellectual property may result in legal action.
+        </Paragraph>
+        <div>
+          <Paragraph>
+            You must not reproduce, distribute, modify, create derivative works
+            of, publicly display, publicly perform, republish, download, store,
+            or transmit any of the material on our Services, except as follows:
+          </Paragraph>
+          <UnorderedList>
+            <li>
+              your computer may temporarily store copies of such materials in
+              RAM incidental to your accessing and viewing those materials;
+            </li>
+            <li>
+              you may store files that are automatically cached by your web
+              browser for display enhancement purposes; and/or
+            </li>
+            <li>
+              you may print or download one copy of a reasonable number of pages
+              of the Site for your own use and not for further reproduction,
+              publication, or distribution.
+            </li>
+          </UnorderedList>
+        </div>
+        <div>
+          <Paragraph>You must not:</Paragraph>
+          <UnorderedList>
+            <li>modify copies of any materials from the Services;</li>
+            <li>
+              use any illustrations, photographs, video or audio sequences, or
+              any graphics separately from the accompanying text; and/or
+            </li>
+            <li>
+              delete or alter any copyright, trademark, or other proprietary
+              rights notices from copies of materials from the Services.
+            </li>
+          </UnorderedList>
+        </div>
+        <Paragraph>
+          If you print, copy, modify, download, or otherwise use or provide any
+          other person with access to any part of the Services in breach of the
+          Terms, your right to access and/or use the Services will stop
+          immediately and you must, at our option, return or destroy any copies
+          of the materials you have made. No right, title, or interest in or to
+          the Services or any content on the Services is transferred to you, and
+          all rights not expressly granted are reserved by the Association. Any
+          use of the Services not expressly permitted by these Terms is a breach
+          of these Terms and may violate copyright, trademark, and other laws.
+        </Paragraph>
+        <Paragraph>
+          The Association’s name, and all trademarks, logos, taglines, service
+          names, designs, and slogans on the Services are trademarks of the
+          Association or its affiliates or licensors. You must not use such
+          marks without our prior written permission.
+        </Paragraph>
+      </Section>
+      <Section title="Taxes and Fraud">
+        <Paragraph>
+          Depending on your location of residence, you may owe taxes on amounts
+          you earn after staking digital assets. It is your responsibility to
+          ensure you have accounted for, reported to the proper governmental
+          authority, and paid all such taxes to the applicable governmental
+          authority. We do not undertake any obligation to report any such
+          taxes, nor collect or disburse them on your behalf. The taxes you owe
+          are solely your responsibility. You hold us harmless and release us
+          from and against any claims, losses, damages or demands arising in
+          connection with taxes you may owe as a result of your use of the
+          Services.
+        </Paragraph>
+        <Paragraph>
+          If we believe that you have engaged in or been a participant in any
+          fraudulent transaction, we reserve the right to take any action we
+          think appropriate, including forwarding your information and
+          information about the transactions we believe or suspect to be
+          fraudulent to applicable law enforcement agencies, which may result in
+          civil or criminal penalties or other actions against you.
+        </Paragraph>
+      </Section>
+      <Section title="Confidentiality">
+        <Paragraph>
+          You and the Association recognize that each has a legitimate interest
+          in maintaining confidentiality regarding these Terms, the subject
+          matter of these Terms, or any other agreements, documents, or
+          transactions referred to or contemplated herein and all trade secrets,
+          confidential and/or proprietary knowledge or information of each other
+          which you and/or the Association may receive or obtain as a result of
+          entering into or performing its obligations under these Terms
+          (collectively, “<strong>Confidential Information</strong>”).
+        </Paragraph>
+        <Paragraph>
+          You and the Association undertake to the other that you and/or the
+          Association shall keep the Confidential Information in the strictest
+          confidence, and shall not, without the prior written consent of the
+          other disclosing the Confidential Information, use or disclose to any
+          person Confidential Information, information relating to these Terms,
+          or the transactions contemplated hereunder it has or acquires or
+          information which by its nature ought to be regarded as confidential
+          (including without limitation, any business information in respect of
+          the other which is not directly applicable or relevant to the
+          transactions contemplated by these Terms).
+        </Paragraph>
+        <div>
+          <Paragraph>
+            The foregoing shall not prohibit disclosure or use of any
+            Confidential Information if and to the extent:
+          </Paragraph>
+          <UnorderedList>
+            <li>
+              the disclosure or use is required by law or any government body;
+            </li>
+            <li>
+              the disclosure or use is required for the purpose of any arbitral
+              or judicial proceedings arising out of these Terms or any other
+              agreement entered into under or pursuant to these Terms;
+            </li>
+            <li>
+              the disclosure is made to your and/or our professional advisers on
+              a need-to-know basis and on terms that such professional advisers
+              undertake to comply with this section L. in respect of such
+              information as if they were a party to these Terms;
+            </li>
+            <li>
+              the information is or becomes publicly available (other than as a
+              result of any breach of confidentiality);
+            </li>
+            <li>
+              the disclosing party has given prior written approval to the
+              disclosure or use; or
+            </li>
+            <li>
+              the Confidential Information is already in the lawful possession
+              of the party receiving such information (as evidenced by written
+              records) at the time of disclosure.
+            </li>
+          </UnorderedList>
+        </div>
+      </Section>
+      <Section title="User’s Representations and Indemnification">
+        <div>
+          <Paragraph>
+            By using the Services, you represent and warrant that:
+          </Paragraph>
+          <UnorderedList>
+            <li>
+              you will not use the Services for any illegal or unauthorized
+              purpose;{" "}
+            </li>
+            <li>
+              your use of the Servies will not violate any applicable law or
+              regulation;
+            </li>
+            <li>
+              you are responsible for the payment of all relevant duties and/or
+              charges and/or taxes arising from the course of your use of the
+              Services;
+            </li>
+            <li>
+              you will not act in any way that violates any applicable policies
+              and terms posted on our Site, as may be revised from time to time,
+              or included in any other agreement between you and us (including,
+              without limitation in these Terms).
+            </li>
+          </UnorderedList>
+        </div>
+        <Paragraph>
+          {`You agree to indemnify, defend, and hold harmless the Association, including our subsidiaries, affiliates, licensors, service providers, directors, officers, employees, agents, partners, contractors, successors, and assigns from and against any and all losses, damages, liabilities, claims, demands, actions, judgments, awards, costs, expenses, or fees (including reasonable attorneys' fees and expenses) arising out of or in connection with any third-party claims or any action, adjudication or decision taken against the Association by any government body, in each case, directly or indirectly arising (in whole or in part) out of any breach of these Terms.`}
+        </Paragraph>
+        <Paragraph>
+          Notwithstanding the foregoing, we reserve the right, at your expense,
+          to assume the exclusive defense and control of any matter for which
+          you are obligated to indemnify us. You agree to cooperate fully in the
+          defense of any such claim, action, or proceeding. This includes
+          allowing us to control the investigation, defense, and settlement of
+          any legal claim subject to your indemnification obligations. We will
+          make reasonable efforts to notify you of any such claim, action, or
+          proceeding as soon as we become aware of it.
+        </Paragraph>
+        <Paragraph>
+          Your indemnification obligations under this section M. survive the
+          termination of these Terms.
+        </Paragraph>
+      </Section>
+      <Section title="Termination">
+        <div>
+          <Paragraph>
+            We reserve the right to terminate or suspend you from accessing and
+            using the Services immediately, without prior notice or liability,
+            for any reason, including but not limited to the following:
+          </Paragraph>
+          <UnorderedList>
+            <li>if you violate any of these Terms;</li>
+            <li>
+              if we are required to do so by law or a regulatory authority;
+            </li>
+            <li>if you engage in fraudulent or illegal activities; and/or</li>
+            <li>
+              if you act in a manner that could harm the reputation or
+              operations of the Association.
+            </li>
+          </UnorderedList>
+        </div>
+        <Paragraph>
+          Upon termination of your access to or use of the Services, you remain
+          responsible for any obligations or liabilities incurred during your
+          use of the Services.
+        </Paragraph>
+        <Paragraph>
+          Termination of your access to or use of the Services does not limit
+          any liability you may have incurred. All provisions of these Terms,
+          which by their nature should survive termination, shall survive,
+          including but not limited to intellectual property and trademarks
+          (section J.), warranty disclaimers (section O.) indemnity (section
+          M.), and limitations of liability (sections Q.) and sections R., S.,
+          T., U., V., W, X.
+        </Paragraph>
+        <Paragraph>
+          The Association shall not be liable to you or any third party for any
+          termination of your access to or use of the Services. Any termination
+          under this section N. shall be at the Association’s sole discretion.
+        </Paragraph>
+        <Paragraph>
+          Without limiting the foregoing, we have the right to fully cooperate
+          with any law enforcement authorities or court order requesting or
+          directing us to disclose the identity or other information of anyone
+          posting any materials on or through the Services. YOU WAIVE AND HOLD
+          HARMLESS THE ASSOCIATION AND ITS AFFILIATES, LICENSEES AND SERVICE
+          PROVIDERS FROM AND AGAINST ANY CLAIMS RESULTING FROM ANY ACTION TAKEN
+          BY ANY OF THE FOREGOING PARTIES DURING, OR TAKEN AS A CONSEQUENCE OF,
+          INVESTIGATIONS BY EITHER SUCH PARTIES OR LAW ENFORCEMENT AUTHORITIES.
+        </Paragraph>
+      </Section>
+      <Section title="Linking to the Site and Social Media Features">
+        <Paragraph>
+          You may link to the Site, provided you do so in a way that is fair and
+          legal and does not damage our reputation or take advantage of it, but
+          you must not establish a link in such a way as to suggest any form of
+          association, approval, or endorsement on our part without our express
+          written consent.
+        </Paragraph>
+        <div>
+          <Paragraph>
+            The Site may provide certain features that enable you to:
+          </Paragraph>
+          <UnorderedList>
+            <li>
+              link from your own or certain third-party websites to certain
+              content on the Site;
+            </li>
+            <li>
+              send emails or other communications with certain content, or links
+              to certain content, on the Site; or
+            </li>
+            <li>
+              cause limited portions of content on the Site to be displayed or
+              appear to be displayed on your own or certain third-party
+              websites.
+            </li>
+          </UnorderedList>
+        </div>
+        <div>
+          <Paragraph>
+            You may use these features solely as they are provided by us, and
+            solely with respect to the content they are displayed with and
+            otherwise in accordance with any additional terms and conditions we
+            provide with respect to such features. Subject to the foregoing, you
+            must not:
+          </Paragraph>
+          <UnorderedList>
+            <li>
+              establish a link from any website that is not owned by you;{" "}
+            </li>
+            <li>
+              cause the Site or portions of it to be displayed on, or appear to
+              be displayed by, any other website, for example, framing, deep
+              linking, or in-line linking;
+            </li>
+            <li>link to any part of the Site;</li>
+            <li>
+              otherwise take any action with respect to the materials on the
+              Site that is inconsistent with any other provision of these Terms.
+            </li>
+          </UnorderedList>
+        </div>
+        <Paragraph>
+          The website from which you are linking, or on which you make certain
+          content accessible, must comply in all respects with these Terms.
+        </Paragraph>
+        <Paragraph>
+          You agree to cooperate with us in causing any unauthorized framing or
+          linking immediately to stop. We reserve the right to withdraw linking
+          permission without notice.
+        </Paragraph>
+        <Paragraph>
+          We may disable all or any social media features and any links at any
+          time without notice in our discretion.
+        </Paragraph>
+      </Section>
+      <Section title="Warranties Disclaimers and Risk Notices">
+        <Paragraph>
+          YOU EXPRESSLY ACKNOWLEDGE AND AGREE THAT YOUR ACCESS TO AND USE OF THE
+          SERVICES, THEIR CONTENTS, AND ANY SERVICES OR ITEMS OBTAINED THROUGH
+          THE SERVICES (INCLUDING THIRD PARTY CONTENT ON OR OFF CHAIN) IS AT
+          YOUR OWN RISK.
+        </Paragraph>
+        <Paragraph>
+          {`TO THE EXTENT NOT PROHIBITED BY LAW, WE PROVIDE SERVICES "AS IS", "WITH ALL FAULTS" AND "AS AVAILABLE", WITHOUT ANY WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. THIS INCLUDES, BUT IS NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. WE DO NOT GUARANTEE THAT THE ACCESS TO OR USE OF THE SERVICES WILL BE UNINTERRUPTED, ERROR-FREE, OR FREE OF HARMFUL COMPONENTS, OR THAT ANY CONTENT WILL BE SECURE OR NOT OTHERWISE LOST OR DAMAGED.`}
+        </Paragraph>
+        <Paragraph>
+          THE ASSOCIATION, ITS AFFILIATES, LICENSORS, AGENTS, SERVICE PROVIDERS,
+          AND THEIR RESPECTIVE BOARD MEMBERS, DIRECTORS, REPRESRNTATIVES, AND
+          EMPLOYEES CANNOT AND DO NOT GUARANTEE OR WARRANT THAT ACCESS TO OR USE
+          OF THE SERVICES WILL BE UNINTERRUPTED, SECURE OR AVAILABLE AT ANY
+          PARTICULAR TIME OR LOCATION; OR THE RESULTS OF USE OF THE SERVICES
+          WILL MEET YOUR REQUIREMENTS.
+        </Paragraph>
+        <Paragraph>
+          THE SERVICES MAY NOT BE AVAILABLE DUE TO ANY NUMBER OF FACTORS
+          INCLUDING, BUT NOT LIMITED TO, PERIODIC SYSTEM MAINTENANCE, SCHEDULED
+          OR UNSCHEDULED, ACTS OF GOD, UNAUTHORIZED ACCESS, VIRUSES, DENIAL OF
+          SERVICE OR OTHER ATTACKS, TECHNICAL FAILURE OF THE SERVICES AND/OR
+          TELECOMMUNICATIONS INFRASTRUCTURE OR DISRUPTION, AND THEREFORE WE
+          EXPRESSLY DISCLAIM ANY EXPRESS OR IMPLIED WARRANTY REGARDING THE USE
+          AND/OR AVAILABILITY, ACCESSIBILITY, SECURITY OR PERFORMANCE OF THE
+          SERVICES CAUSED BY SUCH FACTORS. NEITHER THE ASSOCIATION, NOR ITS
+          AFFILIATES, LICENSORS, AGENTS, SERVICE PROVIDERS, AND THEIR RESPECTIVE
+          BOARD MEMBERS, DIRECTORS, REPRESENTATIVES, AND EMPLOYEES, NOR ANY
+          OTHER PERSON ASSOCIATED WITH THE ASSOCIATION MAKE ANY REPRESENTATIONS
+          OR WARRANTIES AGAINST THE POSSIBILITY OF DELETION, MISDELIVERY OR
+          FAILURE TO STORE COMMUNICATIONS, PERSONALIZED SETTINGS OR OTHER DATA.
+        </Paragraph>
+        <Paragraph>
+          YOU ACCEPT THE INHERENT SECURITY RISKS OF PROVIDING INFORMATION AND
+          DEALING ONLINE OVER THE INTERNET AND WILL NOT HOLD US RESPONSIBLE FOR
+          ANY BREACH OF SECURITY. NEITHER THE ASSOCIATION, NOR ITS AFFILIATES,
+          LICENSORS, AGENTS, SERVICE PROVIDERS, AND THEIR RESPECTIVE BOARD
+          MEMBERS, DIRECTORS, REPRESENTATIVES, AND EMPLOYEES, NOR ANY OTHER
+          PERSON ASSOCIATED WITH THE ASSOCIATION WILL BE RESPONSIBLE OR LIABLE
+          FOR ANY LOSS, AND NO SUCH PARTY TAKES ANY RESPONSIBILITY FOR, AND WILL
+          NOT BE LIABLE FOR, ANY USE OF THE SERVICES, INCLUDING BUT NOT LIMITED
+          TO ANY LOSSES, DAMAGES OR CLAIMS ARISING FROM: (I) SERVER FAILURE OR
+          DATA LOSS; (II) BLOCKCHAIN NETWORKS, DIGITAL WALLETS OR CORRUPT FILES;
+          (III) UNAUTHORIZED ACCESS TO THE SERVICES; OR (IV) ANY THIRD-PARTY
+          ACTIVITIES, INCLUDING WITHOUT LIMITATION THE USE OF VIRUSES, PHISHING,
+          BRUTEFORCING OR OTHER MEANS OF ATTACK.
+        </Paragraph>
+        <Paragraph>
+          By accessing or using the Services, you represent that you understand
+          the inherent risks associated with cryptographic systems and
+          blockchain-based networks; and warrant that you have an understanding
+          of the usage and intricacies of digital assets, smart contract-based
+          cryptographic tokens, decentralized networks, and systems that
+          interact with blockchain-based networks.
+        </Paragraph>
+        <Paragraph>
+          You acknowledge and agree that digital assets are volatile and risky,
+          and their staking is affected by many factors outside our or your
+          control. You are solely responsible for any transactions, and for all
+          fees that you may incur as a result of you staking digital assets,
+          including (without limitation) “gas” costs. The Services do not
+          control the timing of any transaction, yet you acknowledge that the
+          time of a transaction can affect the value of the digital asset or the
+          fees associated with a transaction or both. You hereby agree that you
+          hold us harmless against any and all claims arising from the
+          transaction of your digital assets, or the timing of such
+          transactions. Digital assets are not legal tender and are not backed
+          by any government. Digital assets are not subject to Federal Deposit
+          Insurance Corporation or Securities Investor Protection Corporation
+          protections. We make no guarantee as to the functionality of any
+          digital asset network which might cause delays, conflicts of interest
+          or might be subject to operational decisions of third parties that are
+          unfavorable to you or affect your digital assets, or lead to your
+          inability to complete a transaction. You hold us harmless from and
+          against any losses you suffer as a result of your use of such
+          third-party services, networks and protocols, even if you access them
+          from our Services. There are no guarantees that a transfer initiated
+          via your digital wallet on the Services will successfully transfer
+          title of or right in any digital assets. You acknowledge that, while
+          the Services and the underlying software have been tested, it is still
+          relatively new and could have bugs or security vulnerabilities. You
+          further acknowledge that the Services and the underlying software are
+          still under development and may undergo significant changes over time
+          that may not meet Users’ expectations. You acknowledge that your use
+          of certain technologies (e.g., jailbreaking tech) on the device with
+          which you access the Services, may cause the Services not to work. You
+          acknowledge that you accept all risk associated with your use of such
+          advanced technologies, and any errors they may cause. You hereby hold
+          us harmless from any losses you suffer as a result of your use of such
+          technologies.
+        </Paragraph>
+        <Paragraph>
+          Digital assets and use of our Services may be subject to expropriation
+          and/or theft. Hackers or other malicious actors may attempt to
+          interfere with our Services or your use thereof in a variety of ways,
+          including, but not limited to, use of malware, denial of service
+          attacks, Sybil attacks, and spoofing. Furthermore, because much of our
+          Services rely on open-source software, there is the software
+          underlying our code that may contain intentional or unintentional bugs
+          or weaknesses which may negatively affect the Services, or result in
+          the loss of your digital assets, or your ability to control your
+          digital wallet. You hold us harmless from and against any losses you
+          suffer as a result of such issues. You agree that your use of the
+          Services is subject to, and you will comply with any, applicable
+          open-source licenses governing any such open-source components. The
+          information on our Services may not always be entirely accurate,
+          complete or current. Information on the Services may be changed or
+          updated from time to time without notice, including information
+          regarding our policies, products and services. Accordingly, you should
+          verify all information before relying on it. All decisions you make
+          based on information provided through the Services are your sole
+          responsibility and you hold us harmless from and against any losses
+          you suffer as a result of such decisions. The Services may contain
+          materials offered by or created by third parties. All such materials,
+          and links to third party websites are provided as a convenience only.
+          We do not control such materials, and provide no guarantee as to their
+          accuracy, completeness, legality or usefulness. You acknowledge and
+          agree that we are not responsible for any aspect of the information,
+          content, or services contained in any such third-party materials
+          accessible or linked to from the Services. You agree and understand
+          that all staking decisions and transactions are made solely by you.
+          You agree and understand that under no circumstances will the
+          operation of the Services and your use of it be deemed to create a
+          relationship that includes the provision of or tendering of investment
+          advice. NO FINANCIAL, INVESTMENT, TAX, LEGAL OR SECURITIES ADVICE IS
+          GIVEN THROUGH OR IN CONNECTION WITH OUR SERVICES. No content found on
+          the Services, whether created by us or another User is or should be
+          considered as investment advice. You agree and understand that we
+          accept no responsibility whatsoever for, and shall in no circumstances
+          be liable in connection with, your decisions to use the Services.
+          Nothing contained on the Services constitutes a solicitation,
+          recommendation, endorsement, or offer by us or any third party to
+          stake, buy or sell any digital assets, securities, or other financial
+          instruments. Neither us nor any of our affiliates has: (1) evaluated
+          the merit of any non-custody staking mechanisms available through the
+          Services; or (2) has endorsed or sponsored any digital assets made
+          available.
+        </Paragraph>
+        <Paragraph>
+          IF YOU ARE DISSATISFIED WITH THE USE OF THE SERVICES, OR WITH THESE
+          TERMS, YOUR SOLE AND EXCLUSIVE REMEDY IS TO DISCONTINUE USING THE
+          SERVICES.
+        </Paragraph>
+        <Paragraph>
+          SOME JURISDICTIONS DO NOT ALLOW EXCLUSION OF WARRANTIES OR LIMITATIONS
+          ON THE DURATION OF IMPLIED WARRANTIES, SO THE ABOVE DISCLAIMER MAY NOT
+          APPLY TO YOU IN ITS ENTIRETY BUT WILL APPLY TO THE MAXIMUM EXTENT
+          PERMITTED BY APPLICABLE LAW.
+        </Paragraph>
+      </Section>
+      <Section title="Liability Disclaimer">
+        <Paragraph>
+          THE ASSOCIATION, ITS AFFILIATES, LICENSORS, AGENTS, SERVICE PROVIDERS,
+          AND THEIR RESPECTIVE BOARD MEMBERS, DIRECTORS, REPRESENTATIVES, AND
+          EMPLOYEES WILL NOT BE LIABLE FOR DAMAGES OF ANY KIND, UNDER ANY LEGAL
+          THEORY, ARISING OUT OF OR IN CONNECTION WITH YOUR ACCESS TO OR USE OF,
+          OR INABILITY TO ACCESS OR USE, THE SERVICES. THIS INCLUDES BUT IS NOT
+          LIMITED TO, PERSONAL INJURY, PAIN AND SUFFERING, EMOTIONAL DISTRESS,
+          LOSS OF REVENUE, PROFITS, BUSINESS OR ANTICIPATED SAVINGS, LOSS OF
+          USE, GOODWILL, OR DATA, CAUSED BY TORT (INCLUDING NEGLIGENCE), BREACH
+          OF CONTRACT, OR OTHERWISE, EVEN IF FORESEEABLE.
+        </Paragraph>
+        <Paragraph>
+          THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL,
+          CONSEQUENTIAL, OR OTHER DAMAGES WILL NOT APPLY TO YOU TO THE EXTENT
+          PROHIBITED BY APPLICABLE LAW. IN JURISDICTIONS WHERE SUCH EXCLUSIONS
+          AND LIMITATIONS ARE NOT ALLOWED, WE ARE RESPONSIBLE TO YOU ONLY FOR
+          LOSSES AND DAMAGES THAT ARE A REASONABLY FORESEEABLE RESULT OF OUR
+          FAILURE TO USE REASONABLE SKILL AND CARE OR OUR BREACH OF THESE TERMS.
+        </Paragraph>
+        <Paragraph>
+          THE AGGREGATE LIABILITY OF THE ASSOCIATION AND ITS AFFILIATES,
+          LICENSORS, AGENTS AND SERVICE PROVIDERS RELATING TO THE SERVIES WILL
+          BE LIMITED TO ONE HUNDRET FRANCS (CHF 100.00). THE LIMITATIONS AND
+          EXCLUSIONS APPLY EVEN IF THIS REMEDY DOES NOT FULLY COMPENSATE YOU FOR
+          ANY LOSSES OR FAILS OF ITS ESSENTIAL PURPOSE.
+        </Paragraph>
+        <Paragraph>
+          IF ANY PROVISION OF THIS SECTION Q. IS OR BECOMES INVALID, THE
+          REMAINING PROVISIONS SHALL REMAIN UNAFFECTED AND WILL BE REPLACED BY A
+          VALID PROVISION THAT CLOSELY MATCHES THE ECONOMIC INTENT OF THE
+          INVALID PROVISION.
+        </Paragraph>
+      </Section>
+      <Section title="Force Majeure">
+        <Paragraph>
+          The Association is not liable for any damage, loss, delay, or
+          inconvenience caused by circumstances beyond our reasonable control.
+          Such circumstances include, but are not limited to, war, threats of
+          war, riots, civil disturbances, terrorist activities, industrial
+          disputes, natural or nuclear disasters, fires, airport closures,
+          adverse weather conditions, utility service interruptions or failures,
+          or actions by any local or national government.
+        </Paragraph>
+      </Section>
+      <Section title="Nature of these Terms">
+        <Paragraph>
+          Nothing contained in these Terms shall be construed as creating any
+          agency, partnership, employment of any type or other form of joint
+          enterprise between you and the Association. You shall not represent to
+          the contrary, either expressly, implicitly, by appearance, or
+          otherwise.
+        </Paragraph>
+      </Section>
+      <Section title="Severability">
+        <Paragraph>
+          If any provision of these Terms is deemed invalid by a court of
+          competent jurisdiction, that provision will be limited to the minimum
+          extent necessary, and the remaining provisions will continue to be
+          fully effective.
+        </Paragraph>
+      </Section>
+      <Section title="No Waiver">
+        <Paragraph>
+          Our failure to enforce any provision of these Terms does not
+          constitute a waiver of its right to enforce that provision, any other
+          provision, or these Terms as a whole in the future.
+        </Paragraph>
+      </Section>
+      <Section title="Assignment">
+        <Paragraph>
+          You may not assign any of your rights, licenses, or obligations under
+          these Terms without our prior written consent. Any attempt by you to
+          do so will be void. We may assign its rights, licenses, and
+          obligations under these Terms without any limitations and without your
+          prior consent.
+        </Paragraph>
+      </Section>
+      <Section title="Notices">
+        <Paragraph>
+          Any notices or other communications provided by the Association under
+          these Terms will be given: (i) via email; or (ii) by posting to the
+          Services. For notices made by email, the date of receipt will be
+          deemed the date on which such notice is transmitted.
+        </Paragraph>
+      </Section>
+      <Section title="Applicable Law and Place of Jurisdiction">
+        <Paragraph>
+          All matters relating to the access to and use of the Services and
+          these Terms, including any dispute or claim arising from or related to
+          them (including non-contractual disputes or claims), shall be governed
+          by and construed in accordance with the internal laws of Switzerland,
+          without regard to any choice or conflict of law provision or rule
+          (whether of Switzerland or any other jurisdiction).
+        </Paragraph>
+        <Paragraph>
+          Any disputes, legal suit, action, or proceeding arising out of or
+          related to these Terms shall be subject to the exclusive jurisdiction
+          of the Courts of Zug, ZG, Switzerland, subject to an appeal at the
+          Swiss Federal Court. However, we retain the right to bring any suit,
+          action, or proceeding against you for breach of this Policy in your
+          country of residence or any other relevant country. You waive any
+          objections to the exercise of jurisdiction over you by such courts and
+          to venue in such courts.
+        </Paragraph>
+        <Paragraph>
+          You agree that any dispute with us shall be resolved solely on an
+          individual basis and not as a class action or any other representative
+          proceeding. You agree that you cannot bring a claim as a class or
+          representative action, nor on behalf of any other person or persons.
+        </Paragraph>
+        <Paragraph>
+          In the event of a dispute, you agree to maintain the confidentiality
+          of all proceedings, including, but not limited to, any and all
+          information gathered, prepared, and presented for the purposes of
+          litigation or related to the dispute(s).
+        </Paragraph>
+      </Section>
+    </dl>
+  </main>
+);
+
+type SectionProps = HTMLProps<HTMLElement> & {
+  title: ReactNode;
+  children: ReactNode | ReactNode[];
+};
+
+const Section = ({ title, children, className, ...props }: SectionProps) => (
+  <section
+    className={clsx(
+      "list-item flex-col gap-4 marker:text-xl marker:font-bold",
+      className,
+    )}
+    {...props}
+  >
+    <dd className="ml-2 inline text-xl font-bold">{title}</dd>
+    <dt className="flex flex-col gap-4">{children}</dt>
+  </section>
+);
+
+const Paragraph = ({
+  className,
+  ...props
+}: HTMLProps<HTMLParagraphElement>) => (
+  <p className={clsx("", className)} {...props} />
+);
+
+const UnorderedList = ({
+  className,
+  ...props
+}: HTMLProps<HTMLUListElement>) => (
+  <ul className={clsx("mx-4 list-inside list-disc", className)} {...props} />
+);
+
+const Link = ({ className, ...props }: ComponentProps<typeof BaseLink>) => (
+  <BaseLink className={clsx("underline", className)} {...props} />
+);

+ 1 - 4
apps/staking/src/config/server.ts

@@ -37,10 +37,7 @@ export const WALLETCONNECT_PROJECT_ID = demandInProduction(
 export const RPC = process.env.RPC;
 export const IS_MAINNET = process.env.IS_MAINNET !== undefined;
 export const HERMES_URL =
-  process.env.HERMES_URL ??
-  (IS_PRODUCTION_SERVER
-    ? "https://hermes.pyth.network"
-    : "https://hermes-beta.pyth.network");
+  process.env.HERMES_URL ?? "https://hermes.pyth.network";
 export const BLOCKED_REGIONS =
   process.env.BLOCKED_REGIONS === undefined ||
   process.env.BLOCKED_REGIONS === ""

+ 22 - 17
apps/staking/src/middleware.ts

@@ -7,31 +7,31 @@ import {
 } from "./config/isomorphic";
 import { BLOCKED_REGIONS, PROXYCHECK_API_KEY } from "./config/server";
 
+const PROXY_BLOCK_PATH = `/${REGION_BLOCKED_SEGMENT}`;
+const VPN_BLOCK_PATH = `/${VPN_BLOCKED_SEGMENT}`;
+
 const proxyCheckClient = PROXYCHECK_API_KEY
   ? new ProxyCheck({ api_key: PROXYCHECK_API_KEY })
   : undefined;
 
 export const middleware = async (request: NextRequest) => {
   if (isRegionBlocked(request)) {
-    return NextResponse.rewrite(
-      new URL(`/${REGION_BLOCKED_SEGMENT}`, request.url),
-    );
+    return rewrite(request, PROXY_BLOCK_PATH);
   } else if (await isProxyBlocked(request)) {
-    return NextResponse.rewrite(
-      new URL(`/${VPN_BLOCKED_SEGMENT}`, request.url),
-    );
+    return rewrite(request, VPN_BLOCK_PATH);
+  } else if (isBlockedSegment(request)) {
+    return rewrite(request, "/not-found");
   } else {
-    const { pathname } = request.nextUrl;
-    return pathname.startsWith(`/${REGION_BLOCKED_SEGMENT}`) ||
-      pathname.startsWith(`/${VPN_BLOCKED_SEGMENT}`)
-      ? NextResponse.rewrite(new URL("/not-found", request.url))
-      : undefined;
+    return;
   }
 };
 
-const isRegionBlocked = (request: NextRequest) =>
-  request.geo?.country !== undefined &&
-  BLOCKED_REGIONS.includes(request.geo.country.toLowerCase());
+const rewrite = (request: NextRequest, path: string) =>
+  NextResponse.rewrite(new URL(path, request.url));
+
+const isRegionBlocked = ({ geo }: NextRequest) =>
+  geo?.country !== undefined &&
+  BLOCKED_REGIONS.includes(geo.country.toLowerCase());
 
 const isProxyBlocked = async ({ ip }: NextRequest) => {
   if (proxyCheckClient === undefined || ip === undefined) {
@@ -42,8 +42,13 @@ const isProxyBlocked = async ({ ip }: NextRequest) => {
   }
 };
 
+const isBlockedSegment = ({ nextUrl: { pathname } }: NextRequest) =>
+  pathname.startsWith(`/${REGION_BLOCKED_SEGMENT}`) ||
+  pathname.startsWith(`/${VPN_BLOCKED_SEGMENT}`);
+
 export const config = {
-  matcher: [
-    "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
-  ],
+  // Next.js requires that this is a static string and fails to read it if it's
+  // a String.raw, so let's disable this rule
+  // eslint-disable-next-line unicorn/prefer-string-raw
+  matcher: ["/((?!_next/static|_next/image|.*\\.).*)"],
 };

+ 6 - 1
contract_manager/store/chains/EvmChains.yaml

@@ -491,7 +491,7 @@
   type: EvmChain
 - id: fantom_sonic_testnet
   mainnet: false
-  rpcUrl: https://rpc.sonic.fantom.network/
+  rpcUrl: https://rpc.testnet.soniclabs.com
   networkId: 64165
   type: EvmChain
 - id: dela_deperp_testnet
@@ -714,3 +714,8 @@
   rpcUrl: https://apechain.calderachain.xyz/http
   networkId: 33139
   type: EvmChain
+- id: flow_mainnet
+  mainnet: true
+  rpcUrl: https://mainnet.evm.nodes.onflow.org
+  networkId: 747
+  type: EvmChain

+ 0 - 3
contract_manager/store/contracts/EvmEntropyContracts.yaml

@@ -13,9 +13,6 @@
 - chain: arbitrum_sepolia
   address: "0x549Ebba8036Ab746611B4fFA1423eb0A4Df61440"
   type: EvmEntropyContract
-- chain: fantom_sonic_testnet
-  address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
-  type: EvmEntropyContract
 - chain: blast_s2_testnet
   address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603"
   type: EvmEntropyContract

+ 6 - 3
contract_manager/store/contracts/EvmPriceFeedContracts.yaml

@@ -241,9 +241,6 @@
 - chain: lightlink_pegasus_testnet
   address: "0x5D289Ad1CE59fCC25b6892e7A303dfFf3a9f7167"
   type: EvmPriceFeedContract
-- chain: fantom_sonic_testnet
-  address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320"
-  type: EvmPriceFeedContract
 - chain: dela_deperp_testnet
   address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
   type: EvmPriceFeedContract
@@ -397,3 +394,9 @@
 - chain: apechain_mainnet
   address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
   type: EvmPriceFeedContract
+- chain: flow_mainnet
+  address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
+  type: EvmPriceFeedContract
+- chain: fantom_sonic_testnet
+  address: "0x96124d1F6E44FfDf1fb5D6d74BB2DE1B7Fbe7376"
+  type: EvmPriceFeedContract

+ 6 - 3
contract_manager/store/contracts/EvmWormholeContracts.yaml

@@ -202,9 +202,6 @@
 - chain: lightlink_pegasus_testnet
   address: "0x5f3c61944CEb01B3eAef861251Fb1E0f14b848fb"
   type: EvmWormholeContract
-- chain: fantom_sonic_testnet
-  address: "0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E"
-  type: EvmWormholeContract
 - chain: dela_deperp_testnet
   address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
   type: EvmWormholeContract
@@ -385,3 +382,9 @@
 - chain: apechain_mainnet
   address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
   type: EvmWormholeContract
+- chain: flow_mainnet
+  address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
+  type: EvmWormholeContract
+- chain: fantom_sonic_testnet
+  address: "0xb700C2f6D14e2cfbD0845Bb102701dBDFf5d1bC4"
+  type: EvmWormholeContract

+ 1 - 0
governance/pyth_staking_sdk/src/constants.ts

@@ -5,6 +5,7 @@ const ONE_MINUTE_IN_SECONDS = 60n;
 const ONE_HOUR_IN_SECONDS = 60n * ONE_MINUTE_IN_SECONDS;
 const ONE_DAY_IN_SECONDS = 24n * ONE_HOUR_IN_SECONDS;
 const ONE_WEEK_IN_SECONDS = 7n * ONE_DAY_IN_SECONDS;
+export const ONE_YEAR_IN_SECONDS = 365n * ONE_DAY_IN_SECONDS;
 
 export const EPOCH_DURATION = ONE_WEEK_IN_SECONDS;
 

+ 5 - 3
governance/pyth_staking_sdk/src/index.ts

@@ -1,6 +1,8 @@
+export * from "./pdas";
 export * from "./pyth-staking-client";
+export * from "./types";
+export * from "./utils/apy";
 export * from "./utils/clock";
-export * from "./utils/position";
 export * from "./utils/pool";
-export * from "./utils/apy";
-export * from "./types";
+export * from "./utils/position";
+export * from "./utils/vesting";

+ 76 - 12
governance/pyth_staking_sdk/src/pyth-staking-client.ts

@@ -8,9 +8,12 @@ import {
 } from "@solana/spl-governance";
 import {
   type Account,
+  createAssociatedTokenAccountInstruction,
   createTransferInstruction,
   getAccount,
   getAssociatedTokenAddress,
+  getMint,
+  type Mint,
 } from "@solana/spl-token";
 import type { AnchorWallet } from "@solana/wallet-adapter-react";
 import {
@@ -24,6 +27,8 @@ import {
 import {
   GOVERNANCE_ADDRESS,
   MAX_VOTER_WEIGHT,
+  FRACTION_PRECISION_N,
+  ONE_YEAR_IN_SECONDS,
   POSITIONS_ACCOUNT_SIZE,
 } from "./constants";
 import {
@@ -42,6 +47,7 @@ import {
   type StakeAccountPositions,
   type TargetAccount,
   type VoterWeightAction,
+  type VestingSchedule,
 } from "./types";
 import { convertBigIntToBN, convertBNToBigInt } from "./utils/bn";
 import { epochToDate, getCurrentEpoch } from "./utils/clock";
@@ -63,7 +69,7 @@ import type { Staking } from "../types/staking";
 
 export type PythStakingClientConfig = {
   connection: Connection;
-  wallet: AnchorWallet | undefined;
+  wallet?: AnchorWallet;
 };
 
 export class PythStakingClient {
@@ -112,7 +118,9 @@ export class PythStakingClient {
   }
 
   /** Gets a users stake accounts */
-  public async getAllStakeAccountPositions(): Promise<PublicKey[]> {
+  public async getAllStakeAccountPositions(
+    owner?: PublicKey,
+  ): Promise<PublicKey[]> {
     const positionDataMemcmp = this.stakingProgram.coder.accounts.memcmp(
       "positionData",
     ) as {
@@ -131,7 +139,7 @@ export class PythStakingClient {
             {
               memcmp: {
                 offset: 8,
-                bytes: this.wallet.publicKey.toBase58(),
+                bytes: owner?.toBase58() ?? this.wallet.publicKey.toBase58(),
               },
             },
           ],
@@ -485,21 +493,38 @@ export class PythStakingClient {
   ) {
     const globalConfig = await this.getGlobalConfig();
     const mint = globalConfig.pythTokenMint;
+    const instructions = [];
 
     const receiverTokenAccount = await getAssociatedTokenAddress(
       mint,
       this.wallet.publicKey,
     );
 
-    const instruction = await this.stakingProgram.methods
-      .withdrawStake(new BN(amount.toString()))
-      .accounts({
-        destination: receiverTokenAccount,
-        stakeAccountPositions,
-      })
-      .instruction();
+    // Edge case: if the user doesn't have an ATA, create one
+    try {
+      await this.getOwnerPythAtaAccount();
+    } catch {
+      instructions.push(
+        createAssociatedTokenAccountInstruction(
+          this.wallet.publicKey,
+          receiverTokenAccount,
+          this.wallet.publicKey,
+          mint,
+        ),
+      );
+    }
 
-    return sendTransaction([instruction], this.connection, this.wallet);
+    instructions.push(
+      await this.stakingProgram.methods
+        .withdrawStake(new BN(amount.toString()))
+        .accounts({
+          destination: receiverTokenAccount,
+          stakeAccountPositions,
+        })
+        .instruction(),
+    );
+
+    return sendTransaction(instructions, this.connection, this.wallet);
   }
 
   public async stakeToPublisher(
@@ -519,7 +544,10 @@ export class PythStakingClient {
     return sendTransaction([instruction], this.connection, this.wallet);
   }
 
-  public async getUnlockSchedule(stakeAccountPositions: PublicKey) {
+  public async getUnlockSchedule(
+    stakeAccountPositions: PublicKey,
+    includePastPeriods = false,
+  ) {
     const stakeAccountMetadataAddress = getStakeAccountMetadataAddress(
       stakeAccountPositions,
     );
@@ -538,7 +566,38 @@ export class PythStakingClient {
     return getUnlockSchedule({
       vestingSchedule,
       pythTokenListTime: config.pythTokenListTime,
+      includePastPeriods,
+    });
+  }
+
+  public async getCirculatingSupply() {
+    const vestingSchedule: VestingSchedule = {
+      periodicVestingAfterListing: {
+        initialBalance: 8_500_000_000n * FRACTION_PRECISION_N,
+        numPeriods: 4n,
+        periodDuration: ONE_YEAR_IN_SECONDS,
+      },
+    };
+
+    const config = await this.getGlobalConfig();
+
+    if (config.pythTokenListTime === null) {
+      throw new Error("Pyth token list time not set in global config");
+    }
+
+    const unlockSchedule = getUnlockSchedule({
+      vestingSchedule,
+      pythTokenListTime: config.pythTokenListTime,
+      includePastPeriods: false,
     });
+
+    const totalLocked = unlockSchedule.schedule.reduce(
+      (total, unlock) => total + unlock.amount,
+      0n,
+    );
+
+    const mint = await this.getPythTokenMint();
+    return mint.supply - totalLocked;
   }
 
   async getAdvanceDelegationRecordInstructions(
@@ -809,4 +868,9 @@ export class PythStakingClient {
     const scalingFactor = await this.getScalingFactor();
     return Number(mainAccount.votingTokens) / scalingFactor;
   }
+  
+  public async getPythTokenMint(): Promise<Mint> {
+    const globalConfig = await this.getGlobalConfig();
+    return getMint(this.connection, globalConfig.pythTokenMint);
+  }
 }

+ 6 - 3
governance/pyth_staking_sdk/src/types.ts

@@ -41,9 +41,12 @@ export type TargetAccount = ConvertBNToBigInt<TargetAccountAnchor>;
 export type VoterWeightAction = IdlTypes<Staking>["voterWeightAction"];
 
 export type UnlockSchedule = {
-  date: Date;
-  amount: bigint;
-}[];
+  type: "fullyUnlocked" | "periodicUnlockingAfterListing" | "periodicUnlocking";
+  schedule: {
+    date: Date;
+    amount: bigint;
+  }[];
+};
 
 export type StakeAccountPositions = {
   address: PublicKey;

+ 33 - 19
governance/pyth_staking_sdk/src/utils/vesting.ts

@@ -3,26 +3,38 @@ import type { UnlockSchedule, VestingSchedule } from "../types";
 export const getUnlockSchedule = (options: {
   pythTokenListTime: bigint;
   vestingSchedule: VestingSchedule;
+  includePastPeriods: boolean;
 }): UnlockSchedule => {
-  const { vestingSchedule, pythTokenListTime } = options;
+  const { vestingSchedule, pythTokenListTime, includePastPeriods } = options;
 
   if (vestingSchedule.fullyVested) {
-    return [];
+    return {
+      type: "fullyUnlocked",
+      schedule: [],
+    };
   } else if (vestingSchedule.periodicVestingAfterListing) {
-    return getPeriodicUnlockSchedule({
-      balance: vestingSchedule.periodicVestingAfterListing.initialBalance,
-      numPeriods: vestingSchedule.periodicVestingAfterListing.numPeriods,
-      periodDuration:
-        vestingSchedule.periodicVestingAfterListing.periodDuration,
-      startDate: pythTokenListTime,
-    });
+    return {
+      type: "periodicUnlockingAfterListing",
+      schedule: getPeriodicUnlockSchedule({
+        balance: vestingSchedule.periodicVestingAfterListing.initialBalance,
+        numPeriods: vestingSchedule.periodicVestingAfterListing.numPeriods,
+        periodDuration:
+          vestingSchedule.periodicVestingAfterListing.periodDuration,
+        startDate: pythTokenListTime,
+        includePastPeriods,
+      }),
+    };
   } else {
-    return getPeriodicUnlockSchedule({
-      balance: vestingSchedule.periodicVesting.initialBalance,
-      numPeriods: vestingSchedule.periodicVesting.numPeriods,
-      periodDuration: vestingSchedule.periodicVesting.periodDuration,
-      startDate: vestingSchedule.periodicVesting.startDate,
-    });
+    return {
+      type: "periodicUnlocking",
+      schedule: getPeriodicUnlockSchedule({
+        balance: vestingSchedule.periodicVesting.initialBalance,
+        numPeriods: vestingSchedule.periodicVesting.numPeriods,
+        periodDuration: vestingSchedule.periodicVesting.periodDuration,
+        startDate: vestingSchedule.periodicVesting.startDate,
+        includePastPeriods,
+      }),
+    };
   }
 };
 
@@ -31,16 +43,18 @@ export const getPeriodicUnlockSchedule = (options: {
   startDate: bigint;
   periodDuration: bigint;
   numPeriods: bigint;
-}): UnlockSchedule => {
-  const { balance, startDate, periodDuration, numPeriods } = options;
+  includePastPeriods: boolean;
+}): UnlockSchedule["schedule"] => {
+  const { balance, startDate, periodDuration, numPeriods, includePastPeriods } =
+    options;
 
-  const unlockSchedule: UnlockSchedule = [];
+  const unlockSchedule: UnlockSchedule["schedule"] = [];
   const currentTimeStamp = Date.now() / 1000;
 
   for (let i = 0; i < numPeriods; i++) {
     const unlockTimeStamp =
       Number(startDate) + Number(periodDuration) * (i + 1);
-    if (currentTimeStamp < unlockTimeStamp) {
+    if (currentTimeStamp < unlockTimeStamp || includePastPeriods) {
       unlockSchedule.push({
         date: new Date(unlockTimeStamp * 1000),
         amount: balance / numPeriods,

+ 1 - 0
governance/xc_admin/packages/xc_admin_common/src/chains.ts

@@ -83,6 +83,7 @@ export const RECEIVER_CHAINS = {
   cronos_zkevm_mainnet: 60059,
   idex_xchain_mainnet: 60060,
   apechain_mainnet: 60061,
+  flow_mainnet: 60062,
 
   // Testnets as a separate chain ids (to use stable data sources and governance for them)
   injective_testnet: 60013,

文件差異過大導致無法顯示
+ 139 - 281
pnpm-lock.yaml


+ 1 - 1
target_chains/ethereum/sdk/js/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/pyth-evm-js",
-  "version": "1.67.0",
+  "version": "1.68.0",
   "description": "Pyth Network EVM Utils in JS",
   "homepage": "https://pyth.network",
   "author": {

+ 3 - 3
target_chains/ethereum/sdk/js/src/index.ts

@@ -34,7 +34,7 @@ export const CONTRACT_ADDR: Record<string, string> = {
   etherlink: "0x2880aB155794e7179c9eE2e38200202908C17B43",
   fantom: "0xff1a0f4744e8582DF1aE09D5611b887B6a12925C",
   filecoin: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  flow_previewnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
+  flow_mainnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
   gnosis: "0x2880aB155794e7179c9eE2e38200202908C17B43",
   gravity: "0x2880aB155794e7179c9eE2e38200202908C17B43",
   hedera: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
@@ -95,10 +95,10 @@ export const CONTRACT_ADDR: Record<string, string> = {
   etherlink_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
   eos_testnet: "0x0708325268dF9F66270F1401206434524814508b",
   evmos_testnet: "0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E",
-  fantom_sonic_testnet: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+  fantom_sonic_testnet: "0x96124d1F6E44FfDf1fb5D6d74BB2DE1B7Fbe7376",
   fantom_testnet: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
   filecoin_calibration: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  flow_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
+  flow_previewnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
   fuji: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
   hedera_testnet: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
   idex_xchain_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",

+ 6 - 4
target_chains/ton/contracts/contracts/Main.fc

@@ -13,14 +13,16 @@
 
     ;; * A 32-bit (big-endian) unsigned integer `op`, identifying the `operation` to be performed, or the `method` of the smart contract to be invoked.
     int op = in_msg_body~load_uint(32);
-    ;; * A 64-bit (big-endian) unsigned integer `query_id`, used in all query-response internal messages to indicate that a response is related to a query (the `query_id` of a response must be equal to the `query_id` of the corresponding query). If `op` is not a query-response method (e.g., it invokes a method that is not expected to send an answer), then `query_id` may be omitted.
-    int query_id = in_msg_body~load_uint(64);
+    cell data = in_msg_body~load_ref();
+    slice data_slice = data.begin_parse();
 
     ;; * The remainder of the message body is specific for each supported value of `op`.
     if (op == OP_UPDATE_GUARDIAN_SET) {
-        update_guardian_set(in_msg_body);
+        update_guardian_set(data_slice);
     } elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) {
-        execute_governance_action(in_msg_body);
+        execute_governance_action(data_slice);
+    } elseif (op == OP_UPGRADE_CONTRACT) {
+        execute_upgrade_contract(data);
     } else {
         throw(0xffff); ;; Throw exception for unknown op
     }

+ 21 - 4
target_chains/ton/contracts/contracts/Pyth.fc

@@ -254,8 +254,25 @@ int apply_decimal_expo(int value, int expo) {
     return result;
 }
 
-() execute_upgrade_contract(slice payload) impure {
-    ;; TODO: Implement
+() execute_upgrade_contract(cell new_code) impure {
+    load_data();
+    int hash_code = cell_hash(new_code);
+    throw_unless(ERROR_INVALID_CODE_HASH, upgrade_code_hash == hash_code);
+
+    ;; Set the new code
+    set_code(new_code);
+
+    ;; Set the code continuation to the new code
+    set_c3(new_code.begin_parse().bless());
+
+    ;; Throw an exception to end the current execution
+    ;; The contract will be restarted with the new code
+    throw(0);
+}
+
+() execute_authorize_upgrade_contract(slice payload) impure {
+    int code_hash = payload~load_uint(256);
+    upgrade_code_hash = code_hash;
 }
 
 () execute_authorize_governance_data_source_transfer(slice payload) impure {
@@ -317,8 +334,8 @@ int apply_decimal_expo(int value, int expo) {
 }
 
 () execute_governance_payload(int action, slice payload) impure {
-    if (action == UPGRADE_CONTRACT) {
-        execute_upgrade_contract(payload);
+    if (action == AUTHORIZE_UPGRADE_CONTRACT) {
+        execute_authorize_upgrade_contract(payload);
     } elseif (action == AUTHORIZE_GOVERNANCE_DATA_SOURCE_TRANSFER) {
         execute_authorize_governance_data_source_transfer(payload);
     } elseif (action == SET_DATA_SOURCES) {

+ 2 - 1
target_chains/ton/contracts/contracts/common/errors.fc

@@ -40,6 +40,7 @@ const int ERROR_OLD_GOVERNANCE_MESSAGE = 1033;
 const int ERROR_INVALID_GOVERNANCE_TARGET = 1034;
 const int ERROR_INVALID_GOVERNANCE_MAGIC = 1035;
 const int ERROR_INVALID_GOVERNANCE_MODULE = 1036;
+const int ERROR_INVALID_CODE_HASH = 1037;
 
 ;; Common
-const int ERROR_INSUFFICIENT_GAS = 1037;
+const int ERROR_INSUFFICIENT_GAS = 1038;

+ 1 - 1
target_chains/ton/contracts/contracts/common/governance_actions.fc

@@ -1,4 +1,4 @@
-const int UPGRADE_CONTRACT = 0;
+const int AUTHORIZE_UPGRADE_CONTRACT = 0;
 const int AUTHORIZE_GOVERNANCE_DATA_SOURCE_TRANSFER = 1;
 const int SET_DATA_SOURCES = 2;
 const int SET_FEE = 3;

+ 1 - 0
target_chains/ton/contracts/contracts/common/op.fc

@@ -1,3 +1,4 @@
 const int OP_UPDATE_GUARDIAN_SET = 1;
 const int OP_UPDATE_PRICE_FEEDS = 2;
 const int OP_EXECUTE_GOVERNANCE_ACTION = 3;
+const int OP_UPGRADE_CONTRACT = 4;

+ 3 - 0
target_chains/ton/contracts/contracts/common/storage.fc

@@ -11,6 +11,7 @@ global int single_update_fee;
 global cell data_sources; ;; Dictionary of DataSource tuples, keyed by u8
 global int num_data_sources;
 global cell is_valid_data_source; ;; Dictionary of int (0 as false, -1 as true), keyed by DataSource cell_hash
+global int upgrade_code_hash; ;; 256-bit unsigned integer
 
 
 ;; Wormhole
@@ -54,6 +55,7 @@ global int governance_data_source_index; ;; u32
         .store_ref(governance_data_source)
         .store_uint(last_executed_governance_sequence, 64)
         .store_uint(governance_data_source_index, 32)
+        .store_uint(upgrade_code_hash, 256)
         .end_cell();
 
     begin_cell()
@@ -94,6 +96,7 @@ global int governance_data_source_index; ;; u32
     governance_data_source = governance_slice~load_ref();
     last_executed_governance_sequence = governance_slice~load_uint(64);
     governance_data_source_index = governance_slice~load_uint(32);
+    upgrade_code_hash = governance_slice~load_uint(256);
 
     ds.end_parse();
 }

+ 6 - 3
target_chains/ton/contracts/contracts/tests/PythTest.fc

@@ -20,12 +20,15 @@
 
     int op = in_msg_body~load_uint(32);
     cell data = in_msg_body~load_ref();
+    slice data_slice = data.begin_parse();
     if (op == OP_UPDATE_GUARDIAN_SET) {
-        update_guardian_set(data.begin_parse());
+        update_guardian_set(data_slice);
     } elseif (op == OP_UPDATE_PRICE_FEEDS) {
-        update_price_feeds(msg_value, data.begin_parse());
+        update_price_feeds(msg_value, data_slice);
     } elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) {
-        execute_governance_action(data.begin_parse());
+        execute_governance_action(data_slice);
+    } elseif (op == OP_UPGRADE_CONTRACT) {
+        execute_upgrade_contract(data);
     } else {
         throw(0xffff); ;; Throw exception for unknown op
     }

+ 78 - 0
target_chains/ton/contracts/contracts/tests/PythTestUpgraded.fc

@@ -0,0 +1,78 @@
+{-
+  This test contract is an upgraded version of PythTest.fc. This is used to test the upgrade functionality of the Pyth contract.
+-}
+
+#include "../imports/stdlib.fc";
+#include "../Pyth.fc";
+#include "../Wormhole.fc";
+#include "../common/op.fc";
+
+() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
+    if (in_msg_body.slice_empty?()) {
+        return ();
+    }
+
+    int op = in_msg_body~load_uint(32);
+    cell data = in_msg_body~load_ref();
+    slice data_slice = data.begin_parse();
+    if (op == OP_UPDATE_GUARDIAN_SET) {
+        update_guardian_set(data_slice);
+    } elseif (op == OP_UPDATE_PRICE_FEEDS) {
+        update_price_feeds(msg_value, data_slice);
+    } elseif (op == OP_EXECUTE_GOVERNANCE_ACTION) {
+        execute_governance_action(data_slice);
+    } elseif (op == OP_UPGRADE_CONTRACT) {
+        execute_upgrade_contract(data);
+    } else {
+        throw(0xffff); ;; Throw exception for unknown op
+    }
+}
+
+(int, int, int, int) test_get_price_unsafe(int price_feed_id) method_id {
+    return get_price_unsafe(price_feed_id);
+}
+
+(int, int, int, int) test_get_price_no_older_than(int time_period, int price_feed_id) method_id {
+    return get_price_no_older_than(time_period, price_feed_id);
+}
+
+(int, int, int, int) test_get_ema_price_unsafe(int price_feed_id) method_id {
+    return get_ema_price_unsafe(price_feed_id);
+}
+
+(int, int, int, int) test_get_ema_price_no_older_than(int time_period, int price_feed_id) method_id {
+    return get_ema_price_no_older_than(time_period, price_feed_id);
+}
+
+(int) test_get_update_fee(slice in_msg_body) method_id {
+    return get_update_fee(in_msg_body);
+}
+
+(int) test_get_single_update_fee() method_id {
+    return get_single_update_fee();
+}
+
+(int) test_get_chain_id() method_id {
+    return get_chain_id();
+}
+
+(int) test_get_last_executed_governance_sequence() method_id {
+    return get_last_executed_governance_sequence();
+}
+
+(int) test_get_governance_data_source_index() method_id {
+    return get_governance_data_source_index();
+}
+
+(cell) test_get_governance_data_source() method_id {
+    return get_governance_data_source();
+}
+
+(int) test_get_is_valid_data_source(cell data_source) method_id {
+    return get_is_valid_data_source(data_source);
+}
+
+;; Add a new function to demonstrate the upgrade
+(int) test_new_function() method_id {
+    return 1;
+}

+ 2 - 1
target_chains/ton/contracts/contracts/tests/WormholeTest.fc

@@ -19,8 +19,9 @@
 
     int op = in_msg_body~load_uint(32);
     cell data = in_msg_body~load_ref();
+    slice data_slice = data.begin_parse();
     if (op == OP_UPDATE_GUARDIAN_SET) {
-        update_guardian_set(data.begin_parse());
+        update_guardian_set(data_slice);
     } else {
         throw(0xffff); ;; Throw exception for unknown op
     }

+ 1 - 0
target_chains/ton/contracts/package.json

@@ -18,6 +18,7 @@
     "@ton/ton": "^13.11.2",
     "@types/jest": "^29.5.12",
     "@types/node": "^20.14.10",
+    "@wormhole-foundation/sdk-definitions": "^0.10.7",
     "jest": "^29.7.0",
     "prettier": "^3.3.2",
     "ts-jest": "^29.2.0",

+ 154 - 2
target_chains/ton/contracts/tests/PythTest.spec.ts

@@ -12,10 +12,17 @@ import {
   TEST_GUARDIAN_ADDRESS1,
   PYTH_AUTHORIZE_GOVERNANCE_DATA_SOURCE_TRANSFER,
   PYTH_REQUEST_GOVERNANCE_DATA_SOURCE_TRANSFER,
+  TEST_GUARDIAN_ADDRESS2,
 } from "./utils/pyth";
 import { GUARDIAN_SET_0, MAINNET_UPGRADE_VAAS } from "./utils/wormhole";
 import { DataSource } from "@pythnetwork/xc-admin-common";
-import { parseDataSource } from "./utils";
+import { parseDataSource, createAuthorizeUpgradePayload } from "./utils";
+import {
+  UniversalAddress,
+  createVAA,
+  serialize,
+} from "@wormhole-foundation/sdk-definitions";
+import { mocks } from "@wormhole-foundation/sdk-definitions/testing";
 
 const TIME_PERIOD = 60;
 const PRICE = new Price({
@@ -49,6 +56,11 @@ const TEST_GOVERNANCE_DATA_SOURCES: DataSource[] = [
     emitterAddress:
       "000000000000000000000000000000000000000000000000000000000000002b",
   },
+  {
+    emitterChain: 1,
+    emitterAddress:
+      "0000000000000000000000000000000000000000000000000000000000000000",
+  },
 ];
 
 describe("PythTest", () => {
@@ -343,7 +355,7 @@ describe("PythTest", () => {
       from: deployer.address,
       to: pythTest.address,
       success: false,
-      exitCode: 1037, // ERROR_INSUFFICIENT_GAS
+      exitCode: 1038, // ERROR_INSUFFICIENT_GAS
     });
   });
 
@@ -719,4 +731,144 @@ describe("PythTest", () => {
       exitCode: 1033, // ERROR_OLD_GOVERNANCE_MESSAGE
     });
   });
+
+  it("should successfully upgrade the contract", async () => {
+    // Compile the upgraded contract
+    const upgradedCode = await compile("PythTestUpgraded");
+    const upgradedCodeHash = upgradedCode.hash();
+
+    // Create the authorize upgrade payload
+    const authorizeUpgradePayload =
+      createAuthorizeUpgradePayload(upgradedCodeHash);
+
+    const authorizeUpgradeVaa = createVAA("Uint8Array", {
+      guardianSet: 0,
+      timestamp: 0,
+      nonce: 0,
+      emitterChain: "Solana",
+      emitterAddress: new UniversalAddress(new Uint8Array(32)),
+      sequence: 1n,
+      consistencyLevel: 0,
+      signatures: [],
+      payload: authorizeUpgradePayload,
+    });
+
+    const guardianSet = mocks.devnetGuardianSet();
+    guardianSet.setSignatures(authorizeUpgradeVaa);
+
+    await deployContract(
+      BTC_PRICE_FEED_ID,
+      TIME_PERIOD,
+      PRICE,
+      EMA_PRICE,
+      SINGLE_UPDATE_FEE,
+      DATA_SOURCES,
+      0,
+      [TEST_GUARDIAN_ADDRESS2],
+      1,
+      1,
+      "0000000000000000000000000000000000000000000000000000000000000000",
+      TEST_GOVERNANCE_DATA_SOURCES[2]
+    );
+
+    // Execute the upgrade
+    const sendExecuteGovernanceActionResult =
+      await pythTest.sendExecuteGovernanceAction(
+        deployer.getSender(),
+        Buffer.from(serialize(authorizeUpgradeVaa))
+      );
+
+    expect(sendExecuteGovernanceActionResult.transactions).toHaveTransaction({
+      from: deployer.address,
+      to: pythTest.address,
+      success: true,
+    });
+
+    // Execute the upgrade
+    const sendUpgradeContractResult = await pythTest.sendUpgradeContract(
+      deployer.getSender(),
+      upgradedCode
+    );
+
+    expect(sendUpgradeContractResult.transactions).toHaveTransaction({
+      from: deployer.address,
+      to: pythTest.address,
+      success: true,
+    });
+
+    // Verify that the contract has been upgraded by calling a new method
+    const newMethodResult = await pythTest.getNewFunction();
+    expect(newMethodResult).toBe(1);
+  });
+
+  it("should fail to upgrade the contract with modified code", async () => {
+    // Compile the upgraded contract
+    const upgradedCode = await compile("PythTestUpgraded");
+    const upgradedCodeHash = upgradedCode.hash();
+
+    // Create the authorize upgrade payload
+    const authorizeUpgradePayload =
+      createAuthorizeUpgradePayload(upgradedCodeHash);
+
+    const authorizeUpgradeVaa = createVAA("Uint8Array", {
+      guardianSet: 0,
+      timestamp: 0,
+      nonce: 0,
+      emitterChain: "Solana",
+      emitterAddress: new UniversalAddress(new Uint8Array(32)),
+      sequence: 1n,
+      consistencyLevel: 0,
+      signatures: [],
+      payload: authorizeUpgradePayload,
+    });
+
+    const guardianSet = mocks.devnetGuardianSet();
+    guardianSet.setSignatures(authorizeUpgradeVaa);
+
+    await deployContract(
+      BTC_PRICE_FEED_ID,
+      TIME_PERIOD,
+      PRICE,
+      EMA_PRICE,
+      SINGLE_UPDATE_FEE,
+      DATA_SOURCES,
+      0,
+      [TEST_GUARDIAN_ADDRESS2],
+      1,
+      1,
+      "0000000000000000000000000000000000000000000000000000000000000000",
+      TEST_GOVERNANCE_DATA_SOURCES[2]
+    );
+
+    // Execute the upgrade authorization
+    const sendExecuteGovernanceActionResult =
+      await pythTest.sendExecuteGovernanceAction(
+        deployer.getSender(),
+        Buffer.from(serialize(authorizeUpgradeVaa))
+      );
+
+    expect(sendExecuteGovernanceActionResult.transactions).toHaveTransaction({
+      from: deployer.address,
+      to: pythTest.address,
+      success: true,
+    });
+
+    // Attempt to execute the upgrade with a different code
+    const wormholeTestCode = await compile("WormholeTest");
+    const sendUpgradeContractResult = await pythTest.sendUpgradeContract(
+      deployer.getSender(),
+      wormholeTestCode
+    );
+
+    // Expect the transaction to fail
+    expect(sendUpgradeContractResult.transactions).toHaveTransaction({
+      from: deployer.address,
+      to: pythTest.address,
+      success: false,
+      exitCode: 1037, // ERROR_INVALID_CODE_HASH
+    });
+
+    // Verify that the contract has not been upgraded by attempting to call the new method
+    await expect(pythTest.getNewFunction()).rejects.toThrow();
+  });
 });

+ 16 - 0
target_chains/ton/contracts/tests/utils.ts

@@ -1,5 +1,11 @@
 import { DataSource } from "@pythnetwork/xc-admin-common";
 import { Cell, Transaction, beginCell } from "@ton/core";
+import { Buffer } from "buffer";
+
+const GOVERNANCE_MAGIC = 0x5054474d;
+const GOVERNANCE_MODULE = 1;
+const AUTHORIZE_UPGRADE_CONTRACT_ACTION = 0;
+const TARGET_CHAIN_ID = 1;
 
 export function createCellChain(buffer: Buffer): Cell {
   let chunks = bufferToChunks(buffer, 127);
@@ -67,3 +73,13 @@ export function printTxGasStats(name: string, transaction: Transaction) {
   console.log(`${name} gas cost: ${txComputed.gasFees}`);
   return txComputed.gasFees;
 }
+
+export function createAuthorizeUpgradePayload(newCodeHash: Buffer): Buffer {
+  const payload = Buffer.alloc(8);
+  payload.writeUInt32BE(GOVERNANCE_MAGIC, 0);
+  payload.writeUInt8(GOVERNANCE_MODULE, 4);
+  payload.writeUInt8(AUTHORIZE_UPGRADE_CONTRACT_ACTION, 5);
+  payload.writeUInt16BE(TARGET_CHAIN_ID, 6);
+
+  return Buffer.concat([payload, newCodeHash]);
+}

+ 3 - 0
target_chains/ton/contracts/tests/utils/pyth.ts

@@ -40,6 +40,9 @@ export const BTC_PRICE_FEED_ID =
 export const TEST_GUARDIAN_ADDRESS1 =
   "0x686b9ea8e3237110eaaba1f1b7467559a3273819";
 
+export const TEST_GUARDIAN_ADDRESS2 =
+  "0xbefa429d57cd18b7f8a4d91a2da9ab4af05d0fbe";
+
 // A Pyth governance instruction to authorize governance data source transfer signed by the test guardian #1.
 // From: target_chains/starknet/contracts/tests/data.cairo::pyth_auth_transfer()
 export const PYTH_AUTHORIZE_GOVERNANCE_DATA_SOURCE_TRANSFER =

+ 25 - 2
target_chains/ton/contracts/wrappers/PythTest.ts

@@ -156,8 +156,9 @@ export class PythTest implements Contract {
               .endCell()
           : beginCell().endCell()
       ) // governance_data_source
-      .storeUint(0, 64) // last_executed_governance_sequence
-      .storeUint(0, 32) // governance_data_source_index
+      .storeUint(0, 64) // last_executed_governance_sequence, set to 0 for initial state
+      .storeUint(0, 32) // governance_data_source_index, set to 0 for initial state
+      .storeUint(0, 256) // upgrade_code_hash, set to 0 for initial state
       .endCell();
 
     // Create the main cell with references to grouped data
@@ -350,6 +351,23 @@ export class PythTest implements Contract {
     });
   }
 
+  async sendUpgradeContract(
+    provider: ContractProvider,
+    via: Sender,
+    newCode: Cell
+  ) {
+    const messageBody = beginCell()
+      .storeUint(4, 32) // OP_UPGRADE_CONTRACT
+      .storeRef(newCode)
+      .endCell();
+
+    await provider.internal(via, {
+      value: toNano("0.1"),
+      sendMode: SendMode.PAY_GAS_SEPARATELY,
+      body: messageBody,
+    });
+  }
+
   async getIsValidDataSource(
     provider: ContractProvider,
     dataSource: DataSource
@@ -365,4 +383,9 @@ export class PythTest implements Contract {
     ]);
     return result.stack.readBoolean();
   }
+
+  async getNewFunction(provider: ContractProvider) {
+    const result = await provider.get("test_new_function", []);
+    return result.stack.readNumber();
+  }
 }

+ 6 - 0
target_chains/ton/contracts/wrappers/PythTestUpgraded.compile.ts

@@ -0,0 +1,6 @@
+import { CompilerConfig } from "@ton/blueprint";
+
+export const compile: CompilerConfig = {
+  lang: "func",
+  targets: ["contracts/tests/PythTestUpgraded.fc"],
+};

+ 1 - 0
target_chains/ton/contracts/wrappers/WormholeTest.ts

@@ -85,6 +85,7 @@ export class WormholeTest implements Contract {
       .storeRef(beginCell()) // governance_data_source, empty for initial state
       .storeUint(0, 64) // last_executed_governance_sequence, set to 0 for initial state
       .storeUint(0, 32) // governance_data_source_index, set to 0 for initial state
+      .storeUint(0, 256) // upgrade_code_hash, set to 0 for initial state
       .endCell();
 
     // Create the main cell with references to grouped data

部分文件因文件數量過多而無法顯示