Abstract
These two proposals upgrade the Pyth Lazer verifier contract (PythLazer) to v0.3.0 on 21 EVM mainnets and 12 EVM testnets. v0.3.0 adds an owner-only setVerificationFee function, so the Pythian Council can change the verification fee through governance.
The fee does not change in this proposal. It stays at 1 wei on every chain. Follow-up proposals will set it to 0.
There are two on-chain proposals, with one instruction per chain:
- Mainnet, 21 instructions: Ae2hALypthvCcuKbdVeDAD8Xx3yeebqkynuD8YLCYJfw
- Testnet, 12 instructions: AC7ShDLj5zUvsqgtGSnWVBZkg4LwGJUVrdCH4d1otTKj
Each instruction is a Wormhole governance Execute message. On each chain, the Pyth governance executor calls upgradeToAndCall(<v0.3.0 implementation>, 0x) on the Lazer proxy.
Code changes: PR #4060, merged at commit a4d38b1.
Testnet will be performed first then tested. If successful, the mainnet one will be performed.
Rationale
Every EVM Lazer contract charges a verification_fee of 1 wei on each verifyUpdate call. initialize sets this value once. The versions deployed today (0.1.1 and 0.2.0) have no function to change it. Because of this:
- Every integrator must send value with each verification.
- On Tempo, which does not allow native value transfers,
verifyUpdatecannot be called at all.
v0.3.0 makes the fee a governance parameter:
setVerificationFee(uint256 fee)is owner only and emitsVerificationFeeSet(oldFee, newFee).withdrawFees(address payable recipient)is owner only. It sends the contract balance (the collected fees) torecipient, emitsFeesWithdrawn(recipient, amount)and rejects the zero address.
On each chain, the owner of the Lazer proxy is the Pyth governance executor. Only a Pythian Council proposal can call these functions.
Description
Mainnet: 21 chains
| Chain | Value |
|---|---|
| Every chain except Mezo | proxy 0xACeA761c27A909d4D3895128EBe6370FDE2dF481, new implementation 0xA089a10d05CE37f4B99Dc4F1824ba253434A779E |
| Mezo | proxy 0x00Aa49132AF596DE135C840E85E4bF6871dB4Eb8, new implementation 0xd74CdD8Eef0E97a5a7678F907991316f88E7965A |
The implementations were deployed with CreateX (CREATE2), which gives the same address on every chain. Mezo has no CreateX, so its implementation was deployed with a plain CREATE.
Note that the Mezo implementation address is the same as the Injective EVM executor address. The same deployer account created both contracts at nonce 31, on two different chains.
The table is in instruction order (# = instruction number). The chain names are the contract-manager IDs that check_proposal prints.
# |
Chain | Chain ID | Executor (proxy owner) | Current version |
|---|---|---|---|---|
| 1 | soneium | 1868 | 0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c |
0.1.1 |
| 2 | base | 8453 | 0xf0a1b566B55e0A0CB5BeF52Eb2a57142617Bee67 |
0.1.1 |
| 3 | tempo | 4217 | 0x98046Bd286715D3B0BC227Dd7a956b83D8978603 |
0.1.1 |
| 4 | megaeth | 4326 | 0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb |
0.1.1 |
| 5 | polygon | 137 | 0xEbe57e8045F2F230872523bbff7374986E45C486 |
0.1.1 |
| 6 | arbitrum | 42161 | 0x24654078A8E043e8985D962a5100CDfA2026f92C |
0.1.1 |
| 7 | bsc | 56 | 0x26DD80569a8B23768A1d80869Ed7339e07595E85 |
0.1.1 |
| 8 | fluent | 25363 | 0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509 |
0.1.1 |
| 9 | monad | 143 | 0x26DD80569a8B23768A1d80869Ed7339e07595E85 |
0.1.1 |
| 10 | fantom_sonic_mainnet | 146 | 0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb |
0.1.1 |
| 11 | injective_evm | 1776 | 0xd74CdD8Eef0E97a5a7678F907991316f88E7965A |
0.1.1 |
| 12 | berachain_mainnet | 80094 | 0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb |
0.1.1 |
| 13 | hyperevm | 999 | 0x5D289Ad1CE59fCC25b6892e7A303dfFf3a9f7167 |
0.1.1 |
| 14 | cronos | 25 | 0xDd24F84d36BF92C65F92307595335bdFab5Bbd21 |
0.1.1 |
| 15 | mezo | 31612 | 0xEbe57e8045F2F230872523bbff7374986E45C486 |
0.2.0 |
| 16 | robinhood | 4663 | 0x0708325268dF9F66270F1401206434524814508b |
0.2.0 |
| 17 | flow_mainnet | 747 | 0x26DD80569a8B23768A1d80869Ed7339e07595E85 |
0.2.0 |
| 18 | etherlink | 42793 | 0x98046Bd286715D3B0BC227Dd7a956b83D8978603 |
0.2.0 |
| 19 | sei_evm_mainnet | 1329 | 0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E |
0.2.0 |
| 20 | ethereum | 1 | 0xCcD7e24e71ba746CBc758C39CBAb11f4E0dfc46F |
0.2.0 |
| 21 | arc | 5042 | 0x0708325268dF9F66270F1401206434524814508b |
0.2.0 |
Testnet: 12 chains
| Chain | Value |
|---|---|
| Every chain except Mezo testnet | proxy 0xACeA761c27A909d4D3895128EBe6370FDE2dF481, new implementation 0xA089a10d05CE37f4B99Dc4F1824ba253434A779E |
| Mezo testnet | proxy 0x768dbf6a56b2C16b68C561a1A3395e2e86542D97, new implementation 0xF33dc56d1073870936E6206E0eE5e77D9F14BFA4 |
| # | Chain | Chain ID | Executor (proxy owner) | Current version |
|---|---|---|---|---|
| 1 | soneium_minato_testnet | 1946 | 0x98046Bd286715D3B0BC227Dd7a956b83D8978603 |
0.1.1 |
| 2 | optimism_sepolia | 11155420 | 0x549Ebba8036Ab746611B4fFA1423eb0A4Df61440 |
0.1.1 |
| 3 | base_sepolia | 84532 | 0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509 |
0.1.1 |
| 4 | arbitrum_sepolia | 421614 | 0x9DF02366A266D79DA5E978aDBEe8B8502117ee1a |
0.1.1 |
| 5 | arc_testnet | 5042002 | 0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320 |
0.1.1 |
| 6 | tempo_testnet | 42431 | 0x8D254a21b3C86D32F7179855531CE99164721933 |
0.1.1 |
| 7 | bsc_testnet | 97 | 0xD458261E832415CFd3BAE5E416FdF3230ce6F134 |
0.1.1 |
| 8 | monad_testnet | 10143 | 0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF |
0.1.1 |
| 9 | injective_evm_testnet | 1439 | 0x9DF02366A266D79DA5E978aDBEe8B8502117ee1a |
0.1.1 |
| 10 | mezo_testnet | 31611 | 0xA5F1987F5b922f7730d709F9DC725cFfB1BF5E9a |
0.2.0 |
| 11 | cronos_testnet | 338 | 0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF |
0.2.0 |
| 12 | robinhood_testnet | 46630 | 0x0708325268dF9F66270F1401206434524814508b |
Not included
| Chain | Network | Reason |
|---|---|---|
ethereal |
Mainnet | The proxy owner is not a governance executor that can execute this proposal. This chain will be handled separately. |
polynomial |
Mainnet | No working RPC endpoint when this proposal was written. |
sepolia |
Testnet | The proxy runs an early 0.1.1 build that never wrote the signer-expiry mapping. v0.3.0 reads that mapping, so after the upgrade it would reject Sepolia’s only trusted signer. Sepolia will be upgraded in a separate proposal, together with a trusted-signer update. |
sonic_blaze_testnet, polynomial_testnet, ethereal_devnet, ethereal_testnet_v2 |
Testnet | No working RPC endpoint when this proposal was written. |
Execution order
- The testnet proposal executes first.
- The mainnet proposal is relayed only after the testnet upgrades are confirmed on-chain.
What changes for integrators
No changes
- The inputs and outputs of
verifyUpdate. For the same update, every chain returns a byte-identical(payload, signer)before and after the upgrade. See Testing. - The fee. It stays at 1 wei until a later proposal changes it.
- Signature recovery (
ECDSA.tryRecover), signer-expiry checks and the trusted-signer set. - The owner and the storage layout. The upgrade calls no initializer (the call data is empty), so all existing state stays.
- Upgradeability. The proxy stays UUPS-upgradeable by the same owner.
Changes
- Refund of an overpayment.
- Before: the contract refunded any value above the fee with
transfer, before it verified the update. If the caller could not receive the refund, the whole call reverted. - Now: the contract refunds with
sendafter it verifies the update.sendhas the same 2,300-gas stipend. If the refund fails, the call still succeeds and emitsRefundFailed(payee, amount). The excess stays in the contract, and governance can withdraw it withwithdrawFees. - Callers that pay exactly the fee are not affected.
- Before: the contract refunded any value above the fee with
- Gas.
verifyUpdateuses 58–78 more gas per call. A call that overpays the fee needs about 1.9k more gas available. The gas it actually uses does not increase. - New functions on the 14 mainnets that run 0.1.1 today.
getTrustedSigners()andgetTrustedSignerExpiry(address)become available. Version 0.2.0 added them.
Before the follow-up proposal
The follow-up proposal will set the fee to 0. Before then, integrations that hard-code msg.value = 1 wei, or that require msg.value == fee, should read verification_fee() at call time.
Testing
1. Fork tests.
- We forked each chain locally and executed the upgrade as that chain’s executor.
- We verified real Pyth Lazer updates before and after the upgrade. The 6 update types were:
- a single feed
- several feeds
- 40 feeds
- a funding-rate feed
- the 200ms channel, the 1s channel and the real-time channel
- The results were byte-identical.
- The upgrade changed only the implementation slot in storage.
- The owner could still upgrade the proxy and roll it back.
2. Live checks on each chain’s own RPC. We sent eth_call requests with the implementation slot overridden to v0.3.0. For fresh Lazer updates, the results were identical to the current contract, at the current fee and with the fee set to 0.
Scope
This proposal authorizes: the 33 contract upgrades listed above.
This proposal does not authorize:
- any change to the verification fee
- any fee withdrawal
- any change to trusted signers
- any other contract change
Onchain verification
1. Decode the proposals.
Requirement: Node.js 24 (see .nvmrc).
git clone https://github.com/pyth-network/pyth-crosschain.git
cd pyth-crosschain
pnpm install && pnpm turbo build --filter @pythnetwork/contract-manager
cd contract_manager
pnpm tsx scripts/check_proposal.ts --cluster mainnet-beta --contract-type lazer \
--proposal Ae2hALypthvCcuKbdVeDAD8Xx3yeebqkynuD8YLCYJfw
pnpm tsx scripts/check_proposal.ts --cluster mainnet-beta --contract-type lazer \
--proposal AC7ShDLj5zUvsqgtGSnWVBZkg4LwGJUVrdCH4d1otTKj
For each instruction, the output shows:
- the call:
upgradeToAndCall(<implementation>, 0x)onEvmLazerContractat the chain’s proxy, from the chain’s executor - the code digest of the new implementation
Check these points:
- Every one of the 33 digests is
0xb4e6083b748ecb185c4be398a87e364be836638f047bc69f28d80e5e57fcead7. The digest is calculated with the implementation’s own address replaced by zeros, because UUPS stores that address as an immutable. This is why the Mezo implementations, which have different addresses, give the same digest. - Each proxy’s on-chain
owner()is the executor in the instruction. If an owner is different,check_proposalprints a line that starts withExecutor Address: ... is not equal to Owner Address. - Each run ends with
No problems found.
2. Check the current state of each proxy.
Run this from contract_manager. It takes the RPC URLs and proxy addresses from the repository’s store. For the testnets, replace the chain list with the 12 testnet IDs.
SIGNER=0x26FB61A864c758AE9fBA027a96010480658385B9 # signer of current Pyth Lazer EVM updates
for c in soneium base tempo megaeth polygon arbitrum bsc fluent monad fantom_sonic_mainnet injective_evm berachain_mainnet hyperevm cronos mezo robinhood flow_mainnet etherlink sei_evm_mainnet ethereum arc; do
rpc=$(jq -r --arg c "$c" '.[] | select(.id == $c) | .rpcUrl' src/store/chains/EvmChains.json)
proxy=$(jq -r --arg c "$c" '.[] | select(.chain == $c) | .address' src/store/contracts/EvmLazerContracts.json)
echo "$c impl=$(cast implementation $proxy --rpc-url $rpc)" \
"version=$(cast call $proxy 'version()(string)' --rpc-url $rpc)" \
"fee=$(cast call $proxy 'verification_fee()(uint256)' --rpc-url $rpc)" \
"signer=$(cast call $proxy 'isValidSigner(address)(bool)' $SIGNER --rpc-url $rpc)" \
"owner=$(cast call $proxy 'owner()(address)' --rpc-url $rpc)"
done
Expected result before execution, on every chain:
versionis the current version in the tables above.fee=1signer=trueowneris the executor in the tables above.
3. Check the source verification.
The implementation source is verified as an exact match on Sourcify on every chain except Fluent:
https://repo.sourcify.dev/``<chainId>/0xA089a10d05CE37f4B99Dc4F1824ba253434A779E. For example, Ethereum.- Mezo:
0xd74C…965A - Mezo testnet:
0xF33d…BFA4
Sourcify does not support Fluent. The Fluent implementation is fully verified on Fluentscan.
4. Build the implementation from source and compare the digest.
git checkout a4d38b18550067d79972c94eb31abce2a6c3efc2
git submodule update --init --recursive -- lazer/contracts/evm
foundryup --install 0.3.0
cd lazer/contracts/evm
forge build
jq -r .deployedBytecode.object out/PythLazer.sol/PythLazer.json | tr -d '\r\n' | cast keccak
# 0xb4e6083b748ecb185c4be398a87e364be836638f047bc69f28d80e5e57fcead7
The result must equal the digest from step 1.
- Use
--recursive. Forge writes the remappings of the nested submodules into the contract metadata, and that metadata is part of the bytecode. - Use Foundry 0.3.0. This is the version that built the deployed implementations. Newer versions, for example 1.8.5, build the same executable code. They write different remappings into the metadata, so the metadata hash at the end of the bytecode is different and the digest does not match.
5. After execution.
- Run
git checkout main. - Run step 2 again.
- On every chain, the expected result is:
impl=0xA089a10d05CE37f4B99Dc4F1824ba253434A779E. On Mezo it is0xd74CdD8Eef0E97a5a7678F907991316f88E7965A. On Mezo testnet it is0xF33dc56d1073870936E6206E0eE5e77D9F14BFA4.version="0.3.0"fee=1signer=trueowneris the same as before.
References
- PR #4060: owner-only setVerificationFee, non-blocking refund, version 0.3.0
PythLazer.solata4d38b1- Pyth Pro (Lazer) contract addresses
- OP-PIP-45: Assign Douro as the trusted signer for Pyth Lazer
Proposal IDs
- Mainnet: Pyth Network
- Testnet: Pyth Network