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

Nouns Delegation Strategy & ERC1155 Strategy Bug Fix [Production Release] #759

Merged
merged 5 commits into from
Jan 15, 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
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
Loading