Repository: interlay/interbtc (language: rust)

Issue:
Table 2 referenda on Monday
**Is your feature request related to a problem? Please describe.**
Due to block time variation, it is hard to plan when proposals are tabled to a referendum. Moreover, since only a single proposal is tabled in a given period, there is a backlog of referenda.

**Describe the solution you'd like**
1. Table referenda on Monday at 09.00 am UTC. Assuming 2 days of voting on kintsugi and 7 days of voting on interlay, this will mean that referenda are enacted on Wednesday noon-ish on kintsugi and Monday noon-ish on interlay. This is when most of the Interlay team is online to help with any potential issues.
2. Table 2 referenda each period. This will allow proposals to pass in parallel, e.g., one treasury and one regular governance proposal.



Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 456261c928..85950835f3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2063,11 +2063,13 @@ dependencies = [
 name = "democracy"
 version = "1.2.0"
 dependencies = [
+ "chrono",
  "frame-benchmarking",
  "frame-support",
  "frame-system",
  "pallet-balances",
  "pallet-scheduler",
+ "pallet-timestamp",
  "parity-scale-codec",
  "scale-info",
  "serde",
diff --git a/crates/democracy/Cargo.toml b/crates/democracy/Cargo.toml
index 51debc283f..ff3d1bad39 100644
--- a/crates/democracy/Cargo.toml
+++ b/crates/democracy/Cargo.toml
@@ -16,6 +16,7 @@ targets = ["x86_64-unknown-linux-gnu"]
 serde = { version = "1.0.130", default-features = false, features = ["derive"], optional = true }
 codec = { package = "parity-scale-codec", version = "3.1.5", default-features = false, features = ["derive", "max-encoded-len"] }
 scale-info = { version = "2.2.0", default-features = false, features = ["derive"] }
+chrono = {version = "0.4.23", default-features = false }
 
 sp-api = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31", default-features = false }
 sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31", default-features = false }
@@ -29,6 +30,7 @@ frame-system = { git = "https://github.com/paritytech/substrate", branch = "polk
 [dev-dependencies]
 pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31" }
 pallet-scheduler = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31" }
+pallet-timestamp = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31" }
 
 [features]
 default = ["std"]
diff --git a/crates/democracy/src/benchmarking.rs b/crates/democracy/src/benchmarking.rs
index 6deefd46a0..bbb4be9834 100644
--- a/crates/democracy/src/benchmarking.rs
+++ b/crates/democracy/src/benchmarking.rs
@@ -42,7 +42,7 @@ fn add_referendum<T: Config>(n: u32) -> Result<ReferendumIndex, &'static str> {
     let proposal_hash: T::Hash = T::Hashing::hash_of(&n);
     let vote_threshold = VoteThreshold::SimpleMajority;
 
-    Democracy::<T>::inject_referendum(T::LaunchPeriod::get(), proposal_hash, vote_threshold, 0u32.into());
+    Democracy::<T>::inject_referendum(T::VotingPeriod::get(), proposal_hash, vote_threshold, 0u32.into());
     let referendum_index: ReferendumIndex = ReferendumCount::<T>::get() - 1;
     T::Scheduler::schedule_named(
         (DEMOCRACY_ID, referendum_index).encode(),
@@ -198,7 +198,7 @@ benchmarks! {
         // Launch public
         assert!(add_proposal::<T>(r).is_ok(), "proposal not created");
 
-        let block_number = T::LaunchPeriod::get();
+        let block_number = T::VotingPeriod::get();
 
     }: { Democracy::<T>::on_initialize(block_number) }
     verify {
@@ -264,7 +264,7 @@ benchmarks! {
         assert_eq!(Democracy::<T>::referendum_count(), r, "referenda not created");
         assert_eq!(Democracy::<T>::lowest_unbaked(), 0, "invalid referenda init");
 
-        let block_number = T::LaunchPeriod::get();
+        let block_number = T::VotingPeriod::get();
 
     }: { Democracy::<T>::on_initialize(block_number) }
     verify {
diff --git a/crates/democracy/src/lib.rs b/crates/democracy/src/lib.rs
index 9e3598c00b..2fe5acdda1 100644
--- a/crates/democracy/src/lib.rs
+++ b/crates/democracy/src/lib.rs
@@ -80,12 +80,16 @@
 #![recursion_limit = "256"]
 #![cfg_attr(not(feature = "std"), no_std)]
 
+use core::time::Duration;
+
+use chrono::Days;
 use codec::{Decode, DecodeLimit, Encode, Input, MaxEncodedLen};
 use frame_support::{
     ensure,
     traits::{
         schedule::{DispatchTime, Named as ScheduleNamed},
         BalanceStatus, Currency, Get, LockIdentifier, OnUnbalanced, ReservableCurrency, UnfilteredDispatchable,
+        UnixTime,
     },
     transactional,
     weights::Weight,
@@ -157,6 +161,8 @@ enum Releases {
 
 #[frame_support::pallet]
 pub mod pallet {
+    use core::num::TryFromIntError;
+
     use super::*;
     use frame_support::{
         dispatch::{DispatchClass, DispatchResultWithPostInfo, Pays},
@@ -191,10 +197,6 @@ pub mod pallet {
         #[pallet::constant]
         type EnactmentPeriod: Get<Self::BlockNumber>;
 
-        /// How often (in blocks) new public referenda are launched.
-        #[pallet::constant]
-        type LaunchPeriod: Get<Self::BlockNumber>;
-
         /// How often (in blocks) to check for new votes.
         #[pallet::constant]
         type VotingPeriod: Get<Self::BlockNumber>;
@@ -238,6 +240,14 @@ pub mod pallet {
         /// The maximum number of public proposals that can exist at any time.
         #[pallet::constant]
         type MaxProposals: Get<u32>;
+
+        /// Unix time
+        type UnixTime: UnixTime;
+
+        /// Duration
+        type Moment: TryInto<i64>;
+
+        type LaunchOffsetMillis: Get<Self::Moment>;
     }
 
     /// The number of (public) proposals that have been made so far.
@@ -297,6 +307,9 @@ pub mod pallet {
     #[pallet::storage]
     pub(crate) type StorageVersion<T> = StorageValue<_, Releases>;
 
+    #[pallet::storage]
+    pub type NextLaunchTimestamp<T: Config> = StorageValue<_, u64, ValueQuery>;
+
     #[pallet::genesis_config]
     pub struct GenesisConfig<T: Config> {
         _phantom: sp_std::marker::PhantomData<T>,
@@ -398,6 +411,14 @@ pub mod pallet {
         MaxVotesReached,
         /// Maximum number of proposals reached.
         TooManyProposals,
+        /// Unable to convert value.
+        TryIntoIntError,
+    }
+
+    impl<T> From<TryFromIntError> for Error<T> {
+        fn from(_err: TryFromIntError) -> Self {
+            Error::<T>::TryIntoIntError
+        }
     }
 
     #[pallet::hooks]
@@ -1069,11 +1090,14 @@ impl<T: Config> Pallet<T> {
         let r = last.saturating_sub(next);
 
         // pick out another public referendum if it's time.
-        if (now % T::LaunchPeriod::get()).is_zero() {
+        let current_time = T::UnixTime::now();
+        if Self::should_launch(current_time)? {
             // Errors come from the queue being empty. If the queue is not empty, it will take
             // full block weight.
             if Self::launch_next(now).is_ok() {
                 weight = max_block_weight;
+                // try to launch another one. We ignore the result since weight can't increase beyond max_block_weight
+                let _ = Self::launch_next(now);
             } else {
                 weight = weight.saturating_add(T::WeightInfo::on_initialize_base_with_launch_period(r));
             }
@@ -1091,6 +1115,41 @@ impl<T: Config> Pallet<T> {
         Ok(weight)
     }
 
+    /// determine whether or not a new referendum should be launched. This will return true
+    /// once every week.
+    fn should_launch(now: Duration) -> Result<bool, DispatchError> {
+        if now.as_secs() < NextLaunchTimestamp::<T>::get() {
+            return Ok(false);
+        }
+
+        // time to launch - calculate the date of next launch.
+
+        // convert to format used by `chrono`
+        let secs: i64 = now.as_secs().try_into().map_err(Error::<T>::from)?;
+        let now =
+            chrono::NaiveDateTime::from_timestamp_opt(secs, now.subsec_nanos()).ok_or(Error::<T>::TryIntoIntError)?;
+
+        // calculate next week boundary
+        let beginning_of_week = now.date().week(chrono::Weekday::Mon).first_day();
+        let next_week = beginning_of_week
+            .checked_add_days(Days::new(7))
+            .ok_or(Error::<T>::TryIntoIntError)?
+            .and_time(Default::default());
+
+        let offset = T::LaunchOffsetMillis::get()
+            .try_into()
+            .map_err(|_| Error::<T>::TryIntoIntError)?;
+        let next_launch = next_week
+            .checked_add_signed(chrono::Duration::milliseconds(offset))
+            .ok_or(ArithmeticError::Overflow)?;
+
+        // update storage
+        let next_timestamp: u64 = next_launch.timestamp().try_into().map_err(Error::<T>::from)?;
+        NextLaunchTimestamp::<T>::set(next_timestamp);
+
+        Ok(true)
+    }
+
     /// Reads the length of account in DepositOf without getting the complete value in the runtime.
     ///
     /// Return 0 if no deposit for this proposal.
diff --git a/crates/loans/src/lib.rs b/crates/loans/src/lib.rs
index a06657f11b..97a3496234 100644
--- a/crates/loans/src/lib.rs
+++ b/crates/loans/src/lib.rs
@@ -370,7 +370,7 @@ pub mod pallet {
         }
 
         #[cfg(feature = "try-runtime")]
-        fn post_upgrade(_pre_upgrade_state: Vec<u8>) -> Result<(), &'static str> {
+        fn post_upgrade(_pre_upgrade_state: sp_std::vec::Vec<u8>) -> Result<(), &'static str> {
             let max_exchange_rate = crate::MaxExchangeRate::<T>::get();
             let min_exchange_rate = crate::MinExchangeRate::<T>::get();
             ensure!(
diff --git a/parachain/runtime/interlay/src/lib.rs b/parachain/runtime/interlay/src/lib.rs
index 579ace861c..cca8b76a19 100644
--- a/parachain/runtime/interlay/src/lib.rs
+++ b/parachain/runtime/interlay/src/lib.rs
@@ -459,7 +459,6 @@ type EnsureRootOrAllTechnicalCommittee = EitherOfDiverse<
 >;
 
 parameter_types! {
-    pub const LaunchPeriod: BlockNumber = 7 * DAYS;
     pub const VotingPeriod: BlockNumber = 7 * DAYS;
     pub const FastTrackVotingPeriod: BlockNumber = 3 * HOURS;
     // Require 250 vINTR to make a proposal. Given the crowdloan airdrop, this qualifies about 7500
@@ -469,6 +468,7 @@ parameter_types! {
     pub PreimageByteDeposit: Balance = 10 * MILLICENTS;
     pub const MaxVotes: u32 = 100;
     pub const MaxProposals: u32 = 100;
+    pub LaunchOffsetMillis: u64 = 9 * 60 * 60 * 1000; // 9 hours offset, i.e. MON 9 AM
 }
 
 impl democracy::Config for Runtime {
@@ -476,7 +476,6 @@ impl democracy::Config for Runtime {
     type RuntimeEvent = RuntimeEvent;
     type Currency = Escrow;
     type EnactmentPeriod = EnactmentPeriod;
-    type LaunchPeriod = LaunchPeriod;
     type VotingPeriod = VotingPeriod;
     type MinimumDeposit = MinimumDeposit;
     /// The technical committee can have any proposal be tabled immediately
@@ -490,6 +489,9 @@ impl democracy::Config for Runtime {
     type MaxVotes = MaxVotes;
     type WeightInfo = ();
     type MaxProposals = MaxProposals;
+    type UnixTime = Timestamp;
+    type Moment = Moment;
+    type LaunchOffsetMillis = LaunchOffsetMillis;
 }
 
 parameter_types! {
diff --git a/parachain/runtime/kintsugi/src/lib.rs b/parachain/runtime/kintsugi/src/lib.rs
index 47425c3e2c..192fca05f8 100644
--- a/parachain/runtime/kintsugi/src/lib.rs
+++ b/parachain/runtime/kintsugi/src/lib.rs
@@ -458,7 +458,6 @@ type EnsureRootOrAllTechnicalCommittee = EitherOfDiverse<
 >;
 
 parameter_types! {
-    pub const LaunchPeriod: BlockNumber = 7 * DAYS;
     pub const VotingPeriod: BlockNumber = 2 * DAYS;
     pub const FastTrackVotingPeriod: BlockNumber = 3 * HOURS;
     // Require 5 vKINT to make a proposal. Given the crowdloan airdrop, this qualifies about 3500
@@ -469,6 +468,7 @@ parameter_types! {
     pub PreimageByteDeposit: Balance = 10 * MILLICENTS;
     pub const MaxVotes: u32 = 100;
     pub const MaxProposals: u32 = 100;
+    pub LaunchOffsetMillis: u64 = 9 * 60 * 60 * 1000; // 9 hours offset, i.e. MON 9 AM
 }
 
 impl democracy::Config for Runtime {
@@ -476,7 +476,6 @@ impl democracy::Config for Runtime {
     type RuntimeEvent = RuntimeEvent;
     type Currency = Escrow;
     type EnactmentPeriod = EnactmentPeriod;
-    type LaunchPeriod = LaunchPeriod;
     type VotingPeriod = VotingPeriod;
     type MinimumDeposit = MinimumDeposit;
     /// The technical committee can have any proposal be tabled immediately
@@ -490,6 +489,9 @@ impl democracy::Config for Runtime {
     type MaxVotes = MaxVotes;
     type WeightInfo = ();
     type MaxProposals = MaxProposals;
+    type UnixTime = Timestamp;
+    type Moment = Moment;
+    type LaunchOffsetMillis = LaunchOffsetMillis;
 }
 
 parameter_types! {
diff --git a/standalone/runtime/src/lib.rs b/standalone/runtime/src/lib.rs
index 552ab0d3e2..2010718263 100644
--- a/standalone/runtime/src/lib.rs
+++ b/standalone/runtime/src/lib.rs
@@ -432,7 +432,6 @@ type EnsureRootOrAllTechnicalCommittee = EitherOfDiverse<
 >;
 
 parameter_types! {
-    pub const LaunchPeriod: BlockNumber = 2 * MINUTES;
     pub const VotingPeriod: BlockNumber = 5 * MINUTES;
     pub const FastTrackVotingPeriod: BlockNumber = 1 * MINUTES;
     pub MinimumDeposit: Balance = 100 * DOLLARS;
@@ -440,6 +439,7 @@ parameter_types! {
     pub PreimageByteDeposit: Balance = 1 * CENTS;
     pub const MaxVotes: u32 = 100;
     pub const MaxProposals: u32 = 100;
+    pub LaunchOffsetMillis: u64 = 9 * 60 * 60 * 1000; // 9 hours offset, i.e. MON 9 AM
 }
 
 impl democracy::Config for Runtime {
@@ -447,7 +447,6 @@ impl democracy::Config for Runtime {
     type RuntimeEvent = RuntimeEvent;
     type Currency = Escrow;
     type EnactmentPeriod = EnactmentPeriod;
-    type LaunchPeriod = LaunchPeriod;
     type VotingPeriod = VotingPeriod;
     type MinimumDeposit = MinimumDeposit;
     /// The technical committee can have any proposal be tabled immediately
@@ -461,6 +460,9 @@ impl democracy::Config for Runtime {
     type MaxVotes = MaxVotes;
     type WeightInfo = ();
     type MaxProposals = MaxProposals;
+    type UnixTime = Timestamp;
+    type Moment = Moment;
+    type LaunchOffsetMillis = LaunchOffsetMillis;
 }
 
 parameter_types! {


Test patch:
diff --git a/crates/democracy/src/tests.rs b/crates/democracy/src/tests.rs
index ceb396abae..b950c36c4f 100644
--- a/crates/democracy/src/tests.rs
+++ b/crates/democracy/src/tests.rs
@@ -39,6 +39,7 @@ frame_support::construct_runtime!(
         Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
         Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event<T>},
         Democracy: pallet_democracy::{Pallet, Call, Storage, Config<T>, Event<T>},
+        Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent},
     }
 );
 
@@ -100,6 +101,17 @@ impl pallet_scheduler::Config for Test {
     type Preimages = ();
 }
 
+parameter_types! {
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl pallet_timestamp::Config for Test {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+    type WeightInfo = ();
+}
+
 parameter_types! {
     pub const ExistentialDeposit: u64 = 1;
     pub const MaxLocks: u32 = 10;
@@ -124,6 +136,7 @@ parameter_types! {
     pub const MaxVotes: u32 = 100;
     pub const MaxProposals: u32 = MAX_PROPOSALS;
     pub static PreimageByteDeposit: u64 = 0;
+    pub LaunchOffsetMillis: u64 = 9 * 60 * 60 * 1000; // 9 hours offset, i.e. MON 9 AM
 }
 ord_parameter_types! {
     pub const One: u64 = 1;
@@ -147,7 +160,6 @@ impl Config for Test {
     type RuntimeEvent = RuntimeEvent;
     type Currency = pallet_balances::Pallet<Self>;
     type EnactmentPeriod = EnactmentPeriod;
-    type LaunchPeriod = LaunchPeriod;
     type VotingPeriod = VotingPeriod;
     type FastTrackVotingPeriod = FastTrackVotingPeriod;
     type MinimumDeposit = MinimumDeposit;
@@ -159,6 +171,9 @@ impl Config for Test {
     type PalletsOrigin = OriginCaller;
     type WeightInfo = ();
     type MaxProposals = MaxProposals;
+    type UnixTime = Timestamp;
+    type Moment = u64;
+    type LaunchOffsetMillis = LaunchOffsetMillis;
 }
 
 pub fn new_test_ext() -> sp_io::TestExternalities {
@@ -230,6 +245,9 @@ fn propose_set_balance_and_note(who: u64, value: u64, delay: u64) -> DispatchRes
 }
 
 fn next_block() {
+    let week = 1000 * 60 * 60 * 24 * 7;
+    Timestamp::set_timestamp(System::block_number() * week / 2);
+
     System::set_block_number(System::block_number() + 1);
     Scheduler::on_initialize(System::block_number());
     assert!(Democracy::begin_block(System::block_number()).is_ok());
@@ -265,3 +283,50 @@ fn nay(who: u64) -> Vote<u64> {
 fn tally(r: ReferendumIndex) -> Tally<u64> {
     Democracy::referendum_status(r).unwrap().tally
 }
+
+#[test]
+fn should_launch_works() {
+    new_test_ext().execute_with(|| {
+        let arbitrary_timestamp = 1670864631; // Mon Dec 12 2022 17:03:51 UTC
+
+        let week_boundaries = [
+            1671440400, // Mon Dec 19 2022 09:00:00 UTC
+            1672045200, // Mon Dec 26 2022 09:00:00 UTC
+            1672650000, // Mon Jan 02 2023 09:00:00 UTC
+        ];
+        // first launch immediately after launch of chain / first runtime upgrade
+        assert!(Democracy::should_launch(Duration::from_secs(arbitrary_timestamp)).unwrap());
+        // second time it should return false
+        assert!(!Democracy::should_launch(Duration::from_secs(arbitrary_timestamp)).unwrap());
+
+        for boundary in week_boundaries {
+            // one second before the next week it should still return false
+            assert!(!Democracy::should_launch(Duration::from_secs(boundary - 1)).unwrap());
+
+            // first second of next week it should return true exactly once
+            assert!(Democracy::should_launch(Duration::from_secs(boundary)).unwrap());
+            assert!(!Democracy::should_launch(Duration::from_secs(boundary)).unwrap());
+        }
+    });
+}
+
+#[test]
+fn should_launch_edge_case_behavior() {
+    new_test_ext().execute_with(|| {
+        // test edge case where we launch on monday before 9 am. Next launch will be
+        // in slightly more than 7 days
+        let initial_launch = 1670828400; // Mon Dec 12 2022 07:00:00 UTC
+        let next_launch = 1671440400; // Mon Dec 19 2022 09:00:00 UTC
+
+        // first launch immediately after launch of chain / first runtime upgrade
+        assert!(Democracy::should_launch(Duration::from_secs(initial_launch)).unwrap());
+        assert!(!Democracy::should_launch(Duration::from_secs(initial_launch)).unwrap());
+
+        // one second before the next week it should still return false
+        assert!(!Democracy::should_launch(Duration::from_secs(next_launch - 1)).unwrap());
+
+        // first second of next week it should return true exactly once
+        assert!(Democracy::should_launch(Duration::from_secs(next_launch)).unwrap());
+        assert!(!Democracy::should_launch(Duration::from_secs(next_launch)).unwrap());
+    });
+}
diff --git a/crates/democracy/src/tests/public_proposals.rs b/crates/democracy/src/tests/public_proposals.rs
index 07fcb30b7d..e0bfc09d3d 100644
--- a/crates/democracy/src/tests/public_proposals.rs
+++ b/crates/democracy/src/tests/public_proposals.rs
@@ -116,14 +116,35 @@ fn invalid_seconds_upper_bound_should_not_work() {
 fn runners_up_should_come_after() {
     new_test_ext().execute_with(|| {
         System::set_block_number(0);
+        // make 6 proposals and check that 2 get launched each period launching period
+        // (which is every 2 blocks in these tests)
+
         assert_ok!(propose_set_balance_and_note(1, 2, 2));
         assert_ok!(propose_set_balance_and_note(1, 4, 4));
         assert_ok!(propose_set_balance_and_note(1, 3, 3));
-        fast_forward_to(2);
+        assert_ok!(propose_set_balance_and_note(2, 5, 3));
+        assert_ok!(propose_set_balance_and_note(2, 6, 3));
+        assert_ok!(propose_set_balance_and_note(2, 8, 3));
+
+        // sanity check: nothing launched yet
+        assert!(Democracy::vote(RuntimeOrigin::signed(1), 0, aye(1)).is_err());
+
+        fast_forward_to(1);
         assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), 0, aye(1)));
-        fast_forward_to(4);
         assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), 1, aye(1)));
-        fast_forward_to(6);
+        assert!(Democracy::vote(RuntimeOrigin::signed(1), 2, aye(1)).is_err()); // third one not yet launched
+
+        // sanity check: on next block nothing should get launched
+        fast_forward_to(2);
+        assert!(Democracy::vote(RuntimeOrigin::signed(1), 2, aye(1)).is_err());
+
+        fast_forward_to(3);
         assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), 2, aye(1)));
+        assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), 3, aye(1)));
+        assert!(Democracy::vote(RuntimeOrigin::signed(1), 4, aye(1)).is_err()); // fifth one not yet launched
+
+        fast_forward_to(5);
+        assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), 4, aye(1)));
+        assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), 5, aye(1)));
     });
 }
diff --git a/crates/democracy/src/tests/voting.rs b/crates/democracy/src/tests/voting.rs
index 2fec0a8061..f24736dec9 100644
--- a/crates/democracy/src/tests/voting.rs
+++ b/crates/democracy/src/tests/voting.rs
@@ -21,15 +21,15 @@ fn single_proposal_should_work() {
         let r = 0;
         assert!(Democracy::referendum_info(r).is_none());
 
-        // start of 2 => next referendum scheduled.
-        fast_forward_to(2);
+        // start of 1 => next referendum scheduled.
+        fast_forward_to(1);
         assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(1)));
 
         assert_eq!(Democracy::referendum_count(), 1);
         assert_eq!(
             Democracy::referendum_status(0),
             Ok(ReferendumStatus {
-                end: 4,
+                end: 3,
                 proposal_hash: set_balance_proposal_hash_and_note(2),
                 threshold: VoteThreshold::SuperMajorityAgainst,
                 delay: 2,
@@ -41,19 +41,19 @@ fn single_proposal_should_work() {
             })
         );
 
-        fast_forward_to(3);
+        fast_forward_to(2);
 
         // referendum still running
         assert_ok!(Democracy::referendum_status(0));
 
-        // referendum runs during 2 and 3, ends @ start of 4.
-        fast_forward_to(4);
+        // referendum runs during 1 and 2, ends @ start of 3.
+        fast_forward_to(3);
 
         assert_noop!(Democracy::referendum_status(0), Error::<Test>::ReferendumInvalid);
-        assert!(pallet_scheduler::Agenda::<Test>::get(6)[0].is_some());
+        assert!(pallet_scheduler::Agenda::<Test>::get(5)[0].is_some());
 
         // referendum passes and wait another two blocks for enactment.
-        fast_forward_to(6);
+        fast_forward_to(5);
 
         assert_eq!(Balances::free_balance(42), 2);
     });
diff --git a/parachain/runtime/testnet-interlay/src/lib.rs b/parachain/runtime/testnet-interlay/src/lib.rs
index b5d65e55c9..a8eddb274c 100644
--- a/parachain/runtime/testnet-interlay/src/lib.rs
+++ b/parachain/runtime/testnet-interlay/src/lib.rs
@@ -432,7 +432,6 @@ type EnsureRootOrAllTechnicalCommittee = EitherOfDiverse<
 >;
 
 parameter_types! {
-    pub const LaunchPeriod: BlockNumber = 7 * DAYS;
     pub const VotingPeriod: BlockNumber = 7 * DAYS;
     pub const FastTrackVotingPeriod: BlockNumber = 3 * HOURS;
     // Require 5 vKINT to make a proposal. Given the crowdloan airdrop, this qualifies about 3500
@@ -443,6 +442,7 @@ parameter_types! {
     pub PreimageByteDeposit: Balance = 10 * MILLICENTS;
     pub const MaxVotes: u32 = 100;
     pub const MaxProposals: u32 = 100;
+    pub LaunchOffsetMillis: u64 = 9 * 60 * 60 * 1000; // 9 hours offset, i.e. MON 9 AM
 }
 
 impl democracy::Config for Runtime {
@@ -450,7 +450,6 @@ impl democracy::Config for Runtime {
     type RuntimeEvent = RuntimeEvent;
     type Currency = Escrow;
     type EnactmentPeriod = EnactmentPeriod;
-    type LaunchPeriod = LaunchPeriod;
     type VotingPeriod = VotingPeriod;
     type MinimumDeposit = MinimumDeposit;
     /// The technical committee can have any proposal be tabled immediately
@@ -464,6 +463,9 @@ impl democracy::Config for Runtime {
     type MaxVotes = MaxVotes;
     type WeightInfo = ();
     type MaxProposals = MaxProposals;
+    type UnixTime = Timestamp;
+    type Moment = Moment;
+    type LaunchOffsetMillis = LaunchOffsetMillis;
 }
 
 parameter_types! {
diff --git a/parachain/runtime/testnet-kintsugi/src/lib.rs b/parachain/runtime/testnet-kintsugi/src/lib.rs
index 2bda25ce57..5804a220b5 100644
--- a/parachain/runtime/testnet-kintsugi/src/lib.rs
+++ b/parachain/runtime/testnet-kintsugi/src/lib.rs
@@ -432,7 +432,6 @@ type EnsureRootOrAllTechnicalCommittee = EitherOfDiverse<
 >;
 
 parameter_types! {
-    pub const LaunchPeriod: BlockNumber = 7 * DAYS;
     pub const VotingPeriod: BlockNumber = 2 * DAYS;
     pub const FastTrackVotingPeriod: BlockNumber = 3 * HOURS;
     // Require 5 vKINT to make a proposal. Given the crowdloan airdrop, this qualifies about 3500
@@ -443,6 +442,7 @@ parameter_types! {
     pub PreimageByteDeposit: Balance = 10 * MILLICENTS;
     pub const MaxVotes: u32 = 100;
     pub const MaxProposals: u32 = 100;
+    pub LaunchOffsetMillis: u64 = 9 * 60 * 60 * 1000; // 9 hours offset, i.e. MON 9 AM
 }
 
 impl democracy::Config for Runtime {
@@ -450,7 +450,6 @@ impl democracy::Config for Runtime {
     type RuntimeEvent = RuntimeEvent;
     type Currency = Escrow;
     type EnactmentPeriod = EnactmentPeriod;
-    type LaunchPeriod = LaunchPeriod;
     type VotingPeriod = VotingPeriod;
     type MinimumDeposit = MinimumDeposit;
     /// The technical committee can have any proposal be tabled immediately
@@ -464,6 +463,9 @@ impl democracy::Config for Runtime {
     type MaxVotes = MaxVotes;
     type WeightInfo = ();
     type MaxProposals = MaxProposals;
+    type UnixTime = Timestamp;
+    type Moment = Moment;
+    type LaunchOffsetMillis = LaunchOffsetMillis;
 }
 
 parameter_types! {
diff --git a/standalone/runtime/tests/test_governance.rs b/standalone/runtime/tests/test_governance.rs
index fa8aafe03c..83a5e67519 100644
--- a/standalone/runtime/tests/test_governance.rs
+++ b/standalone/runtime/tests/test_governance.rs
@@ -138,7 +138,7 @@ fn create_set_balance_proposal(amount_to_set: Balance) -> H256 {
 }
 
 fn launch_and_approve_referendum() -> (BlockNumber, ReferendumIndex) {
-    let start_height = <Runtime as democracy::Config>::LaunchPeriod::get();
+    let start_height = <Runtime as democracy::Config>::VotingPeriod::get();
     DemocracyPallet::on_initialize(start_height);
     let index = assert_democracy_started_event();
 
@@ -338,7 +338,7 @@ fn integration_test_governance_voter_can_change_vote() {
         let amount_to_set = 1000;
         create_set_balance_proposal(amount_to_set);
 
-        let start_height = <Runtime as democracy::Config>::LaunchPeriod::get();
+        let start_height = <Runtime as democracy::Config>::VotingPeriod::get();
         DemocracyPallet::on_initialize(start_height);
         let index = assert_democracy_started_event();
 
@@ -404,7 +404,7 @@ fn integration_test_fast_track_referendum() {
         let amount_to_set = 1000;
         let proposal_hash = create_set_balance_proposal(amount_to_set);
 
-        let start_height = <Runtime as democracy::Config>::LaunchPeriod::get();
+        let start_height = <Runtime as democracy::Config>::VotingPeriod::get();
         DemocracyPallet::on_initialize(start_height);
         let index = assert_democracy_started_event();
 
@@ -446,7 +446,7 @@ fn integration_test_governance_voter_can_change_vote_with_limited_funds() {
         let amount_to_set = 1000;
         create_set_balance_proposal(amount_to_set);
 
-        let start_height = <Runtime as democracy::Config>::LaunchPeriod::get();
+        let start_height = <Runtime as democracy::Config>::VotingPeriod::get();
         DemocracyPallet::on_initialize(start_height);
         let index = assert_democracy_started_event();
 
@@ -565,10 +565,10 @@ fn integration_test_vote_exceeds_total_voting_power() {
     ExtBuilder::build().execute_with(|| {
         set_free_balance(account_of(ALICE), 10_000_000_000_000_000_000_000);
 
-        // we choose a referendum height that is both on a SPAN and LAUNCHPERIOD boundary
+        // we choose a referendum height that is both on a SPAN and VotingPeriod boundary
         let referendum_height =
-            <Runtime as democracy::Config>::LaunchPeriod::get() * <Runtime as escrow::Config>::Span::get();
-        let start_height = referendum_height - <Runtime as democracy::Config>::LaunchPeriod::get();
+            <Runtime as democracy::Config>::VotingPeriod::get() * <Runtime as escrow::Config>::Span::get();
+        let start_height = referendum_height - <Runtime as democracy::Config>::VotingPeriod::get();
         let end_height = start_height + <Runtime as democracy::Config>::VotingPeriod::get();
 
         SystemPallet::set_block_number(start_height);
@@ -607,7 +607,7 @@ fn integration_test_vote_exceeds_total_voting_power() {
 fn integration_test_proposing_and_voting_only_possible_with_staked_tokens() {
     ExtBuilder::build().execute_with(|| {
         let minimum_proposal_value = <Runtime as democracy::Config>::MinimumDeposit::get();
-        let start_height = <Runtime as democracy::Config>::LaunchPeriod::get();
+        let start_height = <Runtime as democracy::Config>::VotingPeriod::get();
 
         // making a proposal to increase Eve's balance without having tokens staked fails
         let amount_to_fund = 100_000;
@@ -747,7 +747,7 @@ fn integration_test_proposal_vkint_gets_released_on_regular_launch() {
             start_vkint_carol - minimum_proposal_value
         );
 
-        DemocracyPallet::on_initialize(<Runtime as democracy::Config>::LaunchPeriod::get());
+        DemocracyPallet::on_initialize(<Runtime as democracy::Config>::VotingPeriod::get());
 
         // now that it's no longer a proposal, the deposit should be released
         assert_eq!(get_free_vkint(account_of(ALICE)), start_vkint_alice);