Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[contract_manager] Support for entropy and evm executor #1251

Merged
merged 11 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading