Skip to content

Commit

Permalink
feat: support delegation with EvmSlotValueVotingStrategy (#605)
Browse files Browse the repository at this point in the history
* feat: get voting power from latest checkpoint

* fix remove slot zero check from single slot proof module

* fix: add zero check in the voting strategy only for the checkpoint slot

* chore: updated demo scripts

* refactor: re-added separate EVMSlotValueVotingStrategy and documented separately

* chore: formatting

* chore: fixed comment in ts script

* refactor: remove duplicate module state initialization

* chore: more comments explaining flow

---------

Co-authored-by: Orlando <[email protected]>
  • Loading branch information
Orland0x and Orlando authored Jan 17, 2024
1 parent 05da4ba commit 77d856b
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 139 deletions.
73 changes: 22 additions & 51 deletions scripts/deploy-space.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dotenv from 'dotenv';
import fs from 'fs';
import {
defaultProvider,
Expand All @@ -12,6 +13,8 @@ import {
cairo,
} from 'starknet';

dotenv.config();

const accountAddress = process.env.ADDRESS || '';
const accountPk = process.env.PK || '';
const starknetNetworkUrl = process.env.STARKNET_NETWORK_URL || '';
Expand All @@ -20,68 +23,39 @@ async function main() {
const provider = new RpcProvider({ nodeUrl: starknetNetworkUrl });
const account = new Account(provider, accountAddress, accountPk);

const l1TokenAddress = '0xd96844c9B21CB6cCf2c236257c7fc703E43BA071'; //OZ token 18 decimals
const slotIndex = cairo.uint256(0);
// OZ Votes token 18 decimals
const l1TokenAddress = '0xd96844c9B21CB6cCf2c236257c7fc703E43BA071';

// Slot index of the checkpoints mapping in the token contract,
// obtained using Foundry's Cast Storage Layout tool.
const slotIndex = cairo.uint256(8);

const factsRegistryAddress = '0x01b2111317EB693c3EE46633edd45A4876db14A3a53ACDBf4E5166976d8e869d';
const timestampsRemapperAddress =
'0x2ee57d848297bc7dfc8675111b9aa3bd3085e4038e475250770afe303b772af';

const evmSlotValueVotingStrategySierra = json.parse(
fs
.readFileSync('starknet/target/dev/sx_EvmSlotValueVotingStrategy.sierra.json')
.readFileSync('starknet/target/dev/sx_OZVotesStorageProofVotingStrategy.sierra.json')
.toString('ascii'),
);
const evmSlotValueVotingStrategyCasm = json.parse(
fs
.readFileSync('starknet/target/dev/sx_EvmSlotValueVotingStrategy.casm.json')
.toString('ascii'),
);
const vanillaAuthenticatorSierra = json.parse(
fs.readFileSync('starknet/target/dev/sx_VanillaAuthenticator.sierra.json').toString('ascii'),
);
const vanillaAuthenticatorCasm = json.parse(
fs.readFileSync('starknet/target/dev/sx_VanillaAuthenticator.casm.json').toString('ascii'),
);
const vanillaProposalValidationStrategySierra = json.parse(
fs
.readFileSync('starknet/target/dev/sx_VanillaProposalValidationStrategy.sierra.json')
.toString('ascii'),
);
const vanillaProposalValidationStrategyCasm = json.parse(
fs
.readFileSync('starknet/target/dev/sx_VanillaProposalValidationStrategy.casm.json')
.readFileSync('starknet/target/dev/sx_OZVotesStorageProofVotingStrategy.casm.json')
.toString('ascii'),
);

const spaceSierra = json.parse(
fs.readFileSync('starknet/target/dev/sx_Space.sierra.json').toString('ascii'),
);
const spaceCasm = json.parse(
fs.readFileSync('starknet/target/dev/sx_Space.casm.json').toString('ascii'),
);

// const vanillaAuthenticatorDeployResponse = await account.declareAndDeploy({
// contract: vanillaAuthenticatorSierra,
// casm: vanillaAuthenticatorCasm,
// constructorCalldata: CallData.compile({}),
// });
const vanillaAuthenticatorAddress =
'0x6fa12cffc11ba775ccf99bad7249f06ec5fc605d002716b2f5c7f5561d28081'; //vanillaAuthenticatorDeployResponse.deploy.contract_address;
console.log('Vanilla Authenticator Address: ', vanillaAuthenticatorAddress);
'0x046ad946f22ac4e14e271f24309f14ac36f0fde92c6831a605813fefa46e0893';

// const vanillaProposalValidationStrategyDeployResponse = await account.declareAndDeploy({
// contract: vanillaProposalValidationStrategySierra,
// casm: vanillaProposalValidationStrategyCasm,
// constructorCalldata: CallData.compile({}),
// });
const vanillaProposalValidationStrategyAddress =
'0x18f74b960aeea1b8b8c14eb1834f37fd6e52daed66e983e7364d1f69dc7dbfb';
// vanillaProposalValidationStrategyDeployResponse.deploy.contract_address;
console.log(
'Vanilla Proposal Validation Strategy Address: ',
vanillaProposalValidationStrategyAddress,
);
'0x2247f5d86a60833da9dd8224d8f35c60bde7f4ca3b2a6583d4918d48750f69';

// const deployResponse = await account.declareAndDeploy({
// contract: evmSlotValueVotingStrategySierra,
Expand All @@ -91,22 +65,19 @@ async function main() {
// facts_registry: factsRegistryAddress,
// }),
// });
// const evmSlotValueVotingStrategyAddress =
// '0x07e95f740a049896784969d61389f119291a2de37186f7cfa8ba9d2f3037b32a'; //deployResponse.deploy.contract_address;
// const evmSlotValueVotingStrategyAddress = deployResponse.deploy.contract_address;

const evmSlotValueVotingStrategyAddress =
'0x06cf32ad42d1c6ee98758b00c6a7c7f293d9efb30f2afea370019a88f8e252be';
'0x474edaba6e88a1478d0680bb97f43f01e6a311593ddc496da58d5a7e7a647cf';
console.log('Voting Strategy Address: ', evmSlotValueVotingStrategyAddress);

// const spaceDeployResponse = await account.declareAndDeploy({
// contract: spaceSierra,
// casm: spaceCasm,
// constructorCalldata: CallData.compile({}),
// });
// const spaceAddress = '0x02b9ac7cb47a57ca4144fd0da74203bc8c4aaf411f438b08770bac3680a066cb'; //spaceDeployResponse.deploy.contract_address;
// console.log('Space Address: ', spaceAddress);

const spaceAddress = '0x040e53631973b92651746b4905655b0d797323fd2f47eb80cf6fad521a5ac87d';
const spaceDeployResponse = await account.declareAndDeploy({
contract: spaceSierra,
casm: spaceCasm,
constructorCalldata: CallData.compile({}),
});
const spaceAddress = spaceDeployResponse.deploy.contract_address;
console.log('Space Address: ', spaceAddress);

// initialize space
const result = await account.execute({
Expand Down
85 changes: 59 additions & 26 deletions scripts/herodotus-interaction-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
CairoOptionVariant,
} from 'starknet';
import { utils } from '@snapshot-labs/sx';
import { check } from 'prettier';

dotenv.config();

Expand All @@ -30,15 +31,19 @@ async function main() {
const provider = new RpcProvider({ nodeUrl: starknetNetworkUrl });
const account = new Account(provider, accountAddress, accountPk);

const spaceAddress = '0x040e53631973b92651746b4905655b0d797323fd2f47eb80cf6fad521a5ac87d';
const spaceAddress = '0x2f998d51f78d2b23fea4e8af8306d67095fafaa2a6f76e7e328db6ba3e87bcd';
const vanillaAuthenticatorAddress =
'0x6fa12cffc11ba775ccf99bad7249f06ec5fc605d002716b2f5c7f5561d28081';
'0x046ad946f22ac4e14e271f24309f14ac36f0fde92c6831a605813fefa46e0893';
const evmSlotValueVotingStrategyAddress =
'0x06cf32ad42d1c6ee98758b00c6a7c7f293d9efb30f2afea370019a88f8e252be';
'0x474edaba6e88a1478d0680bb97f43f01e6a311593ddc496da58d5a7e7a647cf';

const l1TokenAddress = '0xd96844c9B21CB6cCf2c236257c7fc703E43BA071'; //OZ token 18 decimals
const slotIndex = 0; // Slot index of the balances mapping in the token contract
const voterAddress = '0x2842c82E20ab600F443646e1BC8550B44a513D82';
// OZ Votes token 18 decimals
const l1TokenAddress = '0xd96844c9B21CB6cCf2c236257c7fc703E43BA071';
// Slot index of the checkpoints mapping in the token contract,
//obtained using Foundry's Cast Storage Layout tool.
const slotIndex = 8;

const voterAddress = '0x1fb824f4a6f82de72ae015931e5cf6923f9acb0f';

const { abi: spaceAbi } = await provider.getClassAt(spaceAddress);
const space = new Contract(spaceAbi, spaceAddress, provider);
Expand All @@ -51,9 +56,27 @@ async function main() {
);
vanillaAuthenticator.connect(account);

const slotKey = ethers.utils.keccak256(
`0x${voterAddress.slice(2).padStart(64, '0')}${slotIndex.toString(16).padStart(64, '0')}`,
const l1Token = new ethers.Contract(
l1TokenAddress,
['function numCheckpoints(address account) public view returns (uint256)'],
new ethers.JsonRpcProvider(ethNetworkUrl),
);
const numCheckpoints = await l1Token.numCheckpoints(voterAddress);
console.log(numCheckpoints);

// Deriving the keys of the final slot in the checkpoints array for the voter and the next empty slot
const checkpointSlotKey =
BigInt(
ethers.keccak256(
ethers.keccak256(
`0x${voterAddress.slice(2).padStart(64, '0')}${slotIndex.toString(16).padStart(64, '0')}`,
),
),
) +
BigInt(numCheckpoints) -
BigInt(1);
const nextEmptySlotKey = checkpointSlotKey + BigInt(1);

let response;

// Create a proposal
Expand Down Expand Up @@ -142,8 +165,9 @@ async function main() {

// This is the snapshot L1 block number
const l1BlockNumber = response.data.path[1].blockNumber;
console.log(l1BlockNumber);

// cache block number in single slot proof voting strategy
// cache block number in voting strategy
await account.execute({
contractAddress: evmSlotValueVotingStrategyAddress,
entrypoint: 'cache_timestamp',
Expand All @@ -165,7 +189,7 @@ async function main() {
}),
});

// Query the node for the storage proof of the desired slot at the snapshot L1 block number
// Query the node for the storage proofs of the 2 slots at the snapshot block number
response = await axios({
method: 'post',
url: ethNetworkUrl,
Expand All @@ -177,24 +201,31 @@ async function main() {
id: 1,
jsonrpc: '2.0',
method: 'eth_getProof',
params: [l1TokenAddress, [slotKey], `0x${l1BlockNumber.toString(16)}`],
params: [
l1TokenAddress,
[`0x${checkpointSlotKey.toString(16)}`, `0x${nextEmptySlotKey.toString(16)}`],
`0x${l1BlockNumber.toString(16)}`,
],
},
});

// This takes the proof from the response and converts it to a list of 64 bit little endian words
const storageProofLittleEndianWords64 = response.data.result.storageProof[0].proof.map(
(node: string) =>
node
.slice(2)
.match(/.{1,16}/g)
?.map(
(word: string) =>
`0x${word
.replace(/^(.(..)*)$/, '0$1')
.match(/../g)
?.reverse()
.join('')}`,
),
// This takes the proofs from the response and converts them to a list of 64 bit little endian words
const storageProofsLittleEndianWords64 = response.data.result.storageProof.map(
(proofWrapper: any) =>
proofWrapper.proof.map(
(node: string) =>
node
.slice(2)
.match(/.{1,16}/g)
?.map(
(word: string) =>
`0x${word
.replace(/^(.(..)*)$/, '0$1')
.match(/../g)
?.reverse()
.join('')}`,
),
),
);

// Cast Vote
Expand All @@ -212,7 +243,9 @@ async function main() {
{
index: '0x0',
params: CallData.compile({
storageProof: storageProofLittleEndianWords64,
checkpoint_index: numCheckpoints - BigInt(1),
checkpoint_mpt_proof: storageProofsLittleEndianWords64[0],
exclusion_mpt_proof: storageProofsLittleEndianWords64[1],
}),
},
],
Expand Down
3 changes: 3 additions & 0 deletions starknet/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ mod voting_strategies {
mod evm_slot_value;
use evm_slot_value::EvmSlotValueVotingStrategy;

mod oz_votes_storage_proof;
use oz_votes_storage_proof::OZVotesStorageProofVotingStrategy;

mod merkle_whitelist;
use merkle_whitelist::MerkleWhitelistVotingStrategy;
}
Expand Down
53 changes: 2 additions & 51 deletions starknet/src/utils/single_slot_proof.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ mod SingleSlotProof {
ITimestampRemappersDispatcherTrait, IEVMFactsRegistryDispatcher,
IEVMFactsRegistryDispatcherTrait
};
use sx::utils::endian::ByteReverse;

#[storage]
struct Storage {
Expand All @@ -30,35 +29,22 @@ mod SingleSlotProof {
self: @ContractState,
timestamp: u32,
l1_contract_address: EthAddress,
slot_index: u256,
mapping_key: u256,
params: Span<felt252>
slot_key: u256,
mpt_proof: Span<Words64>
) -> u256 {
// Checks if the timestamp is already cached.
let l1_block_number = self._cached_remapped_timestamps.read(timestamp);
assert(l1_block_number.is_non_zero(), 'Timestamp not cached');

let mut params = params;
let mpt_proof = Serde::<Span<Words64>>::deserialize(ref params).unwrap();

// Computes the key of the EVM storage slot from the mapping key and the index of the mapping in storage.
let slot_key = InternalImpl::get_mapping_slot_key(mapping_key, slot_index);

// Returns the value of the storage slot of account: `l1_contract_address` at key: `slot_key` and block number: `l1_block_number`.
let slot_value = IEVMFactsRegistryDispatcher {
contract_address: self._facts_registry.read()
}
.get_storage(l1_block_number, l1_contract_address.into(), slot_key, mpt_proof);

assert(slot_value.is_non_zero(), 'Slot is zero');

slot_value
}

fn get_mapping_slot_key(mapping_key: u256, slot_index: u256) -> u256 {
keccak::keccak_u256s_be_inputs(array![mapping_key, slot_index].span()).byte_reverse()
}

fn cache_timestamp(ref self: ContractState, timestamp: u32, tree: BinarySearchTree) {
// Maps timestamp to closest L1 block number that occured before the timestamp. If the queried
// timestamp is less than the earliest timestamp or larger than the latest timestamp in the mapper
Expand All @@ -80,38 +66,3 @@ mod SingleSlotProof {
}
}
}

#[cfg(test)]
mod tests {
use super::SingleSlotProof;

#[test]
#[available_gas(10000000)]
fn get_mapping_slot_key() {
assert(
SingleSlotProof::InternalImpl::get_mapping_slot_key(
0x0_u256, 0x0_u256
) == u256 {
low: 0x2b36e491b30a40b2405849e597ba5fb5, high: 0xad3228b676f7d3cd4284a5443f17f196
},
'Incorrect slot key'
);
assert(
SingleSlotProof::InternalImpl::get_mapping_slot_key(
0x1_u256, 0x0_u256
) == u256 {
low: 0x10426056ef8ca54750cb9bb552a59e7d, high: 0xada5013122d395ba3c54772283fb069b
},
'Incorrect slot key'
);
assert(
SingleSlotProof::InternalImpl::get_mapping_slot_key(
0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045_u256, 0x1_u256
) == u256 {
low: 0xad9172e102b3af1e07a10cc29003beb2, high: 0xb931be0b3d1fb06daf0d92e2b8dfe49e
},
'Incorrect slot key'
);
}
}

Loading

0 comments on commit 77d856b

Please sign in to comment.