Skip to content

Commit

Permalink
Merge pull request #759 from Prop-House/master
Browse files Browse the repository at this point in the history
Nouns Delegation Strategy & ERC1155 Strategy Bug Fix [Production Release]
  • Loading branch information
solimander committed Jan 15, 2024
2 parents 7bef16d + 915d1a6 commit f084ede
Show file tree
Hide file tree
Showing 29 changed files with 367 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ContractState> {
/// 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<felt252>, mut user_params: Span<felt252>,
) -> 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<felt252>, Span<felt252>)>::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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/prop-house-protocol/deployments/goerli.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"ethBalanceOfGovPowerStrategy": "0x4ff2eee9b0d91eda2ae6dd620d34f5c6ddf11990d948088753acb2a3cca2f93",
"ethBalanceOfErc20GovPowerStrategy": "0x000000000000000000000000000000000000000000000000000000000000000",
"ethBalanceOfErc1155GovPowerStrategy": "0x3016c4e41fe92c2ce5cc1a4686c7c22ba5d810e7686546c8a44e8c78a4ba154",
"ethCheckpointableErc721GovPowerStrategy": "0x000000000000000000000000000000000000000000000000000000000000000",
"herodotus": {
"factRegistry": "0x5e6c5b45485f2eb7609a27e413aad727536b3590a64e18ceb5950e30852288f",
"l1HeadersStore": "0x1d9b36a00d7d5300e5da456c56d09c46dfefbc91b3a6b1552b6f2a34d6e34c4"
Expand Down
3 changes: 2 additions & 1 deletion packages/prop-house-protocol/deployments/mainnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"allowlistGovPowerStrategy": "0x3daa40ef909961a576f9ba58eb063d5ebc85411063a8b29435f05af6167079c",
"ethBalanceOfGovPowerStrategy": "0x6ddcc94a4225843546a9b118a2733fd924d6b8a6467279cbe6a1aea79daca54",
"ethBalanceOfErc20GovPowerStrategy": "0x196cf5ceba8e98abe1e633d6184cd28c1e1ebd09ea71f89867dd4c5fda97bbe",
"ethBalanceOfErc1155GovPowerStrategy": "0x6d22f17522d6992eb479deb850e96f9454fc2f6c127993ab2ef9d411f467e8",
"ethBalanceOfErc1155GovPowerStrategy": "0x44e3bdffcb6ce36596d0faa4316932c5dc47005b9eaaf0f7ce0f455c98b2e75",
"ethCheckpointableErc721GovPowerStrategy": "0x10f7529ec5df9069a06191deb7cd2c4158c2b59879e2544cb45dc221daff429",
"herodotus": {
"factRegistry": "0x002081b2d3de51f295e7516257f68bd9f06acbc7f19ba49410c100afbe57540f",
"l1HeadersStore": "0x008caacc818a97ef9122aa67b3b0e14d10e2959b262e7e785f47e20a36ef0ce0"
Expand Down
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.7",
"version": "1.0.9",
"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 @@ -22,6 +22,7 @@ export interface GovPowerStrategies {
balanceOf: string;
balanceOfErc20: string;
balanceOfErc1155: string;
checkpointableErc721: string;
vanilla: string;
}

Expand Down Expand Up @@ -89,6 +90,7 @@ export const contracts: Record<number, ContractAddresses> = {
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: {
Expand Down Expand Up @@ -132,6 +134,7 @@ export const contracts: Record<number, ContractAddresses> = {
balanceOf: mainnet.starknet.address.ethBalanceOfGovPowerStrategy,
balanceOfErc20: mainnet.starknet.address.ethBalanceOfErc20GovPowerStrategy,
balanceOfErc1155: mainnet.starknet.address.ethBalanceOfErc1155GovPowerStrategy,
checkpointableErc721: mainnet.starknet.address.ethCheckpointableErc721GovPowerStrategy,
vanilla: constants.HashZero,
},
auth: {
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.16",
"version": "1.0.18",
"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.21"
"@prophouse/sdk": "1.0.23"
},
"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.22",
"version": "1.0.24",
"description": "Useful tools for interacting with the Prop House protocol",
"author": "solimander",
"homepage": "https://prop.house",
Expand Down Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions packages/prop-house-sdk/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class BalanceOfERC1155Handler extends SingleSlotProofHandler<BalanceOfERC
const strategy = await this.getStrategyAddressAndParams(strategyId);
const [contractAddress, slotIndex, tokenId] = strategy.params;

const slotKey = encoding.getNestedSlotKey([account, tokenId], slotIndex);
const slotKey = encoding.getNestedSlotKey([tokenId, account], slotIndex);
const slotKeyU256 = splitUint256.SplitUint256.fromHex(slotKey);

const block = await this.getBlockNumberForTimestamp(timestamp);
Expand All @@ -98,7 +98,7 @@ export class BalanceOfERC1155Handler extends SingleSlotProofHandler<BalanceOfERC
const strategy = await this.getStrategyAddressAndParams(strategyId);
const [contractAddress, slotIndex, tokenId] = strategy.params;

const slotKey = encoding.getNestedSlotKey([account, tokenId], slotIndex);
const slotKey = encoding.getNestedSlotKey([tokenId, account], slotIndex);

const block = await this.getBlockNumberForTimestamp(timestamp);
const storageHash = await this.getStorageHash(contractAddress, block);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { ChainConfig, GovPowerStrategyType, GovPowerConfig, AccountField, CheckpointableERC721Config } from '../../types';
import { SingleSlotProofHandler } from './base';
import { encoding, splitUint256, storageProofs } from '../../utils';
import { BigNumber } from '@ethersproject/bignumber';
import { Contract } from '@ethersproject/contracts';
import { GET_CURRENT_VOTES_FUNC, NUM_CHECKPOINTS_FUNC } from '../../constants';
import { Call } from 'starknet';

export class CheckpointableERC721Handler extends SingleSlotProofHandler<CheckpointableERC721Config> {
/**
* 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<string[]> {
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<Call[]> {
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<BigNumber> {
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);
}
}
Loading

0 comments on commit f084ede

Please sign in to comment.