diff --git a/contract_manager/scripts/common.ts b/contract_manager/scripts/common.ts
index 73c1cb7a07..2137a9c4fd 100644
--- a/contract_manager/scripts/common.ts
+++ b/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
+ );
+ }
+ }
+}
diff --git a/contract_manager/scripts/deploy_evm_entropy_contracts.ts b/contract_manager/scripts/deploy_evm_entropy_contracts.ts
index 7b658eaa97..d44b06b20b 100644
--- a/contract_manager/scripts/deploy_evm_entropy_contracts.ts
+++ b/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`
diff --git a/contract_manager/scripts/deploy_evm_pulse_contracts.ts b/contract_manager/scripts/deploy_evm_pulse_contracts.ts
new file mode 100644
index 0000000000..df487abba3
--- /dev/null
+++ b/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 --private-key --chain --default-provider --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 {
+ 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();
diff --git a/contract_manager/src/contracts/evm.ts b/contract_manager/src/contracts/evm.ts
index ee7406d683..e685007d32 100644
--- a/contract_manager/src/contracts/evm.ts
+++ b/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 {
+ const contract = this.getContract();
+ return contract.methods.owner().call();
+ }
+
+ async getExecutorContract(): Promise {
+ const owner = await this.getOwner();
+ return new EvmExecutorContract(this.chain, owner);
+ }
+
+ async getPythFeeInWei(): Promise {
+ const contract = this.getContract();
+ return contract.methods.getPythFeeInWei().call();
+ }
+
+ async getFee(callbackGasLimit: number): Promise {
+ const contract = this.getContract();
+ return contract.methods.getFee(callbackGasLimit).call();
+ }
+
+ async getAccruedFees(): Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ const contract = this.getContract();
+ const data = contract.methods
+ .setExclusivityPeriod(periodSeconds)
+ .encodeABI();
+ return this.chain.generateExecutorPayload(
+ await this.getOwner(),
+ this.address,
+ data
+ );
+ }
+}
diff --git a/contract_manager/src/contracts/evm_abis.ts b/contract_manager/src/contracts/evm_abis.ts
index 41dbe1db55..85f182caa9 100644
--- a/contract_manager/src/contracts/evm_abis.ts
+++ b/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
diff --git a/contract_manager/src/store.ts b/contract_manager/src/store.ts
index 8eb82bb7e0..4785391488 100644
--- a/contract_manager/src/store.ts
+++ b/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 = { global: new GlobalChain() };
public contracts: Record = {};
public entropy_contracts: Record = {};
+ public pulse_contracts: Record = {};
public wormhole_contracts: Record = {};
public express_relay_contracts: Record = {};
public tokens: Record = {};
diff --git a/target_chains/ethereum/contracts/contracts/pulse/IPulse.sol b/target_chains/ethereum/contracts/contracts/pulse/IPulse.sol
index b22c5582b2..cf67313fd4 100644
--- a/target_chains/ethereum/contracts/contracts/pulse/IPulse.sol
+++ b/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;
diff --git a/target_chains/ethereum/contracts/contracts/pulse/Pulse.sol b/target_chains/ethereum/contracts/contracts/pulse/Pulse.sol
index 91154a04e9..60e097aef7 100644
--- a/target_chains/ethereum/contracts/contracts/pulse/Pulse.sol
+++ b/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");