Skip to content

Commit

Permalink
Initial multisig support
Browse files Browse the repository at this point in the history
  • Loading branch information
solimander committed Mar 26, 2024
1 parent 11c0d00 commit 19bcbad
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 137 deletions.
2 changes: 1 addition & 1 deletion packages/prop-house-protocol/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@prophouse/protocol",
"version": "1.0.12",
"version": "1.0.13",
"license": "GPL-3.0",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
Expand Down
3 changes: 3 additions & 0 deletions packages/prop-house-protocol/src/addresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface EVMContracts {
messenger: string;
house: HouseImpls;
round: RoundImpls;
starknetCommit: string;
}

export interface StarknetContracts {
Expand Down Expand Up @@ -80,6 +81,7 @@ export const contracts: Record<number, ContractAddresses> = {
infinite: goerli.ethereum.address.infiniteRoundImpl,
timed: goerli.ethereum.address.timedRoundImpl,
},
starknetCommit: goerli.ethereum.address.starknetCommit,
},
starknet: {
roundFactory: goerli.starknet.address.roundFactory,
Expand Down Expand Up @@ -124,6 +126,7 @@ export const contracts: Record<number, ContractAddresses> = {
infinite: constants.HashZero,
timed: mainnet.ethereum.address.timedRoundImpl,
},
starknetCommit: mainnet.ethereum.address.starknetCommit,
},
starknet: {
roundFactory: mainnet.starknet.address.roundFactory,
Expand Down
1 change: 1 addition & 0 deletions packages/prop-house-protocol/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { PropHouse__factory } from '../typechain/factories/PropHouse__factory';
export { CommunityHouse__factory } from '../typechain/factories/CommunityHouse__factory';
export { InfiniteRound__factory } from '../typechain/factories/InfiniteRound__factory';
export { TimedRound__factory } from '../typechain/factories/TimedRound__factory';
export { StarknetCommit__factory } from '../typechain/factories/StarknetCommit__factory';

export { PropHouse as PropHouseContract } from '../typechain/PropHouse';
export { CommunityHouse as CommunityHouseContract } from '../typechain/CommunityHouse';
Expand Down
4 changes: 2 additions & 2 deletions packages/prop-house-protocol/test/utils/setup/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
MockStarknetMessaging__factory,
CreatorPassIssuer__factory,
PropHouse__factory,
StarkNetCommit__factory,
StarknetCommit__factory,
} from '../../../typechain';
import { ethers } from 'hardhat';
import { constants } from 'ethers';
Expand All @@ -17,7 +17,7 @@ export const commonL1Setup = async () => {
const creatorPassIssuerFactory = new CreatorPassIssuer__factory(deployer);

const mockStarknetMessagingFactory = new MockStarknetMessaging__factory(deployer);
const starknetCommitFactory = new StarkNetCommit__factory(deployer);
const starknetCommitFactory = new StarknetCommit__factory(deployer);
const messengerFactory = new Messenger__factory(deployer);

const manager = await managerFactory.deploy();
Expand Down
4 changes: 2 additions & 2 deletions packages/prop-house-sdk-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@prophouse/sdk-react",
"version": "1.0.32",
"version": "1.0.34",
"description": "Useful tools for interacting with the Prop House protocol from React applications",
"author": "solimander",
"homepage": "https://prop.house",
Expand All @@ -18,7 +18,7 @@
"wagmi": ">=0.9.2"
},
"dependencies": {
"@prophouse/sdk": "1.0.34"
"@prophouse/sdk": "1.0.37"
},
"devDependencies": {
"react": "^17.0.2",
Expand Down
4 changes: 2 additions & 2 deletions packages/prop-house-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@prophouse/sdk",
"version": "1.0.36",
"version": "1.0.37",
"description": "Useful tools for interacting with the Prop House protocol",
"author": "solimander",
"homepage": "https://prop.house",
Expand Down Expand Up @@ -34,7 +34,7 @@
"@ethersproject/strings": "~5.7.0",
"@ethersproject/wallet": "^5.7.0",
"@pinata/sdk": "^2.1.0",
"@prophouse/protocol": "1.0.11",
"@prophouse/protocol": "1.0.12",
"bn.js": "^5.2.1",
"graphql": "^16.5.0",
"graphql-request": "5.0.0",
Expand Down
103 changes: 101 additions & 2 deletions packages/prop-house-sdk/src/rounds/implementations/timed.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BigNumber } from '@ethersproject/bignumber';
import { BigNumber, BigNumberish } from '@ethersproject/bignumber';
import { parseEther } from '@ethersproject/units';
import {
AssetType,
Custom,
Expand All @@ -8,7 +9,7 @@ import {
RoundEventState,
GetRoundStateParams,
} from '../../types';
import { TimedRound__factory } from '@prophouse/protocol';
import { TimedRound__factory, StarknetCommit__factory } from '@prophouse/protocol';
import { encoding, intsSequence, splitUint256 } from '../../utils';
import { defaultAbiCoder } from '@ethersproject/abi';
import { ADDRESS_ONE } from '../../constants';
Expand Down Expand Up @@ -556,6 +557,74 @@ export class TimedRound<CS extends void | Custom = void> extends RoundBase<Round
};
}


/**
* Sign proposal votes and return the voter, signature, and signed message
* @param config The round address and proposal vote(s)
*/
public async getVoteCommitment(config: Timed.VoteConfig) {
const address = await this.signer.getAddress();
const suppliedVotingPower = config.votes.reduce(
(acc, { votingPower }) => acc.add(votingPower),
BigNumber.from(0),
);
if (suppliedVotingPower.eq(0)) {
throw new Error('Must vote on at least one proposal');
}
const { govPowerStrategiesRaw } = await this._query.getRoundVotingStrategies(config.round);

if (isAddress(config.round)) {
// If the origin chain round is provided, fetch the Starknet round address
config.round = await this._query.getStarknetRoundAddress(config.round);
}
const timestamp = await this.getVotingPeriodSnapshotTimestamp(config.round);
const nonZeroStrategyVotingPowers = await this._govPower.getPowerForStrategies(
address,
timestamp,
govPowerStrategiesRaw,
);
const totalVotingPower = nonZeroStrategyVotingPowers.reduce(
(acc, { govPower }) => acc.add(govPower),
BigNumber.from(0),
);
const spentVotingPower = await this.getSpentVotingPower(config.round, address);
const remainingVotingPower = totalVotingPower.sub(spentVotingPower);
if (suppliedVotingPower.gt(remainingVotingPower)) {
throw new Error('Not enough voting power remaining');
}

const userParams = await this._govPower.getUserParamsForStrategies(
address,
timestamp,
nonZeroStrategyVotingPowers.map(s => s.strategy),
);
const commitment = encoding.getCommit(config.round, hash.getSelectorFromName('vote'), this.getVoteCalldata({
voter: address,
proposalVotes: config.votes,
usedVotingStrategies: nonZeroStrategyVotingPowers.map(({ strategy }, i) => ({
id: strategy.id,
userParams: userParams[i],
})),
}));

const data = {
round: encoding.hexPadLeft(config.round),
voter: address,
proposalVotes: config.votes,
authStrategy: encoding.hexPadLeft(this._addresses.starknet.auth.timed.sig),
usedVotingStrategies: nonZeroStrategyVotingPowers.map(({ strategy }, i) => ({
id: strategy.id,
userParams: userParams[i],
})),
salt: this.generateSalt(),
};
return {
data,
address,
commitment
};
}

/**
* Sign proposal votes and submit them to the Starknet relayer
* @param config The round address and proposal vote(s)
Expand All @@ -570,6 +639,36 @@ export class TimedRound<CS extends void | Custom = void> extends RoundBase<Round
});
}

/**
* Commit proposal votes and submit them to the Starknet relayer
* @param config The round address and proposal vote(s)
* @param value The value to send with the commitment transaction. Defaults to 0.0001 ETH.
*/
public async voteViaCommitment(config: Timed.VoteConfig, value: BigNumberish = parseEther('0.0001')) {
const { address, data, commitment } = await this.getVoteCommitment(config);

// Submit to the relayer before initiating the commitment transaction
// to ensure the relayer has the data.
await this.sendToRelayer({
address,
commitment,
action: Timed.Action.VOTE,
data,
});

// Create the transaction
return await StarknetCommit__factory.connect(
this._addresses.evm.starknetCommit,
this.signer
).commit(
this._addresses.starknet.auth.timed.tx,
commitment,
{
value,
},
);
}

/**
* Relay a signed propose payload to Starknet
* @param account The Starknet account used to submit the transaction
Expand Down
4 changes: 2 additions & 2 deletions packages/prop-house-webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.17",
"@nouns/prop-house-wrapper": "1.0.0",
"@prophouse/protocol": "1.0.6",
"@prophouse/sdk-react": "1.0.31",
"@prophouse/protocol": "1.0.12",
"@prophouse/sdk-react": "1.0.33",
"@rainbow-me/rainbowkit": "^1.3.0",
"@reduxjs/toolkit": "^1.7.0",
"@supabase/supabase-js": "^2.39.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { openInNewTab } from '../../utils/openInNewTab';
import { GOV_POWER_OVERRIDES } from '../../utils/roundOverrides';
import { BigNumber } from 'ethers';
import { parsedVotingPower } from '../../utils/parsedVotingPower';
import { useEthersProvider } from '../../hooks/useEthersProvider';
import { signerIsContract } from '../../utils/signerIsContract';
import { Hex } from 'viem';

const VoteConfirmationModal: React.FC<{
round: Round;
Expand All @@ -24,6 +27,7 @@ const VoteConfirmationModal: React.FC<{
const { setShowVoteConfirmationModal, round } = props;

const propHouse = usePropHouse();
const provider = useEthersProvider();
const dispatch = useDispatch();
const proposals = useAppSelector(state => state.propHouse.activeProposals);
const proposal = useAppSelector(state => state.propHouse.activeProposal);
Expand Down Expand Up @@ -64,33 +68,48 @@ const VoteConfirmationModal: React.FC<{
return { proposalId: a.proposalId, votingPower: votes };
});

const result = await propHouse.round.timed.voteViaSignature({
round: round.address,
votes,
});

if (!result?.transaction_hash) {
throw new Error(`Vote submission failed: ${result}`);
const isContract = await signerIsContract(
provider,
(await propHouse.signer.getAddress()) as Hex,
);
if (isContract) {
const result = await propHouse.round.timed.voteViaCommitment({
round: round.address,
votes,
});
if (!result?.hash) {
throw new Error(`Vote submission failed: ${result}`);
}

setCurrentModalData(contractSuccessData);
} else {
const result = await propHouse.round.timed.voteViaSignature({
round: round.address,
votes,
});
if (!result?.transaction_hash) {
throw new Error(`Vote submission failed: ${result}`);
}

setCurrentModalData(eoaSuccessData);

// refresh props with new votes
// check if we're updating from the round page (multiple props) or the prop page (single prop)
const propsToCheck = proposals ? proposals : proposal ? [proposal] : [];
const updatedProps = propsToCheck.map(prop => {
const voteForProp = votes.find(v => v.proposalId === prop.id);
let newProp = { ...prop };
if (voteForProp)
newProp.votingPower = parsedVotingPower(newProp.votingPower, round.address)
.add(parsedVotingPower(voteForProp.votingPower, round.address))
.toString();
return newProp;
});
proposals
? dispatch(setOnChainActiveProposals(updatedProps))
: dispatch(setOnchainActiveProposal(updatedProps[0]));
dispatch(clearVoteAllotments());
}

setCurrentModalData(successData);

// refresh props with new votes
// check if we're updating from the round page (multiple props) or the prop page (single prop)
const propsToCheck = proposals ? proposals : proposal ? [proposal] : [];
const updatedProps = propsToCheck.map(prop => {
const voteForProp = votes.find(v => v.proposalId === prop.id);
let newProp = { ...prop };
if (voteForProp)
newProp.votingPower = parsedVotingPower(newProp.votingPower, round.address)
.add(parsedVotingPower(voteForProp.votingPower, round.address))
.toString();
return newProp;
});
proposals
? dispatch(setOnChainActiveProposals(updatedProps))
: dispatch(setOnchainActiveProposal(updatedProps[0]));
dispatch(clearVoteAllotments());
} catch (e: any) {
console.log(e);
setCurrentModalData(errorData(e.message));
Expand Down Expand Up @@ -139,11 +158,9 @@ const VoteConfirmationModal: React.FC<{
};
};

const successData: ModalProps = {
const getSuccessData = (subtitle: string) => ({
title: 'Nounish',
subtitle: `You successfully voted for ${numPropsVotedFor} ${
numPropsVotedFor === 1 ? 'prop' : 'props'
}`,
subtitle,
image: NounImage.Glasses,
button: (
<>
Expand All @@ -166,7 +183,17 @@ const VoteConfirmationModal: React.FC<{
</>
),
setShowModal: setShowVoteConfirmationModal,
};
});

const eoaSuccessData: ModalProps = getSuccessData(
`You successfully voted for ${numPropsVotedFor} ${numPropsVotedFor === 1 ? 'prop' : 'props'}`,
);

const contractSuccessData: ModalProps = getSuccessData(
`You successfully submitted votes for ${numPropsVotedFor} ${
numPropsVotedFor === 1 ? 'prop' : 'props'
}, which will be confirmed shortly.`,
);

const [currentModalData, setCurrentModalData] = React.useState<ModalProps>(confirmVoteData);

Expand Down
4 changes: 1 addition & 3 deletions packages/prop-house-webapp/src/utils/signerIsContract.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Provider } from '@ethersproject/abstract-provider';
import { Signer } from '@ethersproject/abstract-signer';

export const signerIsContract = async (
signer: Signer | undefined,
provider: Provider,
account: `0x${string}` | undefined,
) => {
if (!signer || !provider || !account) return false;
if (!provider || !account) return false;

const code = await provider.getCode(account);
const isContract = code !== '0x';
Expand Down
Loading

0 comments on commit 19bcbad

Please sign in to comment.