Skip to content

Commit

Permalink
[contract_manager] Support for entropy and evm executor (#1251)
Browse files Browse the repository at this point in the history
* Add EvmExecute structures for governance

* Add ExecuteAction file

* uint 256 (#1250)

* Add in value field in ExecuteAction

* Add value arg in contract manager

* add tests for evm execute (#1252)

* add tests for evm execute

* add tests for buffer layout

* remove unneccessary test

* accept admin and ownership payload

* rename to add entropy

* update comment

* address comments

* minor rename

---------

Co-authored-by: Amin Moghaddam <amin@pyth.network>
  • Loading branch information
Dev Kalra and m30m authored Jan 29, 2024
1 parent e1db4aa commit f3ad917
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 20 deletions.
86 changes: 86 additions & 0 deletions contract_manager/scripts/entropy-accept-admin-and-ownership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { DefaultStore, EvmChain, loadHotWallet } from "../src";

const parser = yargs(hideBin(process.argv))
.usage(
"Creates governance proposal to accept pending admin or ownership transfer for Pyth entropy contracts.\n" +
"Usage: $0 --chain <chain_1> --chain <chain_2> --ops-key-path <ops_key_path>"
)
.options({
testnet: {
type: "boolean",
default: false,
desc: "Accept for testnet contracts instead of mainnet",
},
"all-chains": {
type: "boolean",
default: false,
desc: "Accept for contract on all chains. Use with --testnet flag to accept for all testnet contracts",
},
chain: {
type: "array",
string: true,
desc: "Accept for contract on given chains",
},
"ops-key-path": {
type: "string",
demandOption: true,
desc: "Path to the private key of the proposer to use for the operations multisig governance proposal",
},
});

async function main() {
const argv = await parser.argv;
const selectedChains: EvmChain[] = [];

if (argv.allChains && argv.chain)
throw new Error("Cannot use both --all-chains and --chain");
if (!argv.allChains && !argv.chain)
throw new Error("Must use either --all-chains or --chain");
for (const chain of Object.values(DefaultStore.chains)) {
if (!(chain instanceof EvmChain)) continue;
if (
(argv.allChains && chain.isMainnet() !== argv.testnet) ||
argv.chain?.includes(chain.getId())
)
selectedChains.push(chain);
}
if (argv.chain && selectedChains.length !== argv.chain.length)
throw new Error(
`Some chains were not found ${selectedChains
.map((chain) => chain.getId())
.toString()}`
);
for (const chain of selectedChains) {
if (chain.isMainnet() != selectedChains[0].isMainnet())
throw new Error("All chains must be either mainnet or testnet");
}

const vault =
DefaultStore.vaults[
"mainnet-beta_FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj"
];

const payloads: Buffer[] = [];
for (const contract of Object.values(DefaultStore.entropy_contracts)) {
if (selectedChains.includes(contract.chain)) {
console.log("Creating payload for chain: ", contract.chain.getId());
const pendingOwner = await contract.getPendingOwner();
const adminPayload = contract.generateAcceptAdminPayload(pendingOwner);
const ownerPayload =
contract.generateAcceptOwnershipPayload(pendingOwner);

payloads.push(adminPayload, ownerPayload);
}
}

console.log("Using vault at for proposal", vault.getId());
const wallet = await loadHotWallet(argv["ops-key-path"]);
console.log("Using wallet ", wallet.publicKey.toBase58());
await vault.connect(wallet);
const proposal = await vault.proposeWormholeMessage(payloads);
console.log("Proposal address", proposal.address.toBase58());
}

main();
83 changes: 81 additions & 2 deletions contract_manager/src/contracts/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,54 @@ import PythInterfaceAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
import EntropyAbi from "@pythnetwork/entropy-sdk-solidity/abis/IEntropy.json";
import { PriceFeedContract, PrivateKey, Storable } from "../base";
import { Chain, EvmChain } from "../chains";
import { DataSource } from "xc_admin_common";
import { DataSource, EvmExecute } from "xc_admin_common";
import { WormholeContract } from "./wormhole";

// Just to make sure tx gas limit is enough
const GAS_ESTIMATE_MULTIPLIER = 2;
const EXTENDED_ENTROPY_ABI = [
{
inputs: [],
name: "acceptOwnership",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "acceptAdmin",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "owner",
outputs: [
{
internalType: "address",
name: "",
type: "address",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "pendingOwner",
outputs: [
{
internalType: "address",
name: "",
type: "address",
},
],
stateMutability: "view",
type: "function",
},
...EntropyAbi,
] as any; // eslint-disable-line @typescript-eslint/no-explicit-any
const EXTENDED_PYTH_ABI = [
{
inputs: [],
Expand Down Expand Up @@ -321,6 +364,42 @@ export class EvmEntropyContract extends Storable {
return new EvmEntropyContract(chain, parsed.address);
}

// Generate a payload for the given executor address and calldata.
// `executor` and `calldata` should be hex strings.
generateExecutorPayload(executor: string, calldata: string) {
return new EvmExecute(
this.chain.wormholeChainName,
executor.replace("0x", ""),
this.address.replace("0x", ""),
0n,
Buffer.from(calldata.replace("0x", ""), "hex")
).encode();
}

// Generates a payload for the newAdmin to call acceptAdmin on the entropy contracts
generateAcceptAdminPayload(newAdmin: string): Buffer {
const contract = this.getContract();
const data = contract.methods.acceptAdmin().encodeABI();
return this.generateExecutorPayload(newAdmin, data);
}

// Generates a payload for newOwner to call acceptOwnership on the entropy contracts
generateAcceptOwnershipPayload(newOwner: string): Buffer {
const contract = this.getContract();
const data = contract.methods.acceptOwnership().encodeABI();
return this.generateExecutorPayload(newOwner, data);
}

getOwner(): string {
const contract = this.getContract();
return contract.methods.owner().call();
}

getPendingOwner(): string {
const contract = this.getContract();
return contract.methods.pendingOwner().call();
}

toJson() {
return {
chain: this.chain.getId(),
Expand All @@ -331,7 +410,7 @@ export class EvmEntropyContract extends Storable {

getContract() {
const web3 = new Web3(this.chain.getRpcUrl());
return new web3.eth.Contract(EntropyAbi as any, this.address); // eslint-disable-line @typescript-eslint/no-explicit-any
return new web3.eth.Contract(EXTENDED_ENTROPY_ABI, this.address);
}

async getDefaultProvider(): Promise<string> {
Expand Down
15 changes: 15 additions & 0 deletions contract_manager/store/chains/EvmChains.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,18 @@
rpcUrl: https://rpc.ankr.com/filecoin
networkId: 314
type: EvmChain
- id: lightlink_pegasus_testnet
mainnet: false
rpcUrl: https://replicator.pegasus.lightlink.io/rpc/v1
networkId: 1891
type: EvmChain
- id: sei_evm_devnet
mainnet: false
rpcUrl: https://evm-devnet.seinetwork.io
networkId: 713715
type: EvmChain
- id: fantom_sonic_testnet
mainnet: false
rpcUrl: https://rpc.sonic.fantom.network/
networkId: 64165
type: EvmChain
23 changes: 22 additions & 1 deletion contract_manager/store/contracts/EvmEntropyContracts.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
- chain: lightlink_pegasus_testnet
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmEntropyContract
- chain: chiliz_spicy
address: "0xD458261E832415CFd3BAE5E416FdF3230ce6F134"
type: EvmEntropyContract
- chain: conflux_espace_testnet
address: "0xdF21D137Aadc95588205586636710ca2890538d5"
type: EvmEntropyContract
- chain: mode_testnet
address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603"
type: EvmEntropyContract
- chain: arbitrum_sepolia
address: "0x549Ebba8036Ab746611B4fFA1423eb0A4Df61440"
type: EvmEntropyContract
- chain: base_goerli
address: "0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF"
type: EvmPriceFeedContract
type: EvmEntropyContract
- chain: fantom_sonic_testnet
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmEntropyContract
- chain: blast_s2_testnet
address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603"
type: EvmEntropyContract
5 changes: 3 additions & 2 deletions governance/xc_admin/packages/xc_admin_common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@solana/buffer-layout": "^4.0.1",
"@solana/web3.js": "^1.73.0",
"@sqds/mesh": "^1.0.6",
"bigint-buffer": "^1.1.5",
"ethers": "^5.7.2",
"lodash": "^4.17.21",
"typescript": "^4.9.4"
Expand All @@ -34,9 +35,9 @@
"@types/bn.js": "^5.1.1",
"@types/jest": "^29.2.5",
"@types/lodash": "^4.14.191",
"fast-check": "^3.10.0",
"jest": "^29.3.1",
"prettier": "^2.8.1",
"ts-jest": "^29.0.3",
"fast-check": "^3.10.0"
"ts-jest": "^29.0.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import fc from "fast-check";
import { u64be } from "../governance_payload/BufferLayoutExt";

test("Buffer layout extension fc tests", (done) => {
const u64 = u64be();
fc.assert(
fc.property(fc.bigUintN(64), (bi) => {
let encodedUint8Array = new Uint8Array(8);
u64.encode(bi, encodedUint8Array);

let buffer = Buffer.alloc(8);
buffer.writeBigUInt64BE(bi);

const decodedBI = u64.decode(buffer);
return buffer.equals(encodedUint8Array) && bi === decodedBI;
})
);

done();
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
PythGovernanceAction,
decodeGovernancePayload,
EvmSetWormholeAddress,
EvmExecutorAction,
EvmExecute,
} from "..";
import * as fc from "fast-check";
import { ChainName, CHAINS } from "../chains";
Expand Down Expand Up @@ -66,6 +68,14 @@ test("GovernancePayload ser/de", (done) => {
expect(governanceHeader?.targetChainId).toBe("solana");
expect(governanceHeader?.action).toBe("SetFee");

// Valid header 3
expectedGovernanceHeader = new PythGovernanceHeader("solana", "Execute");
buffer = expectedGovernanceHeader.encode();
expect(buffer.equals(Buffer.from([80, 84, 71, 77, 2, 0, 0, 1]))).toBeTruthy();
governanceHeader = PythGovernanceHeader.decode(buffer);
expect(governanceHeader?.targetChainId).toBe("solana");
expect(governanceHeader?.action).toBe("Execute");

// Wrong magic number
expect(
PythGovernanceHeader.decode(
Expand Down Expand Up @@ -157,6 +167,7 @@ function governanceHeaderArb(): Arbitrary<PythGovernanceHeader> {
const actions = [
...Object.keys(ExecutorAction),
...Object.keys(TargetAction),
...Object.keys(EvmExecutorAction),
] as ActionName[];
const actionArb = fc.constantFrom(...actions);
const targetChainIdArb = fc.constantFrom(
Expand Down Expand Up @@ -260,6 +271,24 @@ function governanceActionArb(): Arbitrary<PythGovernanceAction> {
return hexBytesArb({ minLength: 20, maxLength: 20 }).map((address) => {
return new EvmSetWormholeAddress(header.targetChainId, address);
});
} else if (header.action === "Execute") {
return fc
.record({
executerAddress: hexBytesArb({ minLength: 20, maxLength: 20 }),
callAddress: hexBytesArb({ minLength: 20, maxLength: 20 }),
value: fc.bigUintN(256),
callData: bufferArb(),
})
.map(
({ executerAddress, callAddress, value, callData }) =>
new EvmExecute(
header.targetChainId,
executerAddress,
callAddress,
value,
callData
)
);
} else {
throw new Error("Unsupported action type");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import { Layout } from "@solana/buffer-layout";
import { toBigIntBE, toBufferBE } from "bigint-buffer";

export class UInt64BE extends Layout<bigint> {
export class UIntBE extends Layout<bigint> {
// span is the number of bytes to read
constructor(span: number, property?: string) {
super(span, property);
}

// Note: we can not use read/writeBigUInt64BE because it is not supported in the browsers
override decode(b: Uint8Array, offset?: number): bigint {
let o = offset ?? 0;
const buffer = Buffer.from(b.slice(o, o + this.span));
const hi32 = buffer.readUInt32BE();
const lo32 = buffer.readUInt32BE(4);
return BigInt(lo32) + (BigInt(hi32) << BigInt(32));
return toBigIntBE(buffer);
}

override encode(src: bigint, b: Uint8Array, offset?: number): number {
const buffer = Buffer.alloc(this.span);
const hi32 = Number(src >> BigInt(32));
const lo32 = Number(src & BigInt(0xffffffff));
buffer.writeUInt32BE(hi32, 0);
buffer.writeUInt32BE(lo32, 4);
const buffer = toBufferBE(src, this.span);
b.set(buffer, offset);
return this.span;
}
Expand All @@ -45,8 +40,13 @@ export class HexBytes extends Layout<string> {
}

/** A big-endian u64, returned as a bigint. */
export function u64be(property?: string | undefined): UInt64BE {
return new UInt64BE(8, property);
export function u64be(property?: string | undefined): UIntBE {
return new UIntBE(8, property);
}

/** A big-endian u256, returned as a bigint. */
export function u256be(property?: string | undefined): UIntBE {
return new UIntBE(32, property);
}

/** An array of numBytes bytes, returned as a hexadecimal string. */
Expand Down
Loading

1 comment on commit f3ad917

@vercel
Copy link

@vercel vercel bot commented on f3ad917 Jan 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

xc-admin-frontend – ./

xc-admin-frontend-pyth-web.vercel.app
xc-admin-frontend-git-main-pyth-web.vercel.app
xc-admin-frontend.vercel.app

Please sign in to comment.