Browse Source

feat(contract_manager): add deploy_evm_pulse_contracts script (#2439)

* feat(pulse): implement withdrawFees function and update ABI for Pulse contracts

* feat(deploy): enhance bytecode handling and add default provider option for Pulse contract deployment

* add hardcoded default provider and keeper

* refactor(deploy): simplify ERC1967Proxy deployment logic and remove unused code

* feat(deploy): extract common functions
Daniel Chew 8 tháng trước cách đây
mục cha
commit
a5f46d7570

+ 65 - 1
contract_manager/scripts/common.ts

@@ -38,11 +38,26 @@ export async function deployIfNotCached(
       readFileSync(join(config.jsonOutputDir, `${artifactName}.json`), "utf8")
     );
 
+    // Handle bytecode which can be either a string or an object with an 'object' property
+    let bytecode = artifact["bytecode"];
+    if (
+      typeof bytecode === "object" &&
+      bytecode !== null &&
+      "object" in bytecode
+    ) {
+      bytecode = bytecode.object;
+    }
+
+    // Ensure bytecode starts with 0x
+    if (!bytecode.startsWith("0x")) {
+      bytecode = `0x${bytecode}`;
+    }
+
     console.log(`Deploying ${artifactName} on ${chain.getId()}...`);
     const addr = await chain.deploy(
       config.privateKey,
       artifact["abi"],
-      artifact["bytecode"],
+      bytecode,
       deployArgs,
       config.gasMultiplier,
       config.gasPriceMultiplier
@@ -322,3 +337,52 @@ export async function getOrDeployWormholeContract(
     (await deployWormholeContract(chain, config, cacheFile))
   );
 }
+
+export interface DefaultAddresses {
+  mainnet: string;
+  testnet: string;
+}
+
+export async function topupAccountsIfNecessary(
+  chain: EvmChain,
+  deploymentConfig: BaseDeployConfig,
+  accounts: Array<[string, DefaultAddresses]>,
+  minBalance = 0.01
+) {
+  for (const [accountName, defaultAddresses] of accounts) {
+    const accountAddress = chain.isMainnet()
+      ? defaultAddresses.mainnet
+      : defaultAddresses.testnet;
+    const web3 = chain.getWeb3();
+    const balance = Number(
+      web3.utils.fromWei(await web3.eth.getBalance(accountAddress), "ether")
+    );
+    console.log(`${accountName} balance: ${balance} ETH`);
+    if (balance < minBalance) {
+      console.log(
+        `Balance is less than ${minBalance}. Topping up the ${accountName} address...`
+      );
+      const signer = web3.eth.accounts.privateKeyToAccount(
+        deploymentConfig.privateKey
+      );
+      web3.eth.accounts.wallet.add(signer);
+      const estimatedGas = await web3.eth.estimateGas({
+        from: signer.address,
+        to: accountAddress,
+        value: web3.utils.toWei(`${minBalance}`, "ether"),
+      });
+
+      const tx = await web3.eth.sendTransaction({
+        from: signer.address,
+        to: accountAddress,
+        gas: estimatedGas * deploymentConfig.gasMultiplier,
+        value: web3.utils.toWei(`${minBalance}`, "ether"),
+      });
+
+      console.log(
+        `Topped up the ${accountName} address. Tx: `,
+        tx.transactionHash
+      );
+    }
+  }
+}

+ 8 - 41
contract_manager/scripts/deploy_evm_entropy_contracts.ts

@@ -17,8 +17,9 @@ import {
   getWeb3Contract,
   getOrDeployWormholeContract,
   BaseDeployConfig,
+  topupAccountsIfNecessary,
+  DefaultAddresses,
 } from "./common";
-import Web3 from "web3";
 
 interface DeploymentConfig extends BaseDeployConfig {
   type: DeploymentType;
@@ -123,50 +124,16 @@ async function deployEntropyContracts(
   );
 }
 
-async function topupAccountsIfNecessary(
+async function topupEntropyAccountsIfNecessary(
   chain: EvmChain,
   deploymentConfig: DeploymentConfig
 ) {
-  for (const [accountName, defaultAddresses] of [
+  const accounts: Array<[string, DefaultAddresses]> = [
     ["keeper", ENTROPY_DEFAULT_KEEPER],
     ["provider", ENTROPY_DEFAULT_PROVIDER],
-  ] as const) {
-    const accountAddress = chain.isMainnet()
-      ? defaultAddresses.mainnet
-      : defaultAddresses.testnet;
-    const web3 = chain.getWeb3();
-    const balance = Number(
-      web3.utils.fromWei(await web3.eth.getBalance(accountAddress), "ether")
-    );
-    const MIN_BALANCE = 0.01;
-    console.log(`${accountName} balance: ${balance} ETH`);
-    if (balance < MIN_BALANCE) {
-      console.log(
-        `Balance is less than ${MIN_BALANCE}. Topping up the ${accountName} address...`
-      );
-      const signer = web3.eth.accounts.privateKeyToAccount(
-        deploymentConfig.privateKey
-      );
-      web3.eth.accounts.wallet.add(signer);
-      const estimatedGas = await web3.eth.estimateGas({
-        from: signer.address,
-        to: accountAddress,
-        value: web3.utils.toWei(`${MIN_BALANCE}`, "ether"),
-      });
-
-      const tx = await web3.eth.sendTransaction({
-        from: signer.address,
-        to: accountAddress,
-        gas: estimatedGas * deploymentConfig.gasMultiplier,
-        value: web3.utils.toWei(`${MIN_BALANCE}`, "ether"),
-      });
-
-      console.log(
-        `Topped up the ${accountName} address. Tx: `,
-        tx.transactionHash
-      );
-    }
-  }
+  ];
+
+  await topupAccountsIfNecessary(chain, deploymentConfig, accounts);
 }
 
 async function main() {
@@ -189,7 +156,7 @@ async function main() {
     CACHE_FILE
   );
 
-  await topupAccountsIfNecessary(chain, deploymentConfig);
+  await topupEntropyAccountsIfNecessary(chain, deploymentConfig);
 
   console.log(
     `Deployment config: ${JSON.stringify(deploymentConfig, null, 2)}\n`

+ 174 - 0
contract_manager/scripts/deploy_evm_pulse_contracts.ts

@@ -0,0 +1,174 @@
+import yargs from "yargs";
+import { hideBin } from "yargs/helpers";
+import { EvmChain } from "../src/chains";
+import { DefaultStore } from "../src/store";
+import {
+  DeploymentType,
+  toDeploymentType,
+  toPrivateKey,
+  EvmPulseContract,
+  PULSE_DEFAULT_PROVIDER,
+  PULSE_DEFAULT_KEEPER,
+} from "../src";
+import {
+  COMMON_DEPLOY_OPTIONS,
+  deployIfNotCached,
+  getWeb3Contract,
+  getOrDeployWormholeContract,
+  BaseDeployConfig,
+  topupAccountsIfNecessary,
+  DefaultAddresses,
+} from "./common";
+import fs from "fs";
+import path from "path";
+
+interface DeploymentConfig extends BaseDeployConfig {
+  type: DeploymentType;
+  saveContract: boolean;
+}
+
+const CACHE_FILE = ".cache-deploy-evm-pulse-contracts";
+
+const parser = yargs(hideBin(process.argv))
+  .scriptName("deploy_evm_pulse_contracts.ts")
+  .usage(
+    "Usage: $0 --std-output-dir <path/to/std-output-dir/> --private-key <private-key> --chain <chain> --default-provider <default-provider> --wormhole-addr <wormhole-addr>"
+  )
+  .options({
+    ...COMMON_DEPLOY_OPTIONS,
+    chain: {
+      type: "string",
+      demandOption: true,
+      desc: "Chain to upload the contract on. Can be one of the evm chains available in the store",
+    },
+  });
+
+async function deployPulseContracts(
+  chain: EvmChain,
+  config: DeploymentConfig,
+  executorAddr: string
+): Promise<string> {
+  console.log("Deploying PulseUpgradeable on", chain.getId(), "...");
+
+  // Get the artifact and ensure bytecode is properly formatted
+  const pulseArtifact = JSON.parse(
+    fs.readFileSync(
+      path.join(config.jsonOutputDir, "PulseUpgradeable.json"),
+      "utf8"
+    )
+  );
+  console.log("PulseArtifact bytecode type:", typeof pulseArtifact.bytecode);
+
+  const pulseImplAddr = await deployIfNotCached(
+    CACHE_FILE,
+    chain,
+    config,
+    "PulseUpgradeable",
+    []
+  );
+
+  console.log("PulseUpgradeable implementation deployed at:", pulseImplAddr);
+
+  const pulseImplContract = getWeb3Contract(
+    config.jsonOutputDir,
+    "PulseUpgradeable",
+    pulseImplAddr
+  );
+
+  console.log("Preparing initialization data...");
+
+  const pulseInitData = pulseImplContract.methods
+    .initialize(
+      executorAddr, // owner
+      executorAddr, // admin
+      "1", // pythFeeInWei
+      executorAddr, // pythAddress - using executor as a placeholder
+      chain.isMainnet()
+        ? PULSE_DEFAULT_PROVIDER.mainnet
+        : PULSE_DEFAULT_PROVIDER.testnet,
+      true, // prefillRequestStorage
+      3600 // exclusivityPeriodSeconds - 1 hour
+    )
+    .encodeABI();
+
+  console.log("Deploying ERC1967Proxy for Pulse...");
+
+  return await deployIfNotCached(
+    CACHE_FILE,
+    chain,
+    config,
+    "ERC1967Proxy",
+    [pulseImplAddr, pulseInitData],
+    // NOTE: we are deploying a ERC1967Proxy when deploying executor
+    // we need to provide a different cache key. As the `artifactname`
+    // is same in both case which means the cache key will be same
+    `${chain.getId()}-ERC1967Proxy-PULSE1`
+  );
+}
+
+async function topupPulseAccountsIfNecessary(
+  chain: EvmChain,
+  deploymentConfig: DeploymentConfig
+) {
+  const accounts: Array<[string, DefaultAddresses]> = [
+    ["keeper", PULSE_DEFAULT_KEEPER],
+    ["provider", PULSE_DEFAULT_PROVIDER],
+  ];
+
+  await topupAccountsIfNecessary(chain, deploymentConfig, accounts);
+}
+
+async function main() {
+  const argv = await parser.argv;
+
+  const chainName = argv.chain;
+  const chain = DefaultStore.chains[chainName];
+  if (!chain) {
+    throw new Error(`Chain ${chainName} not found`);
+  } else if (!(chain instanceof EvmChain)) {
+    throw new Error(`Chain ${chainName} is not an EVM chain`);
+  }
+
+  const deploymentConfig: DeploymentConfig = {
+    type: toDeploymentType(argv.deploymentType),
+    gasMultiplier: argv.gasMultiplier,
+    gasPriceMultiplier: argv.gasPriceMultiplier,
+    privateKey: toPrivateKey(argv.privateKey),
+    jsonOutputDir: argv.stdOutputDir,
+    saveContract: argv.saveContract,
+  };
+
+  const wormholeContract = await getOrDeployWormholeContract(
+    chain,
+    deploymentConfig,
+    CACHE_FILE
+  );
+
+  await topupPulseAccountsIfNecessary(chain, deploymentConfig);
+
+  console.log(
+    `Deployment config: ${JSON.stringify(deploymentConfig, null, 2)}\n`
+  );
+
+  console.log(`Deploying pulse contracts on ${chain.getId()}...`);
+
+  const executorAddr = wormholeContract.address; // Using wormhole contract as executor for Pulse
+  const pulseAddr = await deployPulseContracts(
+    chain,
+    deploymentConfig,
+    executorAddr
+  );
+
+  if (deploymentConfig.saveContract) {
+    console.log("Saving the contract in the store...");
+    const contract = new EvmPulseContract(chain, pulseAddr);
+    DefaultStore.pulse_contracts[contract.getId()] = contract;
+    DefaultStore.saveAllContracts();
+  }
+
+  console.log(
+    `✅ Deployed pulse contracts on ${chain.getId()} at ${pulseAddr}\n\n`
+  );
+}
+
+main();

+ 204 - 1
contract_manager/src/contracts/evm.ts

@@ -2,7 +2,7 @@ import Web3 from "web3";
 import type { Contract } from "web3-eth-contract";
 import { PriceFeedContract, PrivateKey, Storable } from "../base";
 import { Chain, EvmChain } from "../chains";
-import { DataSource, EvmExecute } from "@pythnetwork/xc-admin-common";
+import { DataSource } from "@pythnetwork/xc-admin-common";
 import { WormholeContract } from "./wormhole";
 import { TokenQty } from "../token";
 import {
@@ -11,6 +11,7 @@ import {
   EXTENDED_ENTROPY_ABI,
   EXTENDED_PYTH_ABI,
   WORMHOLE_ABI,
+  PULSE_UPGRADEABLE_ABI,
 } from "./evm_abis";
 
 /**
@@ -756,3 +757,205 @@ export class EvmPriceFeedContract extends PriceFeedContract {
     };
   }
 }
+
+export const PULSE_DEFAULT_PROVIDER = {
+  mainnet: "0x78357316239040e19fC823372cC179ca75e64b81",
+  testnet: "0x78357316239040e19fC823372cC179ca75e64b81",
+};
+export const PULSE_DEFAULT_KEEPER = {
+  mainnet: "0x78357316239040e19fC823372cC179ca75e64b81",
+  testnet: "0x78357316239040e19fC823372cC179ca75e64b81",
+};
+
+export class EvmPulseContract extends Storable {
+  static type = "EvmPulseContract";
+
+  constructor(public chain: EvmChain, public address: string) {
+    super();
+  }
+
+  getId(): string {
+    return `${this.chain.getId()}_${this.address}`;
+  }
+
+  getChain(): EvmChain {
+    return this.chain;
+  }
+
+  getType(): string {
+    return EvmPulseContract.type;
+  }
+
+  getContract() {
+    const web3 = this.chain.getWeb3();
+    return new web3.eth.Contract(PULSE_UPGRADEABLE_ABI, this.address);
+  }
+
+  static fromJson(
+    chain: Chain,
+    parsed: { type: string; address: string }
+  ): EvmPulseContract {
+    if (parsed.type !== EvmPulseContract.type) throw new Error("Invalid type");
+    if (!(chain instanceof EvmChain))
+      throw new Error(`Wrong chain type ${chain}`);
+    return new EvmPulseContract(chain, parsed.address);
+  }
+
+  toJson() {
+    return {
+      chain: this.chain.getId(),
+      address: this.address,
+      type: EvmPulseContract.type,
+    };
+  }
+
+  async getOwner(): Promise<string> {
+    const contract = this.getContract();
+    return contract.methods.owner().call();
+  }
+
+  async getExecutorContract(): Promise<EvmExecutorContract> {
+    const owner = await this.getOwner();
+    return new EvmExecutorContract(this.chain, owner);
+  }
+
+  async getPythFeeInWei(): Promise<string> {
+    const contract = this.getContract();
+    return contract.methods.getPythFeeInWei().call();
+  }
+
+  async getFee(callbackGasLimit: number): Promise<string> {
+    const contract = this.getContract();
+    return contract.methods.getFee(callbackGasLimit).call();
+  }
+
+  async getAccruedFees(): Promise<string> {
+    const contract = this.getContract();
+    return contract.methods.getAccruedFees().call();
+  }
+
+  async getRequest(sequenceNumber: number): Promise<{
+    provider: string;
+    publishTime: string;
+    priceIds: string[];
+    callbackGasLimit: string;
+    requester: string;
+  }> {
+    const contract = this.getContract();
+    return contract.methods.getRequest(sequenceNumber).call();
+  }
+
+  async getDefaultProvider(): Promise<string> {
+    const contract = this.getContract();
+    return contract.methods.getDefaultProvider().call();
+  }
+
+  async getProviderInfo(provider: string): Promise<{
+    feeInWei: string;
+    accruedFeesInWei: string;
+  }> {
+    const contract = this.getContract();
+    return contract.methods.getProviderInfo(provider).call();
+  }
+
+  async getExclusivityPeriod(): Promise<string> {
+    const contract = this.getContract();
+    return contract.methods.getExclusivityPeriod().call();
+  }
+
+  async getFirstActiveRequests(count: number): Promise<{
+    requests: Array<{
+      provider: string;
+      publishTime: string;
+      priceIds: string[];
+      callbackGasLimit: string;
+      requester: string;
+    }>;
+    actualCount: number;
+  }> {
+    const contract = this.getContract();
+    return contract.methods.getFirstActiveRequests(count).call();
+  }
+
+  async requestPriceUpdatesWithCallback(
+    senderPrivateKey: PrivateKey,
+    publishTime: number,
+    priceIds: string[],
+    callbackGasLimit: number
+  ) {
+    const web3 = this.chain.getWeb3();
+    const { address } = web3.eth.accounts.wallet.add(senderPrivateKey);
+    const contract = new web3.eth.Contract(PULSE_UPGRADEABLE_ABI, this.address);
+
+    const fee = await this.getFee(callbackGasLimit);
+    const transactionObject = contract.methods.requestPriceUpdatesWithCallback(
+      publishTime,
+      priceIds,
+      callbackGasLimit
+    );
+
+    const result = await this.chain.estiamteAndSendTransaction(
+      transactionObject,
+      { from: address, value: fee }
+    );
+    return { id: result.transactionHash, info: result };
+  }
+
+  async executeCallback(
+    senderPrivateKey: PrivateKey,
+    sequenceNumber: number,
+    updateData: string[],
+    priceIds: string[]
+  ) {
+    const web3 = this.chain.getWeb3();
+    const { address } = web3.eth.accounts.wallet.add(senderPrivateKey);
+    const contract = new web3.eth.Contract(PULSE_UPGRADEABLE_ABI, this.address);
+
+    const transactionObject = contract.methods.executeCallback(
+      sequenceNumber,
+      updateData,
+      priceIds
+    );
+
+    const result = await this.chain.estiamteAndSendTransaction(
+      transactionObject,
+      { from: address }
+    );
+    return { id: result.transactionHash, info: result };
+  }
+
+  // Admin functions
+  async generateSetFeeManagerPayload(manager: string): Promise<Buffer> {
+    const contract = this.getContract();
+    const data = contract.methods.setFeeManager(manager).encodeABI();
+    return this.chain.generateExecutorPayload(
+      await this.getOwner(),
+      this.address,
+      data
+    );
+  }
+
+  async generateSetDefaultProviderPayload(provider: string): Promise<Buffer> {
+    const contract = this.getContract();
+    const data = contract.methods.setDefaultProvider(provider).encodeABI();
+    return this.chain.generateExecutorPayload(
+      await this.getOwner(),
+      this.address,
+      data
+    );
+  }
+
+  async generateSetExclusivityPeriodPayload(
+    periodSeconds: number
+  ): Promise<Buffer> {
+    const contract = this.getContract();
+    const data = contract.methods
+      .setExclusivityPeriod(periodSeconds)
+      .encodeABI();
+    return this.chain.generateExecutorPayload(
+      await this.getOwner(),
+      this.address,
+      data
+    );
+  }
+}

+ 1002 - 0
contract_manager/src/contracts/evm_abis.ts

@@ -522,3 +522,1005 @@ export const EXECUTOR_ABI = [
     type: "function",
   },
 ] as any; // eslint-disable-line  @typescript-eslint/no-explicit-any
+
+export const PULSE_UPGRADEABLE_ABI = [
+  {
+    type: "constructor",
+    inputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "function",
+    name: "MAX_PRICE_IDS",
+    inputs: [],
+    outputs: [
+      {
+        name: "",
+        type: "uint8",
+        internalType: "uint8",
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "NUM_REQUESTS",
+    inputs: [],
+    outputs: [
+      {
+        name: "",
+        type: "uint8",
+        internalType: "uint8",
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "NUM_REQUESTS_MASK",
+    inputs: [],
+    outputs: [
+      {
+        name: "",
+        type: "bytes1",
+        internalType: "bytes1",
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "acceptOwnership",
+    inputs: [],
+    outputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "function",
+    name: "executeCallback",
+    inputs: [
+      {
+        name: "sequenceNumber",
+        type: "uint64",
+        internalType: "uint64",
+      },
+      {
+        name: "updateData",
+        type: "bytes[]",
+        internalType: "bytes[]",
+      },
+      {
+        name: "priceIds",
+        type: "bytes32[]",
+        internalType: "bytes32[]",
+      },
+    ],
+    outputs: [],
+    stateMutability: "payable",
+  },
+  {
+    type: "function",
+    name: "getAccruedFees",
+    inputs: [],
+    outputs: [
+      {
+        name: "accruedFeesInWei",
+        type: "uint128",
+        internalType: "uint128",
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "getDefaultProvider",
+    inputs: [],
+    outputs: [
+      {
+        name: "",
+        type: "address",
+        internalType: "address",
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "getExclusivityPeriod",
+    inputs: [],
+    outputs: [
+      {
+        name: "",
+        type: "uint256",
+        internalType: "uint256",
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "getFee",
+    inputs: [
+      {
+        name: "callbackGasLimit",
+        type: "uint256",
+        internalType: "uint256",
+      },
+    ],
+    outputs: [
+      {
+        name: "feeAmount",
+        type: "uint128",
+        internalType: "uint128",
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "getFirstActiveRequests",
+    inputs: [
+      {
+        name: "count",
+        type: "uint256",
+        internalType: "uint256",
+      },
+    ],
+    outputs: [
+      {
+        name: "requests",
+        type: "tuple[]",
+        internalType: "struct PulseState.Request[]",
+        components: [
+          {
+            name: "sequenceNumber",
+            type: "uint64",
+            internalType: "uint64",
+          },
+          {
+            name: "publishTime",
+            type: "uint256",
+            internalType: "uint256",
+          },
+          {
+            name: "priceIds",
+            type: "bytes32[10]",
+            internalType: "bytes32[10]",
+          },
+          {
+            name: "numPriceIds",
+            type: "uint8",
+            internalType: "uint8",
+          },
+          {
+            name: "callbackGasLimit",
+            type: "uint256",
+            internalType: "uint256",
+          },
+          {
+            name: "requester",
+            type: "address",
+            internalType: "address",
+          },
+          {
+            name: "provider",
+            type: "address",
+            internalType: "address",
+          },
+        ],
+      },
+      {
+        name: "actualCount",
+        type: "uint256",
+        internalType: "uint256",
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "getProviderInfo",
+    inputs: [
+      {
+        name: "provider",
+        type: "address",
+        internalType: "address",
+      },
+    ],
+    outputs: [
+      {
+        name: "",
+        type: "tuple",
+        internalType: "struct PulseState.ProviderInfo",
+        components: [
+          {
+            name: "feeInWei",
+            type: "uint128",
+            internalType: "uint128",
+          },
+          {
+            name: "accruedFeesInWei",
+            type: "uint128",
+            internalType: "uint128",
+          },
+          {
+            name: "feeManager",
+            type: "address",
+            internalType: "address",
+          },
+          {
+            name: "isRegistered",
+            type: "bool",
+            internalType: "bool",
+          },
+        ],
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "getPythFeeInWei",
+    inputs: [],
+    outputs: [
+      {
+        name: "pythFeeInWei",
+        type: "uint128",
+        internalType: "uint128",
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "getRequest",
+    inputs: [
+      {
+        name: "sequenceNumber",
+        type: "uint64",
+        internalType: "uint64",
+      },
+    ],
+    outputs: [
+      {
+        name: "req",
+        type: "tuple",
+        internalType: "struct PulseState.Request",
+        components: [
+          {
+            name: "sequenceNumber",
+            type: "uint64",
+            internalType: "uint64",
+          },
+          {
+            name: "publishTime",
+            type: "uint256",
+            internalType: "uint256",
+          },
+          {
+            name: "priceIds",
+            type: "bytes32[10]",
+            internalType: "bytes32[10]",
+          },
+          {
+            name: "numPriceIds",
+            type: "uint8",
+            internalType: "uint8",
+          },
+          {
+            name: "callbackGasLimit",
+            type: "uint256",
+            internalType: "uint256",
+          },
+          {
+            name: "requester",
+            type: "address",
+            internalType: "address",
+          },
+          {
+            name: "provider",
+            type: "address",
+            internalType: "address",
+          },
+        ],
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "initialize",
+    inputs: [
+      {
+        name: "owner",
+        type: "address",
+        internalType: "address",
+      },
+      {
+        name: "admin",
+        type: "address",
+        internalType: "address",
+      },
+      {
+        name: "pythFeeInWei",
+        type: "uint128",
+        internalType: "uint128",
+      },
+      {
+        name: "pythAddress",
+        type: "address",
+        internalType: "address",
+      },
+      {
+        name: "defaultProvider",
+        type: "address",
+        internalType: "address",
+      },
+      {
+        name: "prefillRequestStorage",
+        type: "bool",
+        internalType: "bool",
+      },
+      {
+        name: "exclusivityPeriodSeconds",
+        type: "uint256",
+        internalType: "uint256",
+      },
+    ],
+    outputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "function",
+    name: "owner",
+    inputs: [],
+    outputs: [
+      {
+        name: "",
+        type: "address",
+        internalType: "address",
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "pendingOwner",
+    inputs: [],
+    outputs: [
+      {
+        name: "",
+        type: "address",
+        internalType: "address",
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "proxiableUUID",
+    inputs: [],
+    outputs: [
+      {
+        name: "",
+        type: "bytes32",
+        internalType: "bytes32",
+      },
+    ],
+    stateMutability: "view",
+  },
+  {
+    type: "function",
+    name: "registerProvider",
+    inputs: [
+      {
+        name: "feeInWei",
+        type: "uint128",
+        internalType: "uint128",
+      },
+    ],
+    outputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "function",
+    name: "renounceOwnership",
+    inputs: [],
+    outputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "function",
+    name: "requestPriceUpdatesWithCallback",
+    inputs: [
+      {
+        name: "publishTime",
+        type: "uint256",
+        internalType: "uint256",
+      },
+      {
+        name: "priceIds",
+        type: "bytes32[]",
+        internalType: "bytes32[]",
+      },
+      {
+        name: "callbackGasLimit",
+        type: "uint256",
+        internalType: "uint256",
+      },
+    ],
+    outputs: [
+      {
+        name: "requestSequenceNumber",
+        type: "uint64",
+        internalType: "uint64",
+      },
+    ],
+    stateMutability: "payable",
+  },
+  {
+    type: "function",
+    name: "setDefaultProvider",
+    inputs: [
+      {
+        name: "provider",
+        type: "address",
+        internalType: "address",
+      },
+    ],
+    outputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "function",
+    name: "setExclusivityPeriod",
+    inputs: [
+      {
+        name: "periodSeconds",
+        type: "uint256",
+        internalType: "uint256",
+      },
+    ],
+    outputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "function",
+    name: "setFeeManager",
+    inputs: [
+      {
+        name: "manager",
+        type: "address",
+        internalType: "address",
+      },
+    ],
+    outputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "function",
+    name: "setProviderFee",
+    inputs: [
+      {
+        name: "newFeeInWei",
+        type: "uint128",
+        internalType: "uint128",
+      },
+    ],
+    outputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "function",
+    name: "transferOwnership",
+    inputs: [
+      {
+        name: "newOwner",
+        type: "address",
+        internalType: "address",
+      },
+    ],
+    outputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "function",
+    name: "upgradeTo",
+    inputs: [
+      {
+        name: "newImplementation",
+        type: "address",
+        internalType: "address",
+      },
+    ],
+    outputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "function",
+    name: "upgradeToAndCall",
+    inputs: [
+      {
+        name: "newImplementation",
+        type: "address",
+        internalType: "address",
+      },
+      {
+        name: "data",
+        type: "bytes",
+        internalType: "bytes",
+      },
+    ],
+    outputs: [],
+    stateMutability: "payable",
+  },
+  {
+    type: "function",
+    name: "version",
+    inputs: [],
+    outputs: [
+      {
+        name: "",
+        type: "string",
+        internalType: "string",
+      },
+    ],
+    stateMutability: "pure",
+  },
+  {
+    type: "function",
+    name: "withdrawAsFeeManager",
+    inputs: [
+      {
+        name: "provider",
+        type: "address",
+        internalType: "address",
+      },
+      {
+        name: "amount",
+        type: "uint128",
+        internalType: "uint128",
+      },
+    ],
+    outputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "function",
+    name: "withdrawFees",
+    inputs: [
+      {
+        name: "amount",
+        type: "uint128",
+        internalType: "uint128",
+      },
+    ],
+    outputs: [],
+    stateMutability: "nonpayable",
+  },
+  {
+    type: "event",
+    name: "AdminChanged",
+    inputs: [
+      {
+        name: "previousAdmin",
+        type: "address",
+        indexed: false,
+        internalType: "address",
+      },
+      {
+        name: "newAdmin",
+        type: "address",
+        indexed: false,
+        internalType: "address",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "BeaconUpgraded",
+    inputs: [
+      {
+        name: "beacon",
+        type: "address",
+        indexed: true,
+        internalType: "address",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "ContractUpgraded",
+    inputs: [
+      {
+        name: "oldImplementation",
+        type: "address",
+        indexed: false,
+        internalType: "address",
+      },
+      {
+        name: "newImplementation",
+        type: "address",
+        indexed: false,
+        internalType: "address",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "DefaultProviderUpdated",
+    inputs: [
+      {
+        name: "oldProvider",
+        type: "address",
+        indexed: false,
+        internalType: "address",
+      },
+      {
+        name: "newProvider",
+        type: "address",
+        indexed: false,
+        internalType: "address",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "ExclusivityPeriodUpdated",
+    inputs: [
+      {
+        name: "oldPeriodSeconds",
+        type: "uint256",
+        indexed: false,
+        internalType: "uint256",
+      },
+      {
+        name: "newPeriodSeconds",
+        type: "uint256",
+        indexed: false,
+        internalType: "uint256",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "FeeManagerUpdated",
+    inputs: [
+      {
+        name: "admin",
+        type: "address",
+        indexed: true,
+        internalType: "address",
+      },
+      {
+        name: "oldFeeManager",
+        type: "address",
+        indexed: false,
+        internalType: "address",
+      },
+      {
+        name: "newFeeManager",
+        type: "address",
+        indexed: false,
+        internalType: "address",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "FeesWithdrawn",
+    inputs: [
+      {
+        name: "recipient",
+        type: "address",
+        indexed: true,
+        internalType: "address",
+      },
+      {
+        name: "amount",
+        type: "uint128",
+        indexed: false,
+        internalType: "uint128",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "Initialized",
+    inputs: [
+      {
+        name: "version",
+        type: "uint8",
+        indexed: false,
+        internalType: "uint8",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "OwnershipTransferStarted",
+    inputs: [
+      {
+        name: "previousOwner",
+        type: "address",
+        indexed: true,
+        internalType: "address",
+      },
+      {
+        name: "newOwner",
+        type: "address",
+        indexed: true,
+        internalType: "address",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "OwnershipTransferred",
+    inputs: [
+      {
+        name: "previousOwner",
+        type: "address",
+        indexed: true,
+        internalType: "address",
+      },
+      {
+        name: "newOwner",
+        type: "address",
+        indexed: true,
+        internalType: "address",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "PriceUpdateCallbackFailed",
+    inputs: [
+      {
+        name: "sequenceNumber",
+        type: "uint64",
+        indexed: true,
+        internalType: "uint64",
+      },
+      {
+        name: "provider",
+        type: "address",
+        indexed: true,
+        internalType: "address",
+      },
+      {
+        name: "priceIds",
+        type: "bytes32[]",
+        indexed: false,
+        internalType: "bytes32[]",
+      },
+      {
+        name: "requester",
+        type: "address",
+        indexed: false,
+        internalType: "address",
+      },
+      {
+        name: "reason",
+        type: "string",
+        indexed: false,
+        internalType: "string",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "PriceUpdateExecuted",
+    inputs: [
+      {
+        name: "sequenceNumber",
+        type: "uint64",
+        indexed: true,
+        internalType: "uint64",
+      },
+      {
+        name: "provider",
+        type: "address",
+        indexed: true,
+        internalType: "address",
+      },
+      {
+        name: "priceIds",
+        type: "bytes32[]",
+        indexed: false,
+        internalType: "bytes32[]",
+      },
+      {
+        name: "prices",
+        type: "int64[]",
+        indexed: false,
+        internalType: "int64[]",
+      },
+      {
+        name: "conf",
+        type: "uint64[]",
+        indexed: false,
+        internalType: "uint64[]",
+      },
+      {
+        name: "expos",
+        type: "int32[]",
+        indexed: false,
+        internalType: "int32[]",
+      },
+      {
+        name: "publishTimes",
+        type: "uint256[]",
+        indexed: false,
+        internalType: "uint256[]",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "PriceUpdateRequested",
+    inputs: [
+      {
+        name: "request",
+        type: "tuple",
+        indexed: false,
+        internalType: "struct PulseState.Request",
+        components: [
+          {
+            name: "sequenceNumber",
+            type: "uint64",
+            internalType: "uint64",
+          },
+          {
+            name: "publishTime",
+            type: "uint256",
+            internalType: "uint256",
+          },
+          {
+            name: "priceIds",
+            type: "bytes32[10]",
+            internalType: "bytes32[10]",
+          },
+          {
+            name: "numPriceIds",
+            type: "uint8",
+            internalType: "uint8",
+          },
+          {
+            name: "callbackGasLimit",
+            type: "uint256",
+            internalType: "uint256",
+          },
+          {
+            name: "requester",
+            type: "address",
+            internalType: "address",
+          },
+          {
+            name: "provider",
+            type: "address",
+            internalType: "address",
+          },
+        ],
+      },
+      {
+        name: "priceIds",
+        type: "bytes32[]",
+        indexed: false,
+        internalType: "bytes32[]",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "ProviderFeeUpdated",
+    inputs: [
+      {
+        name: "provider",
+        type: "address",
+        indexed: true,
+        internalType: "address",
+      },
+      {
+        name: "oldFee",
+        type: "uint128",
+        indexed: false,
+        internalType: "uint128",
+      },
+      {
+        name: "newFee",
+        type: "uint128",
+        indexed: false,
+        internalType: "uint128",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "ProviderRegistered",
+    inputs: [
+      {
+        name: "provider",
+        type: "address",
+        indexed: true,
+        internalType: "address",
+      },
+      {
+        name: "feeInWei",
+        type: "uint128",
+        indexed: false,
+        internalType: "uint128",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "event",
+    name: "Upgraded",
+    inputs: [
+      {
+        name: "implementation",
+        type: "address",
+        indexed: true,
+        internalType: "address",
+      },
+    ],
+    anonymous: false,
+  },
+  {
+    type: "error",
+    name: "InsufficientFee",
+    inputs: [],
+  },
+  {
+    type: "error",
+    name: "InvalidPriceIds",
+    inputs: [
+      {
+        name: "providedPriceIdsHash",
+        type: "bytes32",
+        internalType: "bytes32",
+      },
+      {
+        name: "storedPriceIdsHash",
+        type: "bytes32",
+        internalType: "bytes32",
+      },
+    ],
+  },
+  {
+    type: "error",
+    name: "NoSuchRequest",
+    inputs: [],
+  },
+  {
+    type: "error",
+    name: "TooManyPriceIds",
+    inputs: [
+      {
+        name: "provided",
+        type: "uint256",
+        internalType: "uint256",
+      },
+      {
+        name: "maximum",
+        type: "uint256",
+        internalType: "uint256",
+      },
+    ],
+  },
+] as any; // eslint-disable-line  @typescript-eslint/no-explicit-any

+ 2 - 0
contract_manager/src/store.ts

@@ -26,6 +26,7 @@ import {
   EvmExpressRelayContract,
   TonPriceFeedContract,
   TonWormholeContract,
+  EvmPulseContract,
 } from "./contracts";
 import { Token } from "./token";
 import { PriceFeedContract, Storable } from "./base";
@@ -42,6 +43,7 @@ export class Store {
   public chains: Record<string, Chain> = { global: new GlobalChain() };
   public contracts: Record<string, PriceFeedContract> = {};
   public entropy_contracts: Record<string, EvmEntropyContract> = {};
+  public pulse_contracts: Record<string, EvmPulseContract> = {};
   public wormhole_contracts: Record<string, WormholeContract> = {};
   public express_relay_contracts: Record<string, EvmExpressRelayContract> = {};
   public tokens: Record<string, Token> = {};

+ 6 - 0
target_chains/ethereum/contracts/contracts/pulse/IPulse.sol

@@ -75,6 +75,12 @@ interface IPulse is PulseEvents {
 
     function setFeeManager(address manager) external;
 
+    /**
+     * @notice Allows the admin to withdraw accumulated Pyth protocol fees
+     * @param amount The amount of fees to withdraw in wei
+     */
+    function withdrawFees(uint128 amount) external;
+
     function withdrawAsFeeManager(address provider, uint128 amount) external;
 
     function registerProvider(uint128 feeInWei) external;

+ 1 - 1
target_chains/ethereum/contracts/contracts/pulse/Pulse.sol

@@ -244,7 +244,7 @@ abstract contract Pulse is IPulse, PulseState {
         shortHash = uint8(hash[0] & NUM_REQUESTS_MASK);
     }
 
-    function withdrawFees(uint128 amount) external {
+    function withdrawFees(uint128 amount) external override {
         require(msg.sender == _state.admin, "Only admin can withdraw fees");
         require(_state.accruedFeesInWei >= amount, "Insufficient balance");