diff --git a/packages/prop-house-protocol/contracts/starknet/src/common/power.cairo b/packages/prop-house-protocol/contracts/starknet/src/common/power.cairo index 3e7d2548e..53cc1093d 100644 --- a/packages/prop-house-protocol/contracts/starknet/src/common/power.cairo +++ b/packages/prop-house-protocol/contracts/starknet/src/common/power.cairo @@ -2,4 +2,5 @@ mod allowlist; mod ethereum_balance_of_erc20; mod ethereum_balance_of_erc1155; mod ethereum_balance_of; +mod ethereum_checkpointable_erc721; mod vanilla; diff --git a/packages/prop-house-protocol/contracts/starknet/src/common/power/ethereum_balance_of_erc1155.cairo b/packages/prop-house-protocol/contracts/starknet/src/common/power/ethereum_balance_of_erc1155.cairo index 4116f186e..739b3d1b0 100644 --- a/packages/prop-house-protocol/contracts/starknet/src/common/power/ethereum_balance_of_erc1155.cairo +++ b/packages/prop-house-protocol/contracts/starknet/src/common/power/ethereum_balance_of_erc1155.cairo @@ -36,7 +36,7 @@ mod EthereumBalanceOfERC1155GovernancePowerStrategy { let slot_index = *params.at(1); let token_id = *params.at(2); - let mut mapping_keys = array![user.into(), token_id.into()]; + let mut mapping_keys = array![token_id.into(), user.into()]; let valid_slot = get_nested_slot_key(slot_index.into(), mapping_keys.span()); let governance_power = SingleSlotProof::get_slot_value( diff --git a/packages/prop-house-protocol/contracts/starknet/src/common/power/ethereum_checkpointable_erc721.cairo b/packages/prop-house-protocol/contracts/starknet/src/common/power/ethereum_checkpointable_erc721.cairo new file mode 100644 index 000000000..54b87a26c --- /dev/null +++ b/packages/prop-house-protocol/contracts/starknet/src/common/power/ethereum_checkpointable_erc721.cairo @@ -0,0 +1,67 @@ +#[starknet::contract] +mod EthereumCheckpointableERC721GovernancePowerStrategy { + use starknet::ContractAddress; + use prop_house::common::utils::constants::{MASK_96, TWO_POW_32}; + use prop_house::common::utils::traits::IGovernancePowerStrategy; + use prop_house::common::libraries::single_slot_proof::SingleSlotProof; + use prop_house::common::utils::storage::{get_slot_key, get_nested_slot_key}; + use array::{ArrayTrait, SpanTrait}; + use option::OptionTrait; + use zeroable::Zeroable; + use traits::Into; + + #[storage] + struct Storage {} + + #[constructor] + fn constructor(ref self: ContractState, fact_registry: ContractAddress, ethereum_block_registry: ContractAddress) { + let mut ssp_state = SingleSlotProof::unsafe_new_contract_state(); + SingleSlotProof::initializer(ref ssp_state, fact_registry, ethereum_block_registry); + } + + #[external(v0)] + impl EthereumCheckpointableERC721GovernancePowerStrategy of IGovernancePowerStrategy { + /// Returns the governance power of the user at the given timestamp. + /// * `timestamp` - The timestamp at which to get the governance power. + /// * `user` - The address of the user. + /// * `params` - The params, containing the contract address and the slot indices. + /// * `user_params` - The user params, containing the slots and MPT proofs. + fn get_power( + self: @ContractState, timestamp: u64, user: felt252, params: Span, mut user_params: Span, + ) -> u256 { + let params_len = params.len(); + + // Expects contract_address, num_checkpoints_slot_index, and checkpoints_slot_index, with an optional governance_power_multiplier + assert(params_len == 3 || params_len == 4, 'EthC721: Bad param length'); + + let contract_address = *params.at(0); + let num_checkpoints_slot_index = *params.at(1); + let checkpoints_slot_index = *params.at(2); + + let (num_checkpoints_user_params, latest_checkpoint_user_params) = Serde::<(Span, Span)>::deserialize(ref user_params).unwrap(); + + let num_checkpoints_slot = get_slot_key(num_checkpoints_slot_index.into(), user.into()); + let num_checkpoints = SingleSlotProof::get_slot_value( + @SingleSlotProof::unsafe_new_contract_state(), timestamp, contract_address, num_checkpoints_slot, params, num_checkpoints_user_params + ); + assert(num_checkpoints.is_non_zero(), 'EthC721: No checkpoints'); + + let latest_checkpoint_slot = get_nested_slot_key(checkpoints_slot_index.into(), array![user.into(), num_checkpoints - 1].span()); + let latest_checkpoint = SingleSlotProof::get_slot_value( + @SingleSlotProof::unsafe_new_contract_state(), timestamp, contract_address, latest_checkpoint_slot, params, latest_checkpoint_user_params + ); + let governance_power = (latest_checkpoint / TWO_POW_32) & MASK_96; + + assert(governance_power.is_non_zero(), 'EthC721: No governance power'); + + if params_len == 3 { + return governance_power; + } + + let governance_power_multiplier = *params.at(3); + assert(governance_power_multiplier.is_non_zero(), 'EthC721: Invalid multiplier'); + + governance_power * governance_power_multiplier.into() + } + } +} diff --git a/packages/prop-house-protocol/contracts/starknet/src/common/utils/constants.cairo b/packages/prop-house-protocol/contracts/starknet/src/common/utils/constants.cairo index 047350b79..a6aacc1d7 100644 --- a/packages/prop-house-protocol/contracts/starknet/src/common/utils/constants.cairo +++ b/packages/prop-house-protocol/contracts/starknet/src/common/utils/constants.cairo @@ -4,12 +4,14 @@ const MASK_8: u256 = 0xFF; const MASK_16: u256 = 0xFFFF; const MASK_32: u256 = 0xFFFFFFFF; const MASK_64: u256 = 0xFFFFFFFFFFFFFFFF; +const MASK_96: u256 = 0xFFFFFFFFFFFFFFFFFFFFFFFF; const MASK_160: u256 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; const MASK_192: u256 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; const MASK_250: u256 = 0x03FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; const TWO_POW_8: u256 = 0x100; const TWO_POW_24: u256 = 0x1000000; +const TWO_POW_32: u256 = 0x100000000; const TWO_POW_72: u256 = 0x1000000000000000000; const TWO_POW_88: u256 = 0x10000000000000000000000; const TWO_POW_152: u256 = 0x100000000000000000000000000000000000000; diff --git a/packages/prop-house-protocol/deployments/goerli.json b/packages/prop-house-protocol/deployments/goerli.json index cb67bb450..5f7ef2a0a 100644 --- a/packages/prop-house-protocol/deployments/goerli.json +++ b/packages/prop-house-protocol/deployments/goerli.json @@ -27,6 +27,7 @@ "ethBalanceOfGovPowerStrategy": "0x4ff2eee9b0d91eda2ae6dd620d34f5c6ddf11990d948088753acb2a3cca2f93", "ethBalanceOfErc20GovPowerStrategy": "0x000000000000000000000000000000000000000000000000000000000000000", "ethBalanceOfErc1155GovPowerStrategy": "0x3016c4e41fe92c2ce5cc1a4686c7c22ba5d810e7686546c8a44e8c78a4ba154", + "ethCheckpointableErc721GovPowerStrategy": "0x000000000000000000000000000000000000000000000000000000000000000", "herodotus": { "factRegistry": "0x5e6c5b45485f2eb7609a27e413aad727536b3590a64e18ceb5950e30852288f", "l1HeadersStore": "0x1d9b36a00d7d5300e5da456c56d09c46dfefbc91b3a6b1552b6f2a34d6e34c4" diff --git a/packages/prop-house-protocol/deployments/mainnet.json b/packages/prop-house-protocol/deployments/mainnet.json index 58f1d8900..85ad2e970 100644 --- a/packages/prop-house-protocol/deployments/mainnet.json +++ b/packages/prop-house-protocol/deployments/mainnet.json @@ -26,7 +26,8 @@ "allowlistGovPowerStrategy": "0x3daa40ef909961a576f9ba58eb063d5ebc85411063a8b29435f05af6167079c", "ethBalanceOfGovPowerStrategy": "0x6ddcc94a4225843546a9b118a2733fd924d6b8a6467279cbe6a1aea79daca54", "ethBalanceOfErc20GovPowerStrategy": "0x196cf5ceba8e98abe1e633d6184cd28c1e1ebd09ea71f89867dd4c5fda97bbe", - "ethBalanceOfErc1155GovPowerStrategy": "0x6d22f17522d6992eb479deb850e96f9454fc2f6c127993ab2ef9d411f467e8", + "ethBalanceOfErc1155GovPowerStrategy": "0x44e3bdffcb6ce36596d0faa4316932c5dc47005b9eaaf0f7ce0f455c98b2e75", + "ethCheckpointableErc721GovPowerStrategy": "0x10f7529ec5df9069a06191deb7cd2c4158c2b59879e2544cb45dc221daff429", "herodotus": { "factRegistry": "0x002081b2d3de51f295e7516257f68bd9f06acbc7f19ba49410c100afbe57540f", "l1HeadersStore": "0x008caacc818a97ef9122aa67b3b0e14d10e2959b262e7e785f47e20a36ef0ce0" diff --git a/packages/prop-house-protocol/package.json b/packages/prop-house-protocol/package.json index 2d5d3e470..9bd437da1 100644 --- a/packages/prop-house-protocol/package.json +++ b/packages/prop-house-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@prophouse/protocol", - "version": "1.0.7", + "version": "1.0.9", "license": "GPL-3.0", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", diff --git a/packages/prop-house-protocol/src/addresses.ts b/packages/prop-house-protocol/src/addresses.ts index b6bf8f862..1105d1cb9 100644 --- a/packages/prop-house-protocol/src/addresses.ts +++ b/packages/prop-house-protocol/src/addresses.ts @@ -22,6 +22,7 @@ export interface GovPowerStrategies { balanceOf: string; balanceOfErc20: string; balanceOfErc1155: string; + checkpointableErc721: string; vanilla: string; } @@ -89,6 +90,7 @@ export const contracts: Record = { balanceOf: goerli.starknet.address.ethBalanceOfGovPowerStrategy, balanceOfErc20: goerli.starknet.address.ethBalanceOfErc20GovPowerStrategy, balanceOfErc1155: goerli.starknet.address.ethBalanceOfErc1155GovPowerStrategy, + checkpointableErc721: goerli.starknet.address.ethCheckpointableErc721GovPowerStrategy, vanilla: goerli.starknet.address.vanillaGovPowerStrategy, }, auth: { @@ -132,6 +134,7 @@ export const contracts: Record = { balanceOf: mainnet.starknet.address.ethBalanceOfGovPowerStrategy, balanceOfErc20: mainnet.starknet.address.ethBalanceOfErc20GovPowerStrategy, balanceOfErc1155: mainnet.starknet.address.ethBalanceOfErc1155GovPowerStrategy, + checkpointableErc721: mainnet.starknet.address.ethCheckpointableErc721GovPowerStrategy, vanilla: constants.HashZero, }, auth: { diff --git a/packages/prop-house-sdk-react/package.json b/packages/prop-house-sdk-react/package.json index 402d4f41f..e9c965cc0 100644 --- a/packages/prop-house-sdk-react/package.json +++ b/packages/prop-house-sdk-react/package.json @@ -1,6 +1,6 @@ { "name": "@prophouse/sdk-react", - "version": "1.0.16", + "version": "1.0.18", "description": "Useful tools for interacting with the Prop House protocol from React applications", "author": "solimander", "homepage": "https://prop.house", @@ -18,7 +18,7 @@ "wagmi": ">=0.9.2" }, "dependencies": { - "@prophouse/sdk": "1.0.21" + "@prophouse/sdk": "1.0.23" }, "devDependencies": { "react": "^17.0.2", diff --git a/packages/prop-house-sdk/package.json b/packages/prop-house-sdk/package.json index 6715705ca..cead132f8 100644 --- a/packages/prop-house-sdk/package.json +++ b/packages/prop-house-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@prophouse/sdk", - "version": "1.0.22", + "version": "1.0.24", "description": "Useful tools for interacting with the Prop House protocol", "author": "solimander", "homepage": "https://prop.house", @@ -32,7 +32,7 @@ "@ethersproject/strings": "~5.7.0", "@ethersproject/wallet": "^5.7.0", "@pinata/sdk": "^2.1.0", - "@prophouse/protocol": "1.0.6", + "@prophouse/protocol": "1.0.8", "bn.js": "^5.2.1", "ethereumjs-fork-block": "^4.2.4", "ethereumjs-fork-common": "^3.1.3", diff --git a/packages/prop-house-sdk/src/constants.ts b/packages/prop-house-sdk/src/constants.ts index 5fa95c072..06b4ae660 100644 --- a/packages/prop-house-sdk/src/constants.ts +++ b/packages/prop-house-sdk/src/constants.ts @@ -17,6 +17,18 @@ export const BALANCE_OF_FUNC = 'function balanceOf(address account) external vie // prettier-ignore export const BALANCE_OF_ERC1155_FUNC = 'function balanceOf(address account, uint256 id) external view returns (uint256)'; +/** + * The `getCurrentVotes` function signature. + */ +// prettier-ignore +export const GET_CURRENT_VOTES_FUNC = 'function getCurrentVotes(address account) external view returns (uint96)'; + +/** + * The `numCheckpoints` function signature. + */ +// prettier-ignore +export const NUM_CHECKPOINTS_FUNC = 'function numCheckpoints(address account) external view returns (uint32)'; + /** * The address used to query `balanceOf` functions to detect the slot index. */ diff --git a/packages/prop-house-sdk/src/gov-power/handlers/balance-of-erc1155.ts b/packages/prop-house-sdk/src/gov-power/handlers/balance-of-erc1155.ts index 4d265ddd0..492673062 100644 --- a/packages/prop-house-sdk/src/gov-power/handlers/balance-of-erc1155.ts +++ b/packages/prop-house-sdk/src/gov-power/handlers/balance-of-erc1155.ts @@ -75,7 +75,7 @@ export class BalanceOfERC1155Handler extends SingleSlotProofHandler { + /** + * Information about the Nouns mainnet ERC721 token + */ + private static readonly _NOUNS = { + ADDRESS: '0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03', + NUM_CHECKPOINTS_SLOT_INDEX: '0x0d', + CHECKPOINTS_SLOT_INDEX: '0x0c', + }; + + /** + * Returns a `BalanceOfHandler` instance for the provided chain configuration + * @param config The chain config + */ + public static for(config: ChainConfig) { + return new CheckpointableERC721Handler(config); + } + + /** + * The governance power strategy type + */ + public get type() { + return GovPowerStrategyType.CHECKPOINTABLE_ERC721; + } + + /** + * The governance power strategy address + */ + public get address() { + return this._addresses.starknet.govPower.checkpointableErc721; + } + + /** + * @notice Get the governance power strategy params that will be shared amongst all users + * @param strategy The governance power strategy information + */ + public async getStrategyParams(strategy: CheckpointableERC721Config): Promise { + if (strategy.address.toLowerCase() !== CheckpointableERC721Handler._NOUNS.ADDRESS) { + throw new Error('This handler currently only supports the Nouns ERC721 token'); + } + if (strategy.multiplier && BigNumber.from(strategy.multiplier).gt(1)) { + return [ + strategy.address, + CheckpointableERC721Handler._NOUNS.NUM_CHECKPOINTS_SLOT_INDEX, + CheckpointableERC721Handler._NOUNS.CHECKPOINTS_SLOT_INDEX, + strategy.multiplier.toString() + ]; + } + return [ + strategy.address, + CheckpointableERC721Handler._NOUNS.NUM_CHECKPOINTS_SLOT_INDEX, + CheckpointableERC721Handler._NOUNS.CHECKPOINTS_SLOT_INDEX, + ]; + } + + public async getUserParams(account: string, timestamp: string, strategyId: string) { + const strategy = await this.getStrategyAddressAndParams(strategyId); + const [contractAddress, numCheckpointsSlotIndex, checkpointsSlotIndex] = strategy.params; + + const numCheckpointsSlotKey = encoding.getSlotKey(account, numCheckpointsSlotIndex); + const numCheckpointsSlotKeyU256 = splitUint256.SplitUint256.fromHex(numCheckpointsSlotKey); + + const block = await this.getBlockNumberForTimestamp(timestamp); + const numCheckpoints = await this.contractFor(contractAddress).numCheckpoints(account, { + blockTag: block, + }); + const checkpointToQuery = `0x${(Number(numCheckpoints) - 1).toString(16)}`; + + const checkpointsSlotKey = encoding.getNestedSlotKey([account, checkpointToQuery], checkpointsSlotIndex); + const checkpointsSlotKeyU256 = splitUint256.SplitUint256.fromHex(checkpointsSlotKey); + + const [numCheckpointsProofInputs, checkpointsProofInputs] = await Promise.all([ + this.fetchProofInputs(contractAddress, numCheckpointsSlotKey, block), + this.fetchProofInputs(contractAddress, checkpointsSlotKey, block), + ]); + + const numCheckpointsUserParams = [ + // Storage Key (u256) + numCheckpointsSlotKeyU256.low, + numCheckpointsSlotKeyU256.high, + // Storage Proof + `0x${numCheckpointsProofInputs.storageProofSubArrayLength.toString(16)}`, + ...numCheckpointsProofInputs.storageProof, + ]; + const checkpointsUserParams = [ + // Storage Key (u256) + checkpointsSlotKeyU256.low, + checkpointsSlotKeyU256.high, + // Storage Proof + `0x${checkpointsProofInputs.storageProofSubArrayLength.toString(16)}`, + ...checkpointsProofInputs.storageProof, + ]; + + + return [ + `0x${numCheckpointsUserParams.length.toString(16)}`, + ...numCheckpointsUserParams, + `0x${checkpointsUserParams.length.toString(16)}`, + ...checkpointsUserParams, + ] + } + + public async getStrategyPreCalls( + account: string, + timestamp: string, + strategyId: string, + ): Promise { + const strategy = await this.getStrategyAddressAndParams(strategyId); + const [contractAddress, numCheckpointsSlotIndex] = strategy.params; + + // Only the account proof is used, so it's okay to only query with the first slot key. + const slotKey = encoding.getSlotKey(account, numCheckpointsSlotIndex); + + const block = await this.getBlockNumberForTimestamp(timestamp); + const storageHash = await this.getStorageHash(contractAddress, block); + + // We only need to prove the account if the storage hash hasn't been populated. + if (storageHash.isZero()) { + const [proofInputs, processBlockInputs] = await Promise.all([ + this.fetchProofInputs(contractAddress, slotKey, block), + storageProofs.getProcessBlockInputsForBlockNumber( + this.provider, + block, + this._evmChainId, + ), + ]); + return [ + { + contractAddress: this._addresses.starknet.herodotus.factRegistry, + entrypoint: 'prove_account', + calldata: [ + // Account Fields + 1, + AccountField.StorageHash, + // Block Header RLP + processBlockInputs.headerInts.length, + ...processBlockInputs.headerInts, + // Account + contractAddress, + // Proof + proofInputs.accountProofSubArrayLength, + ...proofInputs.accountProof, + ], + }, + ]; + } + return []; + } + + /** + * Get the total governance power for the provided config + * @param config The governance power strategy config information + */ + public async getPower(config: GovPowerConfig): Promise { + const block = await this.getBlockNumberForTimestamp(config.timestamp); + const token = BigNumber.from(config.params[0]).toHexString(); + const balance = await this.contractFor(token).getCurrentVotes(config.user, { + blockTag: block, + }); + return balance.mul(config.params?.[3] ?? 1); + } + + /** + * Returns a contract instance for the provided token address + * @param token The token address + */ + private contractFor(token: string) { + return new Contract(token, [GET_CURRENT_VOTES_FUNC, NUM_CHECKPOINTS_FUNC], this._evm); + } +} diff --git a/packages/prop-house-sdk/src/gov-power/handlers/index.ts b/packages/prop-house-sdk/src/gov-power/handlers/index.ts index 8994a8f5a..e8893dd30 100644 --- a/packages/prop-house-sdk/src/gov-power/handlers/index.ts +++ b/packages/prop-house-sdk/src/gov-power/handlers/index.ts @@ -3,4 +3,5 @@ export { AllowlistHandler } from './allowlist'; export { BalanceOfHandler } from './balance-of'; export { BalanceOfERC20Handler } from './balance-of-erc20'; export { BalanceOfERC1155Handler } from './balance-of-erc1155'; +export { CheckpointableERC721Handler } from './checkpointable-erc721'; export { VanillaHandler } from './vanilla'; diff --git a/packages/prop-house-sdk/src/gov-power/manager.ts b/packages/prop-house-sdk/src/gov-power/manager.ts index 5377bff1e..7a741968a 100644 --- a/packages/prop-house-sdk/src/gov-power/manager.ts +++ b/packages/prop-house-sdk/src/gov-power/manager.ts @@ -12,13 +12,14 @@ import { BalanceOfHandler, BalanceOfERC1155Handler, BalanceOfERC20Handler, + CheckpointableERC721Handler, VanillaHandler, AllowlistHandler, StrategyHandlerBase, } from './handlers'; export class GovPowerManager { - private readonly _defaults = [BalanceOfHandler, BalanceOfERC20Handler, BalanceOfERC1155Handler, VanillaHandler, AllowlistHandler]; + private readonly _defaults = [BalanceOfHandler, BalanceOfERC20Handler, BalanceOfERC1155Handler, CheckpointableERC721Handler, VanillaHandler, AllowlistHandler]; private readonly _all: StrategyHandlerBase>[]; constructor(private readonly _config: GovPowerChainConfig) { diff --git a/packages/prop-house-sdk/src/gql/evm/graphql.ts b/packages/prop-house-sdk/src/gql/evm/graphql.ts index 6bc000ec9..ba70ae346 100644 --- a/packages/prop-house-sdk/src/gql/evm/graphql.ts +++ b/packages/prop-house-sdk/src/gql/evm/graphql.ts @@ -853,6 +853,7 @@ export enum GovPowerStrategyType { Allowlist = 'ALLOWLIST', BalanceOf = 'BALANCE_OF', BalanceOfErc1155 = 'BALANCE_OF_ERC1155', + CheckpointableErc721 = 'CHECKPOINTABLE_ERC721', Unknown = 'UNKNOWN', Vanilla = 'VANILLA', } diff --git a/packages/prop-house-sdk/src/gql/query-wrapper.ts b/packages/prop-house-sdk/src/gql/query-wrapper.ts index 1a8909795..153c2bbac 100644 --- a/packages/prop-house-sdk/src/gql/query-wrapper.ts +++ b/packages/prop-house-sdk/src/gql/query-wrapper.ts @@ -960,6 +960,15 @@ export class QueryWrapper { ...(multiplier ? { multiplier: Number(multiplier) } : {}), }; }; + case GovPowerStrategyType.CHECKPOINTABLE_ERC721: { + const [address, , , multiplier] = strategy.params; + return { + id: strategy.id, + strategyType: GovPowerStrategyType.CHECKPOINTABLE_ERC721, + tokenAddress: `0x${BigInt(address).toString(16)}`, + ...(multiplier ? { multiplier: Number(multiplier) } : {}), + }; + }; case GovPowerStrategyType.ALLOWLIST: { const cid = encoding.stringFromLE(strategy.params.slice(1).map(p => p.toString())); const allowlist: AllowlistJson = await ipfs.getJSON(cid); diff --git a/packages/prop-house-sdk/src/gql/types.ts b/packages/prop-house-sdk/src/gql/types.ts index 64f9532a8..2177c8f9a 100644 --- a/packages/prop-house-sdk/src/gql/types.ts +++ b/packages/prop-house-sdk/src/gql/types.ts @@ -50,6 +50,13 @@ export interface BalanceOfERC1155Strategy { multiplier?: number; } +export interface CheckpointableERC721Strategy { + id: string; + strategyType: GovPowerStrategyType.CHECKPOINTABLE_ERC721; + tokenAddress: string; + multiplier?: number; +} + export interface AllowlistStrategy { id: string; strategyType: GovPowerStrategyType.ALLOWLIST; @@ -65,7 +72,7 @@ export interface UnknownStrategy extends GovPowerStrategyWithID { strategyType: GovPowerStrategyType.UNKNOWN; } -export type ParsedGovPowerStrategy = BalanceOfStrategy | BalanceOfERC20Strategy | BalanceOfERC1155Strategy | AllowlistStrategy | VanillaStrategy | UnknownStrategy; +export type ParsedGovPowerStrategy = BalanceOfStrategy | BalanceOfERC20Strategy | BalanceOfERC1155Strategy | CheckpointableERC721Strategy | AllowlistStrategy | VanillaStrategy | UnknownStrategy; export type ProposingStrategy = ParsedGovPowerStrategy; export type VotingStrategy = ParsedGovPowerStrategy; diff --git a/packages/prop-house-sdk/src/types.ts b/packages/prop-house-sdk/src/types.ts index 4e265612b..7a863e627 100644 --- a/packages/prop-house-sdk/src/types.ts +++ b/packages/prop-house-sdk/src/types.ts @@ -452,6 +452,7 @@ export enum GovPowerStrategyType { BALANCE_OF_ERC1155 = 'BALANCE_OF_ERC1155', BALANCE_OF_ERC20 = 'BALANCE_OF_ERC20', BALANCE_OF = 'BALANCE_OF', + CHECKPOINTABLE_ERC721 = 'CHECKPOINTABLE_ERC721', ALLOWLIST = 'ALLOWLIST', VANILLA = 'VANILLA', UNKNOWN = 'UNKNOWN', @@ -486,6 +487,13 @@ export interface BalanceOfERC1155Config { multiplier?: number; } +export interface CheckpointableERC721Config { + strategyType: GovPowerStrategyType.CHECKPOINTABLE_ERC721; + assetType: AssetType.ERC721; + address: string; + multiplier?: number; +} + export interface AllowlistMember { address: string; govPower: string; @@ -504,7 +512,7 @@ export interface Custom { strategyType: string; } -export type DefaultGovPowerConfigs = BalanceOfConfig | BalanceOfERC20Config | BalanceOfERC1155Config | AllowlistConfig | VanillaConfig; +export type DefaultGovPowerConfigs = BalanceOfConfig | BalanceOfERC20Config | BalanceOfERC1155Config | CheckpointableERC721Config | AllowlistConfig | VanillaConfig; // prettier-ignore export type GovPowerStrategyConfig = C extends void ? DefaultGovPowerConfigs : DefaultGovPowerConfigs | C; diff --git a/packages/prop-house-subgraph/schema.graphql b/packages/prop-house-subgraph/schema.graphql index a0d466c25..6caca2889 100644 --- a/packages/prop-house-subgraph/schema.graphql +++ b/packages/prop-house-subgraph/schema.graphql @@ -11,6 +11,7 @@ enum GovPowerStrategyType { BALANCE_OF BALANCE_OF_ERC20 BALANCE_OF_ERC1155 + CHECKPOINTABLE_ERC721 UNKNOWN } diff --git a/packages/prop-house-subgraph/src/lib/constants.ts b/packages/prop-house-subgraph/src/lib/constants.ts index 9a6f083fb..df92efb23 100644 --- a/packages/prop-house-subgraph/src/lib/constants.ts +++ b/packages/prop-house-subgraph/src/lib/constants.ts @@ -25,6 +25,7 @@ export namespace GovPowerStrategyType { export const BALANCE_OF = 'BALANCE_OF'; export const BALANCE_OF_ERC20 = 'BALANCE_OF_ERC20'; export const BALANCE_OF_ERC1155 = 'BALANCE_OF_ERC1155'; + export const CHECKPOINTABLE_ERC721 = 'CHECKPOINTABLE_ERC721'; export const UNKNOWN = 'UNKNOWN'; } diff --git a/packages/prop-house-subgraph/src/lib/utils.ts b/packages/prop-house-subgraph/src/lib/utils.ts index c1ff5979f..d04341abf 100644 --- a/packages/prop-house-subgraph/src/lib/utils.ts +++ b/packages/prop-house-subgraph/src/lib/utils.ts @@ -99,9 +99,12 @@ export function getGovPowerStrategyType(addr: string): string { if (addr == '0x196cf5ceba8e98abe1e633d6184cd28c1e1ebd09ea71f89867dd4c5fda97bbe') { return GovPowerStrategyType.BALANCE_OF_ERC20; } - if (addr == '0x6d22f17522d6992eb479deb850e96f9454fc2f6c127993ab2ef9d411f467e8') { + if (addr == '0x6d22f17522d6992eb479deb850e96f9454fc2f6c127993ab2ef9d411f467e8' || addr == '0x44e3bdffcb6ce36596d0faa4316932c5dc47005b9eaaf0f7ce0f455c98b2e75') { return GovPowerStrategyType.BALANCE_OF_ERC1155; } + if (addr == '0x10f7529ec5df9069a06191deb7cd2c4158c2b59879e2544cb45dc221daff429') { + return GovPowerStrategyType.CHECKPOINTABLE_ERC721; + } if (addr == '0x3daa40ef909961a576f9ba58eb063d5ebc85411063a8b29435f05af6167079c') { return GovPowerStrategyType.ALLOWLIST; } diff --git a/packages/prop-house-webapp/package.json b/packages/prop-house-webapp/package.json index 333f4278b..6f14f7d8b 100644 --- a/packages/prop-house-webapp/package.json +++ b/packages/prop-house-webapp/package.json @@ -10,7 +10,7 @@ "@fortawesome/react-fontawesome": "^0.1.17", "@nouns/prop-house-wrapper": "1.0.0", "@prophouse/protocol": "1.0.6", - "@prophouse/sdk-react": "1.0.15", + "@prophouse/sdk-react": "1.0.17", "@rainbow-me/rainbowkit": "^1.3.0", "@reduxjs/toolkit": "^1.7.0", "@supabase/supabase-js": "^2.39.0", diff --git a/packages/prop-house-webapp/src/components/HouseManager/AddVoter/index.tsx b/packages/prop-house-webapp/src/components/HouseManager/AddVoter/index.tsx index 25a445210..307563c15 100644 --- a/packages/prop-house-webapp/src/components/HouseManager/AddVoter/index.tsx +++ b/packages/prop-house-webapp/src/components/HouseManager/AddVoter/index.tsx @@ -132,13 +132,13 @@ const AddVoter: React.FC<{ ...newVoter, type: VotingStrategyType.BALANCE_OF, asset: AssetType.ERC721, - }) ; + }); } else if (selectedType === StrategyType.ERC20) { setVoter({ ...newVoter, type: VotingStrategyType.BALANCE_OF_ERC20, asset: AssetType.ERC20, - }) ; + }); } else if (selectedType === StrategyType.ERC1155) { setVoter({ ...newVoter, diff --git a/packages/prop-house-webapp/src/components/ProposingStrategiesDisplay/index.tsx b/packages/prop-house-webapp/src/components/ProposingStrategiesDisplay/index.tsx index a48edd5fa..a26ba5c39 100644 --- a/packages/prop-house-webapp/src/components/ProposingStrategiesDisplay/index.tsx +++ b/packages/prop-house-webapp/src/components/ProposingStrategiesDisplay/index.tsx @@ -78,6 +78,17 @@ const ProposingStrategiesDisplay: React.FC<{ ); + if (stratType === GovPowerStrategyType.CHECKPOINTABLE_ERC721) + copy = ( + <> + Owners or delegates of the{' '} + + {display(strat.tokenAddress)} + {' '} + token can propose. {propThreshold > 1 && `${propThreshold} tokens required`} + + ); + if (stratType === GovPowerStrategyType.UNKNOWN) copy = <>Error reading proposing strategy; return formattedContent(copy); diff --git a/packages/prop-house-webapp/src/components/VotingStrategiesDisplay/index.tsx b/packages/prop-house-webapp/src/components/VotingStrategiesDisplay/index.tsx index 5cbeb128a..f44207bd9 100644 --- a/packages/prop-house-webapp/src/components/VotingStrategiesDisplay/index.tsx +++ b/packages/prop-house-webapp/src/components/VotingStrategiesDisplay/index.tsx @@ -81,6 +81,17 @@ const VotingStrategiesDisplay: React.FC<{ ); + if (stratType === GovPowerStrategyType.CHECKPOINTABLE_ERC721) + copy = ( + <> + Owners or delegates of the{' '} + + {display(strat.tokenAddress)} + {' '} + token can vote. {strat.multiplier ? strat.multiplier : 1} vote per token. + + ); + if (stratType === GovPowerStrategyType.UNKNOWN) copy = <>Error reading voting strategy; return formattedContent(copy); diff --git a/packages/prop-house-webapp/src/hooks/useTokenNames.ts b/packages/prop-house-webapp/src/hooks/useTokenNames.ts index e74be1bd7..1e1cf000c 100644 --- a/packages/prop-house-webapp/src/hooks/useTokenNames.ts +++ b/packages/prop-house-webapp/src/hooks/useTokenNames.ts @@ -27,7 +27,8 @@ const useTokenNames = (strategies: ParsedGovPowerStrategy[]): UseTokenNamesResul const contracts = strategies .map(strategy => { const isErc20OrErc721 = strategy.strategyType === GovPowerStrategyType.BALANCE_OF || - strategy.strategyType === GovPowerStrategyType.BALANCE_OF_ERC20; + strategy.strategyType === GovPowerStrategyType.BALANCE_OF_ERC20 || + strategy.strategyType === GovPowerStrategyType.CHECKPOINTABLE_ERC721; if (isErc20OrErc721) return { diff --git a/packages/prop-house-webapp/src/utils/createVoterStrategy.ts b/packages/prop-house-webapp/src/utils/createVoterStrategy.ts index ec76a03b1..8224719f7 100644 --- a/packages/prop-house-webapp/src/utils/createVoterStrategy.ts +++ b/packages/prop-house-webapp/src/utils/createVoterStrategy.ts @@ -6,10 +6,22 @@ import { } from '@prophouse/sdk-react'; import { NewVoter } from '../components/HouseManager/VotersConfig'; +/** + * Known ERC721 checkpointable tokens. + */ +const ERC721_CHECKPOINTABLE_TOKENS = ['0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03' /* Nouns */]; + const createVoterStrategy = (voter: NewVoter): GovPowerStrategyConfig | null => { let s: GovPowerStrategyConfig | null = null; - if (voter.type === VotingStrategyType.BALANCE_OF_ERC1155) { + if (voter.type === VotingStrategyType.CHECKPOINTABLE_ERC721 || ERC721_CHECKPOINTABLE_TOKENS.includes(voter.address?.toLowerCase())) { + s = { + strategyType: VotingStrategyType.CHECKPOINTABLE_ERC721, + assetType: AssetType.ERC721, + address: voter.address, + multiplier: voter.multiplier, + }; + } else if (voter.type === VotingStrategyType.BALANCE_OF_ERC1155) { s = { strategyType: voter.type, assetType: AssetType.ERC1155, diff --git a/yarn.lock b/yarn.lock index e0e724e92..b21797f1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5978,17 +5978,24 @@ dependencies: ethers "~5.7.2" -"@prophouse/sdk-react@1.0.15": - version "1.0.15" - resolved "https://registry.npmjs.org/@prophouse/sdk-react/-/sdk-react-1.0.15.tgz#a9df1ee3363d0c7f0fe9bcba04a2103f090acf7d" - integrity sha512-CJakH5gWbyi3GVw2sZ29BnHqmow36/urenjKmrwXohTML0EAtYDy8No+fClMRL32UHWvDxMMYBqNR1yU3gEYXg== +"@prophouse/protocol@1.0.8": + version "1.0.8" + resolved "https://registry.npmjs.org/@prophouse/protocol/-/protocol-1.0.8.tgz#745869a4932df727221514ece06563ed7a93a011" + integrity sha512-XSYA5iqrbD2pyZFD3bSoJKFOeQs/9LgElddZ1cOj6UzQhe2xA0e5ZW5aTwGqEW4eIq8Ed6Am8ruNF8SwyAcphA== + dependencies: + ethers "~5.7.2" + +"@prophouse/sdk-react@1.0.17": + version "1.0.17" + resolved "https://registry.npmjs.org/@prophouse/sdk-react/-/sdk-react-1.0.17.tgz#5d4b31962156d243197c7144dcf5db5ffee81381" + integrity sha512-96udnh5fIefG7+eYG/n+2WevYYogcQyE4fxYCNxBGy9zyhjUVSuieMWegwzYzRTI8GWPxHCUxFyXpaWXghCaxQ== dependencies: - "@prophouse/sdk" "1.0.21" + "@prophouse/sdk" "1.0.23" -"@prophouse/sdk@1.0.21": - version "1.0.21" - resolved "https://registry.npmjs.org/@prophouse/sdk/-/sdk-1.0.21.tgz#c83c532d77d2a696c5795be3ff8880bbb53a4c82" - integrity sha512-ADUwQir+5QezJ/rTTcwTXCy8Ge3SAalV9dAsFOrGhenuW6E7ROUTx4PvC8BXsXdPiY5LgOivLPBYr29KbRQmdw== +"@prophouse/sdk@1.0.23": + version "1.0.23" + resolved "https://registry.npmjs.org/@prophouse/sdk/-/sdk-1.0.23.tgz#c24716ec419d5ff3acce985c148bfbe6e381c924" + integrity sha512-N/tlB+BHeUmycroErHpu8ZoP+GXFvXgc2Uowbd0HkX+JIqU5GfOP2up4dCJ+3fz6I5rPdqUA5i+G66e2+q73kA== dependencies: "@ethersproject/abi" "~5.7.0" "@ethersproject/abstract-provider" "~5.7.0" @@ -6004,7 +6011,7 @@ "@ethersproject/strings" "~5.7.0" "@ethersproject/wallet" "^5.7.0" "@pinata/sdk" "^2.1.0" - "@prophouse/protocol" "1.0.6" + "@prophouse/protocol" "1.0.8" bn.js "^5.2.1" ethereumjs-fork-block "^4.2.4" ethereumjs-fork-common "^3.1.3"