diff --git a/pallets/exodus/Cargo.toml b/pallets/exodus/Cargo.toml index fce2d5f..98925d0 100644 --- a/pallets/exodus/Cargo.toml +++ b/pallets/exodus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-exodus" -version = "0.0.4" +version = "0.0.5" description = "Threshold signature generation with DKG included" license.workspace = true authors.workspace = true diff --git a/pallets/exodus/src/benchmarking.rs b/pallets/exodus/src/benchmarking.rs index daea21d..85401cf 100644 --- a/pallets/exodus/src/benchmarking.rs +++ b/pallets/exodus/src/benchmarking.rs @@ -472,6 +472,7 @@ mod benchmarks { #[benchmark] fn register_nonce_commitment() -> Result<(), BenchmarkError> { + let network_id: NetworkIdOf = 69_420u64.unique_saturated_into(); let (mut dkg_state, network_curve, authority, authority_index) = prepare_pallet::(1); @@ -483,9 +484,22 @@ mod benchmarks { dkg_state.insert_index(authority_index); let exodus_session = CurrentExodus::::get(); - let dummy_bytes = EvmBytes32::repeat_byte(69); + let dummy_address = EvmAddress::repeat_byte(69); let dummy_nonce = vec![0u8; network_curve.element_bytes_len()]; - let request = ExodusRequest::evm_rotation(exodus_session, dummy_bytes, 0); + + let create_bond = GovernanceAction::Create { + market: [69, 420, 1337], + terms: [420, 1337], + token: dummy_address, + intervals: [34, 35], + booleans: [true, true], + }; + let request = ExodusRequest::, BalanceOf>::evm_governance( + exodus_session, + network_id, + dummy_address, + create_bond, + ); let authorities = QualificationAuthorities::::get(&network_curve); let active_dkg: ActivatedState = (&dkg_state).into(); @@ -534,6 +548,7 @@ mod benchmarks { #[benchmark] fn register_group_commitment() -> Result<(), BenchmarkError> { + let network_id: NetworkIdOf = 69_420u64.unique_saturated_into(); let (mut dkg_state, network_curve, authority, authority_index) = prepare_pallet::(1); @@ -546,9 +561,23 @@ mod benchmarks { let exodus_session = CurrentExodus::::get(); let dummy_bytes = EvmBytes32::repeat_byte(69); + let dummy_address = EvmAddress::repeat_byte(69); let dummy_merkle_root = ExodusHash::repeat_byte(69); let dummy_group_commitment = vec![0u8; network_curve.element_bytes_len()]; - let request = ExodusRequest::evm_rotation(exodus_session, dummy_bytes, 0); + + let create_bond = GovernanceAction::Create { + market: [69, 420, 1337], + terms: [420, 1337], + token: dummy_address, + intervals: [34, 35], + booleans: [true, true], + }; + let request = ExodusRequest::, BalanceOf>::evm_governance( + exodus_session, + network_id, + dummy_address, + create_bond, + ); let authorities = QualificationAuthorities::::get(&network_curve); let active_dkg: ActivatedState = (&dkg_state).into(); @@ -609,6 +638,7 @@ mod benchmarks { fn register_signature_share() -> Result<(), BenchmarkError> { let max_signers = T::MaxAuthorities::get(); let threshold = get_byzantium_threshold(max_signers); + let network_id: NetworkIdOf = 69_420u64.unique_saturated_into(); let (mut dkg_state, network_curve, authority, authority_index) = prepare_pallet::(max_signers as usize); @@ -632,8 +662,20 @@ mod benchmarks { ]; dummy_element[1..33].copy_from_slice(&g_x_coords); - let dummy_bytes = EvmBytes32::repeat_byte(69); - let request = ExodusRequest::evm_rotation(exodus_session, dummy_bytes, 0); + let dummy_address = EvmAddress::repeat_byte(69); + let create_bond = GovernanceAction::Create { + market: [69, 420, 1337], + terms: [420, 1337], + token: dummy_address, + intervals: [34, 35], + booleans: [true, true], + }; + let request = ExodusRequest::, BalanceOf>::evm_governance( + exodus_session, + network_id, + dummy_address, + create_bond, + ); let mut dummy_scalar = vec![0u8; network_curve.scalar_bytes_len()]; dummy_scalar[network_curve.scalar_bytes_len() - 1] = 1; @@ -755,7 +797,7 @@ mod benchmarks { assert!(!committers_exists); assert!(!ExodusRequests::::contains_key(&network_curve, &exodus_session)); - assert!(ExodusSignedRotations::::contains_key((&exodus_session, NetworkType::Evm), dkg_index)); + assert!(ExodusSignedGovernance::::contains_key((&exodus_session, network_id), dkg_index)); Ok(()) } diff --git a/pallets/exodus/src/lib.rs b/pallets/exodus/src/lib.rs index 6ac9705..ed20965 100644 --- a/pallets/exodus/src/lib.rs +++ b/pallets/exodus/src/lib.rs @@ -108,7 +108,7 @@ const EXODUS_PREFIX_SHARES: &[u8] = b"exodus-shares"; const EXODUS_PREFIX_BLENDS: &[u8] = b"exodus-blends"; const EXODUS_PREFIX_EXILES: &[u8] = b"exodus-exiles"; -const MAX_MESSAGE_SIZE : u32 = 420; +const MAX_MESSAGE_SIZE : u32 = 512; const HEADER_MAX_BYTES : u32 = 1 + 4; const ELEMENT_MAX_BYTES : u32 = 33; const SCALAR_MAX_BYTES : u32 = 32; diff --git a/pallets/exodus/src/tests.rs b/pallets/exodus/src/tests.rs index 9a4f733..a3ad699 100644 --- a/pallets/exodus/src/tests.rs +++ b/pallets/exodus/src/tests.rs @@ -1132,8 +1132,13 @@ fn test_all_validators_can_execute_dkg() { ExodusSignedBridges::::iter().count() }); + let governances_count = ext.execute_with(|| { + ExodusSignedGovernance::::iter().count() + }); + assert_eq!(rotations_count, 3); assert_eq!(bridges_count, 2); + assert_eq!(governances_count, 15); } #[test] diff --git a/pallets/exodus/src/types/exodus_request.rs b/pallets/exodus/src/types/exodus_request.rs index 2134e76..3846eca 100644 --- a/pallets/exodus/src/types/exodus_request.rs +++ b/pallets/exodus/src/types/exodus_request.rs @@ -71,10 +71,70 @@ macro_rules! impl_evm_encodable { } )* }; + + (const_array: $( [ $inner_ty:ty ; $len:expr ] ),* $(,)? ) => { + $( + impl EvmAbiEncode for [ $inner_ty ; $len ] { + #[inline(always)] + fn size(&self) -> usize { + $len * 32 + } + + #[inline(always)] + fn abi_encode(&self, buffer: &mut [u8], cursor: &mut usize) -> Option<()> { + for item in self.iter() { + EvmAbiEncode::abi_encode(item, buffer, cursor)?; + } + Some(()) + } + } + )* + }; + + (bytes_slice: ) => { + impl EvmAbiEncode for [u8] { + #[inline(always)] + fn size(&self) -> usize { + let content_len = self.len(); + let remainder = content_len % 32; + let alignment = if remainder == 0 { 0 } else { 32 - remainder }; + 32 + content_len + alignment + } + + #[inline(always)] + fn abi_encode(&self, buffer: &mut [u8], cursor: &mut usize) -> Option<()> { + if self.size() >= MAX_MESSAGE_SIZE as usize { return None; } + + let pointer = cursor.checked_sub(4).map(|x| x.saturating_add(32))?; + let head_pointer = pointer as u32; + EvmAbiEncode::abi_encode(&head_pointer, buffer, cursor); + + let content_len = self.len() as u32; + + EvmAbiEncode::abi_encode(&content_len, buffer, cursor)?; + + let value_slice = buffer.get_mut(*cursor..*cursor + self.len())?; + value_slice.copy_from_slice(self); + *cursor += self.len(); + + let remainder = self.len() % 32; + if remainder != 0 { + let alignment = 32 - remainder; + let alignment_slice = buffer.get_mut(*cursor..*cursor + alignment)?; + alignment_slice.fill(0); + *cursor += alignment; + } + + Some(()) + } + } + }; } impl_evm_encodable!(numeric: u8, u16, u32, u64, u128); impl_evm_encodable!(bytes_fixed: EvmAddress, EvmBytes32); +impl_evm_encodable!(const_array: [u128; 3], [u128; 2], [u32; 2], [u8; 2]); +impl_evm_encodable!(bytes_slice:); macro_rules! abi_encode_evm_message { ($buffer:expr, $sig:expr, [ $( $val:expr ),* $(,)? ]) => {{ @@ -102,6 +162,152 @@ macro_rules! abi_encode_evm_message { }}; } +#[derive(Encode, Decode, Clone, Copy, PartialEq, TypeInfo, RuntimeDebug)] +#[repr(u8)] +pub enum GovernanceStatus { + ReserveDepositor = 0, + ReserveToken = 1, + LiquidityDepositor = 2, + LiquidityToken = 3, + RewardManager = 4, +} + +#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, RuntimeDebug)] +pub enum GovernanceAction { + SetDistributor { distributor: EvmAddress }, + SetWarmupPeriod { warmup_period: u128 }, + UpdateGatekeeperAddress { new_gatekeeper: EvmAddress }, + + SetBounty { bounty: u128 }, + AddPool { address: EvmAddress }, + RemovePool { index: u128 }, + SetAdjustment { rate: u128, target: u128, add: bool }, + + Enable { status: GovernanceStatus, address: EvmAddress, calculator: EvmAddress }, + Disable { status: GovernanceStatus, address: EvmAddress }, + ForfeitReserves { router: EvmAddress, liquidity: u128, destroyer_mode: bool }, + RedeemReserves { router: EvmAddress, amount: u128 }, + Withdraw { token: EvmAddress, amount: u128 }, + AuditReserves, + + Close { id: u128 }, + Create { + market: [u128; 3], + terms: [u128; 2], + token: EvmAddress, + intervals: [u32; 2], + booleans: [bool; 2], + }, +} + +impl GovernanceAction { + pub fn get_message<'a>( + &self, + message_buffer: &'a mut [u8; MAX_MESSAGE_SIZE as usize], + ) -> Option<&'a [u8]> { + match &self { + Self::SetDistributor { distributor } => { + abi_encode_evm_message!( + message_buffer, + "setDistributor(address)", + [distributor] + ) + }, + Self::SetWarmupPeriod { warmup_period } => { + abi_encode_evm_message!( + message_buffer, + "setWarmupPeriod(uint256)", + [warmup_period] + ) + }, + Self::UpdateGatekeeperAddress { new_gatekeeper } => { + abi_encode_evm_message!( + message_buffer, + "updateGatekeeperAddress(address)", + [new_gatekeeper] + ) + }, + Self::SetBounty { bounty } => { + abi_encode_evm_message!( + message_buffer, + "setBounty(uint256)", + [bounty] + ) + }, + Self::AddPool { address } => { + abi_encode_evm_message!( + message_buffer, + "addPool(address)", + [address] + ) + }, + Self::RemovePool { index } => { + abi_encode_evm_message!( + message_buffer, + "removePool(uint256)", + [index] + ) + }, + Self::SetAdjustment { rate, target, add } => { + abi_encode_evm_message!( + message_buffer, + "setAdjustment(uint256,uint256,bool)", + [rate, target, &(*add as u8)] + ) + }, + Self::Enable { status, address, calculator } => { + abi_encode_evm_message!( + message_buffer, + "enable(uint8,address,address)", + [&(*status as u8), address, calculator] + ) + }, + Self::Disable { status, address } => { + abi_encode_evm_message!( + message_buffer, + "disable(uint8,address)", + [&(*status as u8), address] + ) + }, + Self::ForfeitReserves { router, liquidity, destroyer_mode } => { + abi_encode_evm_message!( + message_buffer, + "forfeitReserves(address,uint256,bool)", + [router, liquidity, &(*destroyer_mode as u8)] + ) + }, + Self::RedeemReserves { router, amount } => { + abi_encode_evm_message!( + message_buffer, + "redeemReserve(address,uint256)", + [router, amount] + ) + }, + Self::Withdraw { token, amount } => { + abi_encode_evm_message!( + message_buffer, + "withdraw(address,uint256)", + [token, amount] + ) + }, + Self::AuditReserves => { + abi_encode_evm_message!(message_buffer, "auditReserves()", []) + }, + Self::Close { id } => { + abi_encode_evm_message!(message_buffer, "close(uint256)", [id]) + }, + Self::Create { market, terms, token, intervals, booleans } => { + let booleans_u8 = [booleans[0] as u8, booleans[1] as u8]; + abi_encode_evm_message!( + message_buffer, + "create(uint256[3],uint256[2],address,uint32[2],bool[2])", + [market, terms, token, intervals, &booleans_u8] + ) + }, + } + } +} + #[derive(Encode, Decode, Clone, PartialEq, TypeInfo, RuntimeDebug)] pub struct ExodusRequest { pub r#type: ExodusRequestType, @@ -123,7 +329,12 @@ pub enum ExodusRequestType { bounty: Perbill, receiver: EvmAddress, }, - EvmGovernance { network_id: NetworkId }, + EvmGovernance { + exodus_session: ExodusSession, + network_id: NetworkId, + target: EvmAddress, + action: GovernanceAction, + }, UtxoBridgeOut { network_id: NetworkId }, } @@ -144,6 +355,24 @@ where [exodus_session, public_key, parity] ) }, + ExodusRequestType::EvmGovernance { exodus_session, network_id, target, action } => { + let mut action_buffer = [0u8; MAX_MESSAGE_SIZE as usize]; + let action_payload = action.get_message(&mut action_buffer)?; + + let mut metadata_buffer = [0u8; 32]; + + let chain_id_u64: u64 = (*network_id).unique_saturated_into(); + metadata_buffer[0..8].copy_from_slice(&chain_id_u64.to_be_bytes()); + metadata_buffer[8..28].copy_from_slice(target.as_ref()); + + let packed_metadata = EvmBytes32::from(&metadata_buffer); + + abi_encode_evm_message!( + message_buffer, + "govern(uint256,uint256,bytes)", + [exodus_session, &packed_metadata, action_payload] + ) + }, ExodusRequestType::EvmBridgeOut { exodus_session, network_id, amount, bounty, receiver } => { let mut buffer = [0u8; 32]; buffer[0..20].copy_from_slice(receiver.as_ref()); @@ -170,7 +399,7 @@ where abi_encode_evm_message!( message_buffer, - "materialize(uint256,uint256,uint256)", + "recall(uint256,uint256,uint256)", [exodus_session, &amount_u256, &packed_metadata] ) } @@ -221,6 +450,21 @@ where Self::default_request_for(evm_bridge_out) } + pub fn evm_governance( + exodus_session: ExodusSession, + network_id: NetworkId, + target: EvmAddress, + action: GovernanceAction, + ) -> Self { + let evm_governance = ExodusRequestType::EvmGovernance { + exodus_session, + network_id, + target, + action, + }; + Self::default_request_for(evm_governance) + } + pub fn is_rotation(&self) -> bool { match &self.r#type { ExodusRequestType::EvmRotation { .. } | ExodusRequestType::UtxoRotation { .. } => true, @@ -234,3 +478,255 @@ where Self { r#type: request_type, next_roast_session: Default::default() } } } + +#[cfg(test)] +mod tests { + use super::*; + use sp_io::hashing::keccak_256; + + #[test] + fn test_rotation_message_valid() { + let mut buffer = [0u8; MAX_MESSAGE_SIZE as usize]; + let public_key = EvmBytes32::repeat_byte(0x11); + let session = 69u64; + + let rotation_request = ExodusRequest::::evm_rotation( + session, + public_key.clone(), + 1, + ); + + let calldata = rotation_request.get_message(&mut buffer).unwrap(); + assert_eq!(calldata.len(), 100); + + let expected_hash = keccak_256("rotate(uint256,bytes32,uint8)".as_bytes()); + + assert_eq!(calldata[0..4], expected_hash[0..4]); + assert_eq!(calldata[35], 0x45); + assert_eq!(&calldata[4..35], &[0u8; 31]); + assert_eq!(&calldata[36..68], public_key.as_ref()); + assert_eq!(calldata[99], 1); + assert_eq!(&calldata[68..99], &[0u8; 31]); + } + + #[test] + fn test_bridge_out_message_valid() { + let mut buffer = [0u8; MAX_MESSAGE_SIZE as usize]; + let receiver = EvmAddress::repeat_byte(0xAA); + let network_id = 420u64; + let session = 69u64; + + let bridge_request = ExodusRequest::::evm_bridge_out( + session, + network_id, + 1_000_000u128, + sp_runtime::Perbill::from_percent(1), + receiver.clone() + ); + + let calldata = bridge_request.get_message(&mut buffer).unwrap(); + assert_eq!(calldata.len(), 100); + + let expected_hash = keccak_256("recall(uint256,uint256,uint256)".as_bytes()); + assert_eq!(calldata[0..4], expected_hash[0..4]); + assert_eq!(calldata[35], 69); + assert_eq!(calldata[67], 0x40); + assert_eq!(calldata[66], 0x42); + assert_eq!(calldata[65], 0x0F); + + let metadata_slot = &calldata[68..100]; + assert_eq!(&metadata_slot[0..20], receiver.as_ref()); + assert_eq!(metadata_slot[27], 0xA4); + assert_eq!(metadata_slot[26], 0x01); + + assert_eq!(metadata_slot[31], 0x28); + assert_eq!(metadata_slot[30], 0x5C); + assert_eq!(metadata_slot[29], 0x8F); + assert_eq!(metadata_slot[28], 0x02); + } + + #[test] + fn test_governance_message_valid() { + let mut buffer = [0u8; MAX_MESSAGE_SIZE as usize]; + let target = EvmAddress::repeat_byte(0xAA); + let network_id = 420u64; + let session = 69u64; + + let actions = vec![ + GovernanceAction::SetDistributor { distributor: EvmAddress::repeat_byte(0x11) }, + GovernanceAction::SetWarmupPeriod { warmup_period: 3600 }, + GovernanceAction::UpdateGatekeeperAddress { new_gatekeeper: EvmAddress::repeat_byte(0x22) }, + + GovernanceAction::SetBounty { bounty: 500 }, + GovernanceAction::AddPool { address: EvmAddress::repeat_byte(0x22) }, + GovernanceAction::RemovePool { index: 2 }, + GovernanceAction::SetAdjustment { rate: 10, target: 100, add: true }, + + GovernanceAction::Enable { + status: GovernanceStatus::ReserveDepositor, + address: EvmAddress::repeat_byte(0x33), + calculator: EvmAddress::repeat_byte(0x44) + }, + GovernanceAction::Disable { + status: GovernanceStatus::ReserveToken, + address: EvmAddress::repeat_byte(0x55) + }, + GovernanceAction::ForfeitReserves { + router: EvmAddress::repeat_byte(0x66), + liquidity: 5000, + destroyer_mode: false + }, + GovernanceAction::RedeemReserves { router: EvmAddress::repeat_byte(0x77), amount: 10000 }, + GovernanceAction::Withdraw { token: EvmAddress::repeat_byte(0x88), amount: 2500 }, + GovernanceAction::AuditReserves, + + GovernanceAction::Close { id: 99 }, + GovernanceAction::Create { + market: [69, 420, 1337], + terms: [69, 420], + token: EvmAddress::repeat_byte(0x99), + intervals: [69, 1337], + booleans: [true, false], + }, + ]; + + for (index, action) in actions.into_iter().enumerate() { + let action_clone = action.clone(); + + let governance_request = ExodusRequest::::evm_governance( + session, + network_id, + target, + action, + ); + + let message = governance_request + .get_message(&mut buffer) + .get_or_insert(&[]) + .to_vec(); + + assert!(!message.is_empty(), "Failed to encode action at index {}", index); + + let expected_master_hash = keccak_256("govern(uint256,uint256,bytes)".as_bytes()); + assert_eq!(message[0..4], expected_master_hash[0..4]); + + assert_eq!(message[35], 69); + assert_eq!(&message[4..35], &[0u8; 31]); + + assert_eq!(message[43], 0xA4); + assert_eq!(message[42], 0x01); + assert_eq!(&message[44..64], target.as_ref()); + assert_eq!(&message[64..68], &[0u8; 4]); + + assert_eq!(message[99], 96); + + let inner_payload_len = u32::from_be_bytes(message[128..132].try_into().unwrap()) as usize; + let inner_payload = &message[132..132 + inner_payload_len]; + + assert_eq!((message.len() - 4) % 32, 0); + + match action_clone { + GovernanceAction::SetDistributor { distributor } => { + let hash = keccak_256("setDistributor(address)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(&inner_payload[16..36], distributor.as_ref()); + }, + GovernanceAction::SetWarmupPeriod { warmup_period } => { + let hash = keccak_256("setWarmupPeriod(uint256)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(inner_payload[inner_payload.len() - 1], warmup_period as u8); + }, + GovernanceAction::UpdateGatekeeperAddress { new_gatekeeper } => { + let hash = keccak_256("updateGatekeeperAddress(address)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(&inner_payload[16..36], new_gatekeeper.as_ref()); + }, + GovernanceAction::SetBounty { bounty } => { + let hash = keccak_256("setBounty(uint256)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(inner_payload[inner_payload.len() - 1], bounty as u8); + }, + GovernanceAction::AddPool { address } => { + let hash = keccak_256("addPool(address)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(&inner_payload[16..36], address.as_ref()); + }, + GovernanceAction::RemovePool { index } => { + let hash = keccak_256("removePool(uint256)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(inner_payload[inner_payload.len() - 1], index as u8); + }, + GovernanceAction::SetAdjustment { rate, target, add } => { + let hash = keccak_256("setAdjustment(uint256,uint256,bool)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(inner_payload[35], rate as u8); + assert_eq!(inner_payload[67], target as u8); + assert_eq!(inner_payload[99], add as u8); + }, + GovernanceAction::Enable { status, address, calculator } => { + let hash = keccak_256("enable(uint8,address,address)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(inner_payload[35], status as u8); + assert_eq!(&inner_payload[48..68], address.as_ref()); + assert_eq!(&inner_payload[80..100], calculator.as_ref()); + }, + GovernanceAction::Disable { status, address } => { + let hash = keccak_256("disable(uint8,address)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(inner_payload[35], status as u8); + assert_eq!(&inner_payload[48..68], address.as_ref()); + }, + GovernanceAction::ForfeitReserves { router, liquidity, destroyer_mode } => { + let hash = keccak_256("forfeitReserves(address,uint256,bool)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(&inner_payload[16..36], router.as_ref()); + assert_eq!(inner_payload[67], liquidity as u8); + assert_eq!(inner_payload[99], destroyer_mode as u8); + }, + GovernanceAction::RedeemReserves { router, amount } => { + let hash = keccak_256("redeemReserve(address,uint256)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(&inner_payload[16..36], router.as_ref()); + assert_eq!(inner_payload[67], amount as u8); + }, + GovernanceAction::Withdraw { token, amount } => { + let hash = keccak_256("withdraw(address,uint256)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(&inner_payload[16..36], token.as_ref()); + assert_eq!(inner_payload[67], amount as u8); + }, + GovernanceAction::AuditReserves => { + let hash = keccak_256("auditReserves()".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(inner_payload.len(), 4); + }, + GovernanceAction::Close { id } => { + let hash = keccak_256("close(uint256)".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(inner_payload[inner_payload.len() - 1], id as u8); + }, + GovernanceAction::Create { market, terms, token, intervals, booleans } => { + let hash = keccak_256("create(uint256[3],uint256[2],address,uint32[2],bool[2])".as_bytes()); + assert_eq!(inner_payload[0..4], hash[0..4]); + assert_eq!(inner_payload.len(), 324); + assert_eq!(inner_payload[35], 69); + + assert_eq!(u128::from_be_bytes(inner_payload[20..36].try_into().unwrap()), market[0]); + assert_eq!(u128::from_be_bytes(inner_payload[52..68].try_into().unwrap()), market[1]); + assert_eq!(u128::from_be_bytes(inner_payload[84..100].try_into().unwrap()), market[2]); + + assert_eq!(u128::from_be_bytes(inner_payload[116..132].try_into().unwrap()), terms[0]); + assert_eq!(u128::from_be_bytes(inner_payload[148..164].try_into().unwrap()), terms[1]); + + assert_eq!(&inner_payload[176..196], token.as_ref()); + + assert_eq!(u32::from_be_bytes(inner_payload[224..228].try_into().unwrap()), intervals[0]); + assert_eq!(u32::from_be_bytes(inner_payload[256..260].try_into().unwrap()), intervals[1]); + + assert_eq!(inner_payload[291], booleans[0] as u8); + assert_eq!(inner_payload[323], booleans[1] as u8); + } + } + } + } +}