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");