From 77d856ba573fae0cf3f2d70db57490c287126dda Mon Sep 17 00:00:00 2001 From: Orland0x <37511817+Orland0x@users.noreply.github.com> Date: Wed, 17 Jan 2024 07:27:24 -0500 Subject: [PATCH] feat: support delegation with EvmSlotValueVotingStrategy (#605) * 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 --- scripts/deploy-space.ts | 73 ++---- scripts/herodotus-interaction-demo.ts | 85 ++++--- starknet/src/lib.cairo | 3 + starknet/src/utils/single_slot_proof.cairo | 53 +---- .../voting_strategies/evm_slot_value.cairo | 67 +++++- .../oz_votes_storage_proof.cairo | 211 ++++++++++++++++++ 6 files changed, 353 insertions(+), 139 deletions(-) create mode 100644 starknet/src/voting_strategies/oz_votes_storage_proof.cairo diff --git a/scripts/deploy-space.ts b/scripts/deploy-space.ts index 7732d223..6fc411fc 100644 --- a/scripts/deploy-space.ts +++ b/scripts/deploy-space.ts @@ -1,3 +1,4 @@ +import dotenv from 'dotenv'; import fs from 'fs'; import { defaultProvider, @@ -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 || ''; @@ -20,40 +23,27 @@ 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'), ); @@ -61,27 +51,11 @@ async function main() { 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, @@ -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({ diff --git a/scripts/herodotus-interaction-demo.ts b/scripts/herodotus-interaction-demo.ts index 9915a64c..88093bbf 100644 --- a/scripts/herodotus-interaction-demo.ts +++ b/scripts/herodotus-interaction-demo.ts @@ -11,6 +11,7 @@ import { CairoOptionVariant, } from 'starknet'; import { utils } from '@snapshot-labs/sx'; +import { check } from 'prettier'; dotenv.config(); @@ -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); @@ -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 @@ -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', @@ -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, @@ -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 @@ -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], }), }, ], diff --git a/starknet/src/lib.cairo b/starknet/src/lib.cairo index 3baac963..bb65523c 100644 --- a/starknet/src/lib.cairo +++ b/starknet/src/lib.cairo @@ -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; } diff --git a/starknet/src/utils/single_slot_proof.cairo b/starknet/src/utils/single_slot_proof.cairo index a78ccc88..3eee718b 100644 --- a/starknet/src/utils/single_slot_proof.cairo +++ b/starknet/src/utils/single_slot_proof.cairo @@ -6,7 +6,6 @@ mod SingleSlotProof { ITimestampRemappersDispatcherTrait, IEVMFactsRegistryDispatcher, IEVMFactsRegistryDispatcherTrait }; - use sx::utils::endian::ByteReverse; #[storage] struct Storage { @@ -30,35 +29,22 @@ mod SingleSlotProof { self: @ContractState, timestamp: u32, l1_contract_address: EthAddress, - slot_index: u256, - mapping_key: u256, - params: Span + slot_key: u256, + mpt_proof: Span ) -> 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::>::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 @@ -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' - ); - } -} - diff --git a/starknet/src/voting_strategies/evm_slot_value.cairo b/starknet/src/voting_strategies/evm_slot_value.cairo index 3e8efacd..ad15fb4c 100644 --- a/starknet/src/voting_strategies/evm_slot_value.cairo +++ b/starknet/src/voting_strategies/evm_slot_value.cairo @@ -5,15 +5,14 @@ mod EvmSlotValueVotingStrategy { use sx::types::{UserAddress, UserAddressTrait}; use sx::interfaces::IVotingStrategy; use sx::utils::{SingleSlotProof, TIntoU256}; + use sx::utils::endian::ByteReverse; #[storage] struct Storage {} #[external(v0)] impl EvmSlotValueVotingStrategy of IVotingStrategy { - /// Returns the EVM slot value of contract `C` at slot index `I`, using `voter` as the mapping key. - /// The contract address and slot index is stored in the strategy parameters (defined by the space owner). - /// The proof itself is supplied by the voter, in the `user_params` argument. + /// Returns the value of a slot in a mapping in an EVM contract at the block number corresponding to the given timestamp. /// /// # Notes /// @@ -33,28 +32,33 @@ mod EvmSlotValueVotingStrategy { self: @ContractState, timestamp: u32, voter: UserAddress, - params: Span, // [contract_address: address, slot_index: u256] - user_params: Span, // encoded proofs + mut params: Span, // [contract_address: address, slot_index: u256] + mut user_params: Span, // [mpt_proof: u64[][]] ) -> u256 { // Cast voter address to an Ethereum address // Will revert if the address is not a valid Ethereum address let voter = voter.to_ethereum_address(); - // Decode params - let mut params = params; + // Decode params and user_params let (evm_contract_address, slot_index) = Serde::<( EthAddress, u256 )>::deserialize(ref params) .unwrap(); + let mpt_proof = Serde::>>::deserialize(ref user_params).unwrap(); - // Get the balance of the voter at the given block timestamp + // 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(voter.into(), slot_index); + + // Returns the value of the storage slot at the block number corresponding to the given timestamp. // Migration to components planned ; disregard the `unsafe` keyword, // it is actually safe. let state = SingleSlotProof::unsafe_new_contract_state(); - let balance = SingleSlotProof::InternalImpl::get_storage_slot( - @state, timestamp, evm_contract_address, slot_index, voter.into(), user_params + let slot_value = SingleSlotProof::InternalImpl::get_storage_slot( + @state, timestamp, evm_contract_address, slot_key, mpt_proof ); - balance + assert(slot_value.is_non_zero(), 'Slot is zero'); + + slot_value } } @@ -91,6 +95,13 @@ mod EvmSlotValueVotingStrategy { } } + #[generate_trait] + impl InternalImpl of InternalTrait { + 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() + } + } + #[constructor] fn constructor( ref self: ContractState, @@ -103,3 +114,37 @@ mod EvmSlotValueVotingStrategy { SingleSlotProof::InternalImpl::initializer(ref state, timestamp_remappers, facts_registry); } } + +#[cfg(test)] +mod tests { + use super::EvmSlotValueVotingStrategy; + + #[test] + #[available_gas(10000000)] + fn get_mapping_slot_key() { + assert( + EvmSlotValueVotingStrategy::InternalImpl::get_mapping_slot_key( + 0x0_u256, 0x0_u256 + ) == u256 { + low: 0x2b36e491b30a40b2405849e597ba5fb5, high: 0xad3228b676f7d3cd4284a5443f17f196 + }, + 'Incorrect slot key' + ); + assert( + EvmSlotValueVotingStrategy::InternalImpl::get_mapping_slot_key( + 0x1_u256, 0x0_u256 + ) == u256 { + low: 0x10426056ef8ca54750cb9bb552a59e7d, high: 0xada5013122d395ba3c54772283fb069b + }, + 'Incorrect slot key' + ); + assert( + EvmSlotValueVotingStrategy::InternalImpl::get_mapping_slot_key( + 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045_u256, 0x1_u256 + ) == u256 { + low: 0xad9172e102b3af1e07a10cc29003beb2, high: 0xb931be0b3d1fb06daf0d92e2b8dfe49e + }, + 'Incorrect slot key' + ); + } +} diff --git a/starknet/src/voting_strategies/oz_votes_storage_proof.cairo b/starknet/src/voting_strategies/oz_votes_storage_proof.cairo new file mode 100644 index 00000000..7c299b85 --- /dev/null +++ b/starknet/src/voting_strategies/oz_votes_storage_proof.cairo @@ -0,0 +1,211 @@ +#[starknet::contract] +mod OZVotesStorageProofVotingStrategy { + use starknet::{EthAddress, ContractAddress}; + use sx::external::herodotus::BinarySearchTree; + use sx::types::{UserAddress, UserAddressTrait}; + use sx::interfaces::IVotingStrategy; + use sx::utils::{SingleSlotProof, TIntoU256}; + use sx::utils::endian::ByteReverse; + + #[storage] + struct Storage {} + + #[external(v0)] + impl OZVotesStorageProofVotingStrategy of IVotingStrategy { + /// Returns the delegated voting power of `voter` at the block number corresponding to `timestamp` for tokens that implement OZVotes. + /// + /// # Arguments + /// + /// * `timestamp` - The timestamp of the block at which the voting power is calculated. This will be mapped to an L1 block number by + /// the Herodotus Timestamp Remapper within the SingleSlotProof module call. + /// * `voter` - The address of the voter. Expected to be an ethereum address. + /// * `params` - Should contain the token contract address and the slot index. + /// * `user_params` - Should contain the index of the final checkpoint in the checkpoints array for `voter` and + /// the encoded storage proofs required prove the corresponding slot and the slot after it. + /// + /// # Returns + /// + /// `u256` - The voting power of `voter` at the L1 block number corresponding to `timestamp`. + fn get_voting_power( + self: @ContractState, + timestamp: u32, + voter: UserAddress, + mut params: Span, // [contract_address: address, slot_index: u256] + mut user_params: Span, // [checkpoint_index: u32, checkpoint_mpt_proof: u64[][], exclusion_mpt_proof: u64[][]] + ) -> u256 { + // Cast voter address to an Ethereum address + // Will revert if the address is not a valid Ethereum address + let voter = voter.to_ethereum_address(); + + // Decode params and user_params + let (evm_contract_address, slot_index) = Serde::<( + EthAddress, u256 + )>::deserialize(ref params) + .unwrap(); + let (checkpoint_index, checkpoint_mpt_proof, exclusion_mpt_proof) = Serde::<( + u32, Span>, Span> + )>::deserialize(ref user_params) + .unwrap(); + + // Get the slot key for the final checkpoint + let slot_key = InternalImpl::final_checkpoint_slot_key( + voter.into(), slot_index, checkpoint_index + ); + + // Get the slot containing the final checkpoint + // Migration to components planned ; disregard the `unsafe` keyword, + // it is actually safe. + let state = SingleSlotProof::unsafe_new_contract_state(); + let checkpoint = SingleSlotProof::InternalImpl::get_storage_slot( + @state, timestamp, evm_contract_address, slot_key, checkpoint_mpt_proof + ); + assert(checkpoint.is_non_zero(), 'Slot is zero'); + + // Verify the checkpoint is indeed the final checkpoint by checking the next slot is zero. + assert( + SingleSlotProof::InternalImpl::get_storage_slot( + @state, timestamp, evm_contract_address, slot_key + 1, exclusion_mpt_proof + ) + .is_zero(), + 'Invalid Checkpoint' + ); + + // Extract voting power from the encoded checkpoint slot. + let (_, vp) = InternalImpl::decode_checkpoint_slot(checkpoint); + + vp + } + } + + #[external(v0)] + #[generate_trait] + impl SingleSlotProofImpl of SingleSlotProofTrait { + /// Queries the Timestamp Remapper contract for the closest L1 block number that occured before + /// the given timestamp and then caches the result. If the queried timestamp is less than the earliest + /// timestamp or larger than the latest timestamp in the mapper then the transaction will revert. + /// This function should be used to cache a remapped timestamp before its used when calling the + /// `get_storage_slot` function with the same timestamp. + /// + /// # Arguments + /// + /// * `timestamp` - The timestamp at which to query. + /// * `tree` - The tree proof required to query the remapper. + fn cache_timestamp(ref self: ContractState, timestamp: u32, tree: BinarySearchTree) { + let mut state = SingleSlotProof::unsafe_new_contract_state(); + SingleSlotProof::InternalImpl::cache_timestamp(ref state, timestamp, tree); + } + + /// View function exposing the cached remapped timestamps. Reverts if the timestamp is not cached. + /// + /// # Arguments + /// + /// * `timestamp` - The timestamp to query. + /// + /// # Returns + /// + /// * `u256` - The cached L1 block number corresponding to the timestamp. + fn cached_timestamps(self: @ContractState, timestamp: u32) -> u256 { + let state = SingleSlotProof::unsafe_new_contract_state(); + SingleSlotProof::InternalImpl::cached_timestamps(@state, timestamp) + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn final_checkpoint_slot_key(mapping_key: u256, slot_index: u256, offset: u32) -> u256 { + // Refer to the Solidity compiler documentation for the derivation of this slot key. + // https://docs.soliditylang.org/en/v0.8.23/internals/layout_in_storage.html#mappings-and-dynamic-arrays + keccak::keccak_u256s_be_inputs( + array![ + keccak::keccak_u256s_be_inputs(array![mapping_key, slot_index].span()) + .byte_reverse() + ] + .span() + ) + .byte_reverse() + + integer::U32IntoU256::into(offset) + } + + fn decode_checkpoint_slot(slot: u256) -> (u32, u256) { + // Checkpoints are represented by the following Solidity struct in the token contract: + // struct Checkpoint { + // uint32 fromBlock; + // uint224 votes; + // } + // This is represented in storage as a single 256 bit slot with the `fromBlock` field + // stored in the lower 32 bits and the `votes` field stored in the upper 224 bits. + let block_number = slot.low & 0xffffffff; + let vp = slot / 0x100000000; + (block_number.try_into().unwrap(), vp) + } + } + + #[constructor] + fn constructor( + ref self: ContractState, + timestamp_remappers: ContractAddress, + facts_registry: ContractAddress + ) { + // Migration to components planned ; disregard the `unsafe` keyword, + // it is actually safe. + let mut state = SingleSlotProof::unsafe_new_contract_state(); + SingleSlotProof::InternalImpl::initializer(ref state, timestamp_remappers, facts_registry); + } +} + +#[cfg(test)] +mod tests { + use super::OZVotesStorageProofVotingStrategy; + + #[test] + #[available_gas(10000000)] + fn get_mapping_slot_key() { + assert( + OZVotesStorageProofVotingStrategy::InternalImpl::final_checkpoint_slot_key( + 0x0_u256, 0x0_u256, 0 + ) == u256 { + low: 0x1e019e72ec816e127a59e7195f2cd7f5, high: 0xf0df3dcda05b4fbd9c655cde3d5ceb21 + }, + 'Incorrect slot key' + ); + assert( + OZVotesStorageProofVotingStrategy::InternalImpl::final_checkpoint_slot_key( + 0x106b1F88867D99840CaaCAC2dA91265BA6E93e2B_u256, 0x8_u256, 0 + ) == u256 { + low: 0xe29cc80a3c50310ba7fddc5044149d44, high: 0x87c554e6c4e8f9242420b8d1db45854c + }, + 'Incorrect slot key' + ); + assert( + OZVotesStorageProofVotingStrategy::InternalImpl::final_checkpoint_slot_key( + 0x106b1F88867D99840CaaCAC2dA91265BA6E93e2B_u256, 0x8_u256, 4 + ) == u256 { + low: 0xe29cc80a3c50310ba7fddc5044149d48, high: 0x87c554e6c4e8f9242420b8d1db45854c + }, + 'Incorrect slot key' + ); + } + + #[test] + #[available_gas(10000000)] + fn decode_checkpoint_slot() { + assert( + OZVotesStorageProofVotingStrategy::InternalImpl::decode_checkpoint_slot( + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff_u256 + ) == (0xffffffff, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff_u256), + 'Incorrect checkpoint slot' + ); + assert( + OZVotesStorageProofVotingStrategy::InternalImpl::decode_checkpoint_slot( + 0x0_u256 + ) == (0, 0), + 'Incorrect checkpoint slot' + ); + assert( + OZVotesStorageProofVotingStrategy::InternalImpl::decode_checkpoint_slot( + 0x000000056bc75e2d631f4240009c3685_u256 + ) == (10237573, 100000000000001000000), + 'Incorrect checkpoint slot' + ); + } +}