Skip to content
crowdloan.rs 71.4 KiB
Newer Older
			amount: u64,
		) -> DispatchResult {
			let height = System::block_number();
			BIDS_PLACED.with(|p| {
				p.borrow_mut().push(BidPlaced {
					height,
					bidder,
					para,
					first_period,
					last_period,
					amount,
				})
			});
		fn lease_period_index(b: BlockNumber) -> Option<(u64, bool)> {
			let (lease_period_length, offset) = Self::lease_period_length();
			let b = b.checked_sub(offset)?;

			let lease_period = b / lease_period_length;
			let first_block = (b % lease_period_length).is_zero();
			Some((lease_period, first_block))
		fn lease_period_length() -> (u64, u64) {
			(20, 0)
		}

		fn has_won_an_auction(para: ParaId, bidder: &u64) -> bool {
			HAS_WON.with(|p| *p.borrow().get(&(para, *bidder)).unwrap_or(&false))
		}
	}

	parameter_types! {
		pub const SubmissionDeposit: u64 = 1;
		pub const MinContribution: u64 = 10;
		pub const CrowdloanPalletId: PalletId = PalletId(*b"py/cfund");
Shawn Tabrizi's avatar
Shawn Tabrizi committed
		pub const RemoveKeysLimit: u32 = 10;
		pub const MaxMemoLength: u8 = 32;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
		type Event = Event;
		type SubmissionDeposit = SubmissionDeposit;
		type MinContribution = MinContribution;
		type PalletId = CrowdloanPalletId;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
		type RemoveKeysLimit = RemoveKeysLimit;
		type Registrar = TestRegistrar<Test>;
		type Auctioneer = TestAuctioneer;
		type MaxMemoLength = MaxMemoLength;
		type WeightInfo = crate::crowdloan::TestWeightInfo;
	use pallet_balances::Error as BalancesError;

	// This function basically just builds a genesis storage key/value store according to
	// our desired mockup.
Shawn Tabrizi's avatar
Shawn Tabrizi committed
	pub fn new_test_ext() -> sp_io::TestExternalities {
		let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
		pallet_balances::GenesisConfig::<Test> {
			balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)],
		}
		.assimilate_storage(&mut t)
		.unwrap();
		let keystore = KeyStore::new();
		let mut t: sp_io::TestExternalities = t.into();
		t.register_extension(KeystoreExt(Arc::new(keystore)));
		t
	fn new_para() -> ParaId {
		for i in 0.. {
			let para: ParaId = i.into();
			if TestRegistrar::<Test>::is_registered(para) {
				continue
			}
			assert_ok!(TestRegistrar::<Test>::register(
				1,
				para,
				dummy_head_data(),
				dummy_validation_code()
			));
			return para
	fn run_to_block(n: u64) {
		while System::block_number() < n {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			Crowdloan::on_finalize(System::block_number());
			Balances::on_finalize(System::block_number());
			System::on_finalize(System::block_number());
			System::set_block_number(System::block_number() + 1);
			System::on_initialize(System::block_number());
			Balances::on_initialize(System::block_number());
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			Crowdloan::on_initialize(System::block_number());
	fn last_event() -> Event {
		System::events().pop().expect("Event expected").event
	}

	#[test]
	fn basic_setup_works() {
		new_test_ext().execute_with(|| {
			assert_eq!(System::block_number(), 0);
			assert_eq!(Crowdloan::funds(ParaId::from(0)), None);
			let empty: Vec<ParaId> = Vec::new();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			assert_eq!(Crowdloan::new_raise(), empty);
			assert_eq!(Crowdloan::contribution_get(0u32, &1).0, 0);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			assert_eq!(Crowdloan::endings_count(), 0);

			assert_ok!(TestAuctioneer::new_auction(5, 0));

			assert_eq!(bids(), vec![]);
			assert_ok!(TestAuctioneer::place_bid(1, 2.into(), 0, 3, 6));
			let b = BidPlaced {
				height: 0,
				bidder: 1,
				para: 2.into(),
				first_period: 0,
				last_period: 3,
				amount: 6,
			};
			assert_eq!(bids(), vec![b]);
			assert_eq!(TestAuctioneer::auction_status(4), AuctionStatus::<u64>::StartingPeriod);
			assert_eq!(TestAuctioneer::auction_status(5), AuctionStatus::<u64>::EndingPeriod(0, 0));
			assert_eq!(TestAuctioneer::auction_status(9), AuctionStatus::<u64>::EndingPeriod(4, 0));
			assert_eq!(TestAuctioneer::auction_status(11), AuctionStatus::<u64>::NotStarted);
		});
	}

	#[test]
	fn create_works() {
		new_test_ext().execute_with(|| {
			let para = new_para();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			// Now try to create a crowdloan campaign
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, None));
			// This is what the initial `fund_info` should look like
			let fund_info = FundInfo {
				depositor: 1,
				verifier: None,
				deposit: 1,
				raised: 0,
				// 5 blocks length + 3 block ending period + 1 starting block
				end: 9,
				cap: 1000,
				last_contribution: LastContribution::Never,
				first_period: 1,
				last_period: 4,
				trie_index: 0,
			assert_eq!(Crowdloan::funds(para), Some(fund_info));
			// User has deposit removed from their free balance
			assert_eq!(Balances::free_balance(1), 999);
			// Deposit is placed in reserved
			assert_eq!(Balances::reserved_balance(1), 1);
			// No new raise until first contribution
			let empty: Vec<ParaId> = Vec::new();
			assert_eq!(Crowdloan::new_raise(), empty);
		});
	}

	#[test]
	fn create_with_verifier_works() {
		new_test_ext().execute_with(|| {
			let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
			let para = new_para();
			// Now try to create a crowdloan campaign
			assert_ok!(Crowdloan::create(
				Origin::signed(1),
				para,
				1000,
				1,
				4,
				9,
				Some(pubkey.clone())
			));
			// This is what the initial `fund_info` should look like
			let fund_info = FundInfo {
				depositor: 1,
				verifier: Some(pubkey),
				deposit: 1,
				raised: 0,
				// 5 blocks length + 3 block ending period + 1 starting block
				end: 9,
				cap: 1000,
				last_contribution: LastContribution::Never,
				first_period: 1,
				last_period: 4,
				trie_index: 0,
			assert_eq!(Crowdloan::funds(ParaId::from(0)), Some(fund_info));
			// User has deposit removed from their free balance
			assert_eq!(Balances::free_balance(1), 999);
			// Deposit is placed in reserved
			assert_eq!(Balances::reserved_balance(1), 1);
			// No new raise until first contribution
			let empty: Vec<ParaId> = Vec::new();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			assert_eq!(Crowdloan::new_raise(), empty);
		});
	}

	#[test]
	fn create_handles_basic_errors() {
		new_test_ext().execute_with(|| {
			// Now try to create a crowdloan campaign
			let para = new_para();

			let e = Error::<Test>::InvalidParaId;
			assert_noop!(Crowdloan::create(Origin::signed(1), 1.into(), 1000, 1, 4, 9, None), e);
			// Cannot create a crowdloan with bad lease periods
			let e = Error::<Test>::LastPeriodBeforeFirstPeriod;
			assert_noop!(Crowdloan::create(Origin::signed(1), para, 1000, 4, 1, 9, None), e);
			let e = Error::<Test>::LastPeriodTooFarInFuture;
			assert_noop!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 9, 9, None), e);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			// Cannot create a crowdloan without some deposit funds
			assert_ok!(TestRegistrar::<Test>::register(
				1337,
				ParaId::from(1234),
				dummy_head_data(),
				dummy_validation_code()
			let e = BalancesError::<Test, _>::InsufficientBalance;
			assert_noop!(
				Crowdloan::create(Origin::signed(1337), ParaId::from(1234), 1000, 1, 3, 9, None),
				e
			);

			// Cannot create a crowdloan with nonsense end date
			// This crowdloan would end in lease period 2, but is bidding for some slot that starts in lease period 1.
			assert_noop!(
				Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 41, None),
				Error::<Test>::EndTooFarInFuture
			);
		});
	}

	#[test]
	fn contribute_works() {
		new_test_ext().execute_with(|| {
			let para = new_para();

Shawn Tabrizi's avatar
Shawn Tabrizi committed
			// Set up a crowdloan
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, None));

			// No contributions yet
			assert_eq!(Crowdloan::contribution_get(u32::from(para), &1).0, 0);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			// User 1 contributes to their own crowdloan
			assert_ok!(Crowdloan::contribute(Origin::signed(1), para, 49, None));
			// User 1 has spent some funds to do this, transfer fees **are** taken
Gavin Wood's avatar
Gavin Wood committed
			assert_eq!(Balances::free_balance(1), 950);
			// Contributions are stored in the trie
			assert_eq!(Crowdloan::contribution_get(u32::from(para), &1).0, 49);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			// Contributions appear in free balance of crowdloan
			assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(para)), 49);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			// Crowdloan is added to NewRaise
			assert_eq!(Crowdloan::new_raise(), vec![para]);
			let fund = Crowdloan::funds(para).unwrap();

			// Last contribution time recorded
			assert_eq!(fund.last_contribution, LastContribution::PreEnding(0));
			assert_eq!(fund.raised, 49);
		});
	}

	#[test]
	fn contribute_with_verifier_works() {
		new_test_ext().execute_with(|| {
			let para = new_para();
			let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
			// Set up a crowdloan
			assert_ok!(Crowdloan::create(
				Origin::signed(1),
				para,
				1000,
				1,
				4,
				9,
				Some(pubkey.clone())
			));

			// No contributions yet
			assert_eq!(Crowdloan::contribution_get(u32::from(para), &1).0, 0);
			assert_noop!(
				Crowdloan::contribute(Origin::signed(1), para, 49, None),
				Error::<Test>::InvalidSignature
			);

			let payload = (0u32, 1u64, 0u64, 49u64);
			let valid_signature =
				crypto::create_ed25519_signature(&payload.encode(), pubkey.clone());
			let invalid_signature = MultiSignature::default();

			// Invalid signature
			assert_noop!(
				Crowdloan::contribute(Origin::signed(1), para, 49, Some(invalid_signature)),
				Error::<Test>::InvalidSignature
			);

			// Valid signature wrong parameter
			assert_noop!(
				Crowdloan::contribute(Origin::signed(1), para, 50, Some(valid_signature.clone())),
				Error::<Test>::InvalidSignature
			);
			assert_noop!(
				Crowdloan::contribute(Origin::signed(2), para, 49, Some(valid_signature.clone())),
				Error::<Test>::InvalidSignature
			);
			assert_ok!(Crowdloan::contribute(
				Origin::signed(1),
				para,
				49,
				Some(valid_signature.clone())
			));

			// Reuse valid signature
			assert_noop!(
				Crowdloan::contribute(Origin::signed(1), para, 49, Some(valid_signature)),
				Error::<Test>::InvalidSignature
			);

			let payload_2 = (0u32, 1u64, 49u64, 10u64);
			let valid_signature_2 = crypto::create_ed25519_signature(&payload_2.encode(), pubkey);

			// New valid signature
			assert_ok!(Crowdloan::contribute(Origin::signed(1), para, 10, Some(valid_signature_2)));

			// Contributions appear in free balance of crowdloan
			assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(para)), 59);

			// Contribution amount is correct
			let fund = Crowdloan::funds(para).unwrap();
			assert_eq!(fund.raised, 59);
		});
	}

	#[test]
	fn contribute_handles_basic_errors() {
		new_test_ext().execute_with(|| {
			let para = new_para();

			// Cannot contribute to non-existing fund
			assert_noop!(
				Crowdloan::contribute(Origin::signed(1), para, 49, None),
				Error::<Test>::InvalidParaId
			);
			// Cannot contribute below minimum contribution
			assert_noop!(
				Crowdloan::contribute(Origin::signed(1), para, 9, None),
				Error::<Test>::ContributionTooSmall
			);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			// Set up a crowdloan
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(1), para, 101, None));

			// Cannot contribute past the limit
			assert_noop!(
				Crowdloan::contribute(Origin::signed(2), para, 900, None),
				Error::<Test>::CapExceeded
			);

			// Move past end date
			run_to_block(10);

			// Cannot contribute to ended fund
			assert_noop!(
				Crowdloan::contribute(Origin::signed(1), para, 49, None),
				Error::<Test>::ContributionPeriodOver
			);

			// If a crowdloan has already won, it should not allow contributions.
			let para_2 = new_para();
			assert_ok!(Crowdloan::create(Origin::signed(1), para_2, 1000, 1, 4, 40, None));
			// Emulate a win by leasing out and putting a deposit. Slots pallet would normally do this.
			let crowdloan_account = Crowdloan::fund_account_id(para_2);
			set_winner(para_2, crowdloan_account, true);
			assert_noop!(
				Crowdloan::contribute(Origin::signed(1), para_2, 49, None),
				Error::<Test>::BidOrLeaseActive
			);

			// Move past lease period 1, should not be allowed to have further contributions with a crowdloan
			// that has starting period 1.
			let para_3 = new_para();
			assert_ok!(Crowdloan::create(Origin::signed(1), para_3, 1000, 1, 4, 40, None));
			run_to_block(40);
			let now = System::block_number();
			assert_eq!(TestAuctioneer::lease_period_index(now).unwrap().0, 2);
			assert_noop!(
				Crowdloan::contribute(Origin::signed(1), para_3, 49, None),
				Error::<Test>::ContributionPeriodOver
			);
	fn cannot_contribute_during_vrf() {
		new_test_ext().execute_with(|| {
			let para = new_para();
			let first_period = 1;
			let last_period = 4;

			assert_ok!(TestAuctioneer::new_auction(5, 0));
			assert_ok!(Crowdloan::create(
				Origin::signed(1),
				para,
				1000,
				first_period,
				last_period,
				20,
				None
			));

			run_to_block(8);
			// Can def contribute when auction is running.
			assert!(TestAuctioneer::auction_status(System::block_number()).is_ending().is_some());
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 250, None));

			run_to_block(10);
			// Can't contribute when auction is in the VRF delay period.
			assert!(TestAuctioneer::auction_status(System::block_number()).is_vrf());
			assert_noop!(
				Crowdloan::contribute(Origin::signed(2), para, 250, None),
				Error::<Test>::VrfDelayInProgress
			);

			run_to_block(15);
			// Its fine to contribute when no auction is running.
			assert!(!TestAuctioneer::auction_status(System::block_number()).is_in_progress());
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 250, None));
		})
	}

	#[test]
	fn bidding_works() {
		new_test_ext().execute_with(|| {
			let para = new_para();
			let first_period = 1;
			let last_period = 4;
			assert_ok!(TestAuctioneer::new_auction(5, 0));
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			// Set up a crowdloan
			assert_ok!(Crowdloan::create(
				Origin::signed(1),
				para,
				1000,
				first_period,
				last_period,
				9,
				None
			));
			let bidder = Crowdloan::fund_account_id(para);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			// Fund crowdloan
			run_to_block(1);
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None));
			run_to_block(3);
			assert_ok!(Crowdloan::contribute(Origin::signed(3), para, 150, None));
			run_to_block(5);
			assert_ok!(Crowdloan::contribute(Origin::signed(4), para, 200, None));
			run_to_block(8);
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 250, None));
			run_to_block(10);

			assert_eq!(
				bids(),
				vec![
					BidPlaced { height: 5, amount: 250, bidder, para, first_period, last_period },
					BidPlaced { height: 6, amount: 450, bidder, para, first_period, last_period },
					BidPlaced { height: 9, amount: 700, bidder, para, first_period, last_period },
				]
			);
			// Endings count incremented
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			assert_eq!(Crowdloan::endings_count(), 1);
	fn withdraw_from_failed_works() {
		new_test_ext().execute_with(|| {
			let para = new_para();
			// Set up a crowdloan
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 1, 9, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(3), para, 50, None));

			run_to_block(10);
			let account_id = Crowdloan::fund_account_id(para);
			// para has no reserved funds, indicating it did not win the auction.
			assert_eq!(Balances::reserved_balance(&account_id), 0);
			// but there's still the funds in its balance.
			assert_eq!(Balances::free_balance(&account_id), 150);
			assert_eq!(Balances::free_balance(2), 1900);
			assert_eq!(Balances::free_balance(3), 2950);

			assert_ok!(Crowdloan::withdraw(Origin::signed(2), 2, para));
			assert_eq!(Balances::free_balance(&account_id), 50);
			assert_eq!(Balances::free_balance(2), 2000);
			assert_ok!(Crowdloan::withdraw(Origin::signed(2), 3, para));
			assert_eq!(Balances::free_balance(&account_id), 0);
			assert_eq!(Balances::free_balance(3), 3000);
	#[test]
	fn withdraw_cannot_be_griefed() {
		new_test_ext().execute_with(|| {
			let para = new_para();

			// Set up a crowdloan
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 1, 9, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None));

			run_to_block(10);
			let account_id = Crowdloan::fund_account_id(para);

			// user sends the crowdloan funds trying to make an accounting error
			assert_ok!(Balances::transfer(Origin::signed(1), account_id, 10));

			// overfunded now
			assert_eq!(Balances::free_balance(&account_id), 110);
			assert_eq!(Balances::free_balance(2), 1900);

			assert_ok!(Crowdloan::withdraw(Origin::signed(2), 2, para));
			assert_eq!(Balances::free_balance(2), 2000);

			// Some funds are left over
			assert_eq!(Balances::free_balance(&account_id), 10);
			// They wil be left in the account at the end
			assert_ok!(Crowdloan::dissolve(Origin::signed(1), para));
			assert_eq!(Balances::free_balance(&account_id), 10);
		});
	}

	#[test]
	fn refund_works() {
		new_test_ext().execute_with(|| {
			let para = new_para();
			let account_id = Crowdloan::fund_account_id(para);

			// Set up a crowdloan ending on 9
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 1, 9, None));
			// Make some contributions
			assert_ok!(Crowdloan::contribute(Origin::signed(1), para, 100, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 200, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(3), para, 300, None));

			assert_eq!(Balances::free_balance(account_id), 600);

			// Can't refund before the crowdloan it has ended
			assert_noop!(
				Crowdloan::refund(Origin::signed(1337), para),
				Error::<Test>::FundNotEnded,
			);

			// Move to the end of the crowdloan
			run_to_block(10);
			assert_ok!(Crowdloan::refund(Origin::signed(1337), para));

			// Funds are returned
			assert_eq!(Balances::free_balance(account_id), 0);
			// 1 deposit for the crowdloan which hasn't dissolved yet.
			assert_eq!(Balances::free_balance(1), 1000 - 1);
			assert_eq!(Balances::free_balance(2), 2000);
			assert_eq!(Balances::free_balance(3), 3000);
	fn multiple_refund_works() {
		new_test_ext().execute_with(|| {
			let para = new_para();
			let account_id = Crowdloan::fund_account_id(para);

			// Set up a crowdloan ending on 9
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 100000, 1, 1, 9, None));
			// Make more contributions than our limit
			for i in 1..=RemoveKeysLimit::get() * 2 {
				Balances::make_free_balance_be(&i.into(), (1000 * i).into());
				assert_ok!(Crowdloan::contribute(
					Origin::signed(i.into()),
					para,
					(i * 100).into(),
					None
				));
			}

			assert_eq!(Balances::free_balance(account_id), 21000);

			// Move to the end of the crowdloan
			run_to_block(10);
			assert_ok!(Crowdloan::refund(Origin::signed(1337), para));
			assert_eq!(last_event(), super::Event::<Test>::PartiallyRefunded(para).into());

			// Funds still left over
			assert!(!Balances::free_balance(account_id).is_zero());

			// Call again
			assert_ok!(Crowdloan::refund(Origin::signed(1337), para));
			assert_eq!(last_event(), super::Event::<Test>::AllRefunded(para).into());

			// Funds are returned
			assert_eq!(Balances::free_balance(account_id), 0);
			// 1 deposit for the crowdloan which hasn't dissolved yet.
			for i in 1..=RemoveKeysLimit::get() * 2 {
				assert_eq!(Balances::free_balance(&i.into()), i as u64 * 1000);
			}
		});
	}

	#[test]
	fn refund_and_dissolve_works() {
		new_test_ext().execute_with(|| {
			let para = new_para();
			let issuance = Balances::total_issuance();
			// Set up a crowdloan
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 1, 9, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(3), para, 50, None));

			run_to_block(10);
			// All funds are refunded
			assert_ok!(Crowdloan::refund(Origin::signed(2), para));
			// Now that `fund.raised` is zero, it can be dissolved.
			assert_ok!(Crowdloan::dissolve(Origin::signed(1), para));
			assert_eq!(Balances::free_balance(1), 1000);
			assert_eq!(Balances::free_balance(2), 2000);
			assert_eq!(Balances::free_balance(3), 3000);
			assert_eq!(Balances::total_issuance(), issuance);
		new_test_ext().execute_with(|| {
			let para = new_para();
			let issuance = Balances::total_issuance();
			// Set up a crowdloan
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 1, 9, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(3), para, 50, None));
			// Can't dissolve before it ends
			assert_noop!(
				Crowdloan::dissolve(Origin::signed(1), para),
				Error::<Test>::NotReadyToDissolve
			);
			run_to_block(10);
			set_winner(para, 1, true);
			// Can't dissolve when it won.
			assert_noop!(
				Crowdloan::dissolve(Origin::signed(1), para),
				Error::<Test>::NotReadyToDissolve
			);
			set_winner(para, 1, false);
			// Can't dissolve while it still has user funds
			assert_noop!(
				Crowdloan::dissolve(Origin::signed(1), para),
				Error::<Test>::NotReadyToDissolve
			);
			// All funds are refunded
			assert_ok!(Crowdloan::refund(Origin::signed(2), para));

			// Now that `fund.raised` is zero, it can be dissolved.
			assert_ok!(Crowdloan::dissolve(Origin::signed(1), para));
			assert_eq!(Balances::free_balance(1), 1000);
			assert_eq!(Balances::free_balance(2), 2000);
			assert_eq!(Balances::free_balance(3), 3000);
			assert_eq!(Balances::total_issuance(), issuance);
	fn withdraw_from_finished_works() {
		new_test_ext().execute_with(|| {
			let para = new_para();
			let account_id = Crowdloan::fund_account_id(para);

			// Set up a crowdloan
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 1, 9, None));

			// Fund crowdloans.
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(3), para, 50, None));
			// simulate the reserving of para's funds. this actually happens in the Slots pallet.
			assert_ok!(Balances::reserve(&account_id, 150));

			run_to_block(19);
			assert_noop!(
				Crowdloan::withdraw(Origin::signed(2), 2, para),
				Error::<Test>::BidOrLeaseActive
			);
			run_to_block(20);
			// simulate the unreserving of para's funds, now that the lease expired. this actually
			// happens in the Slots pallet.
			Balances::unreserve(&account_id, 150);

			// para has no reserved funds, indicating it did ot win the auction.
			assert_eq!(Balances::reserved_balance(&account_id), 0);
			// but there's still the funds in its balance.
			assert_eq!(Balances::free_balance(&account_id), 150);
			assert_eq!(Balances::free_balance(2), 1900);
			assert_eq!(Balances::free_balance(3), 2950);

			assert_ok!(Crowdloan::withdraw(Origin::signed(2), 2, para));
			assert_eq!(Balances::free_balance(&account_id), 50);
Gavin Wood's avatar
Gavin Wood committed
			assert_eq!(Balances::free_balance(2), 2000);
			assert_ok!(Crowdloan::withdraw(Origin::signed(2), 3, para));
			assert_eq!(Balances::free_balance(&account_id), 0);
Gavin Wood's avatar
Gavin Wood committed
			assert_eq!(Balances::free_balance(3), 3000);
	fn on_swap_works() {
		new_test_ext().execute_with(|| {
			let para_1 = new_para();
			let para_2 = new_para();

			// Set up crowdloans
			assert_ok!(Crowdloan::create(Origin::signed(1), para_1, 1000, 1, 1, 9, None));
			assert_ok!(Crowdloan::create(Origin::signed(1), para_2, 1000, 1, 1, 9, None));
			// Different contributions
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para_1, 100, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(3), para_2, 50, None));
			// Original state
			assert_eq!(Funds::<Test>::get(para_1).unwrap().raised, 100);
			assert_eq!(Funds::<Test>::get(para_2).unwrap().raised, 50);
			// Swap
			Crowdloan::on_swap(para_1, para_2);
			// Final state
			assert_eq!(Funds::<Test>::get(para_2).unwrap().raised, 100);
			assert_eq!(Funds::<Test>::get(para_1).unwrap().raised, 50);
	fn cannot_create_fund_when_already_active() {
		new_test_ext().execute_with(|| {
			let para_1 = new_para();
			assert_ok!(Crowdloan::create(Origin::signed(1), para_1, 1000, 1, 1, 9, None));
			// Cannot create a fund again
			assert_noop!(
				Crowdloan::create(Origin::signed(1), para_1, 1000, 1, 1, 9, None),
				Error::<Test>::FundNotEnded,
			);

	#[test]
	fn edit_works() {
		new_test_ext().execute_with(|| {
			let para_1 = new_para();

			assert_ok!(Crowdloan::create(Origin::signed(1), para_1, 1000, 1, 1, 9, None));
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para_1, 100, None));
			let old_crowdloan = Crowdloan::funds(para_1).unwrap();

			assert_ok!(Crowdloan::edit(Origin::root(), para_1, 1234, 2, 3, 4, None));
			let new_crowdloan = Crowdloan::funds(para_1).unwrap();

			// Some things stay the same
			assert_eq!(old_crowdloan.depositor, new_crowdloan.depositor);
			assert_eq!(old_crowdloan.deposit, new_crowdloan.deposit);
			assert_eq!(old_crowdloan.raised, new_crowdloan.raised);

			// Some things change
			assert!(old_crowdloan.cap != new_crowdloan.cap);
			assert!(old_crowdloan.first_period != new_crowdloan.first_period);
			assert!(old_crowdloan.last_period != new_crowdloan.last_period);

	#[test]
	fn add_memo_works() {
		new_test_ext().execute_with(|| {
			let para_1 = new_para();

			assert_ok!(Crowdloan::create(Origin::signed(1), para_1, 1000, 1, 1, 9, None));
			// Cant add a memo before you have contributed.
			assert_noop!(
				Crowdloan::add_memo(Origin::signed(1), para_1, b"hello, world".to_vec()),
				Error::<Test>::NoContributions,
			);
			// Make a contribution. Initially no memo.
			assert_ok!(Crowdloan::contribute(Origin::signed(1), para_1, 100, None));
			assert_eq!(Crowdloan::contribution_get(0u32, &1), (100, vec![]));
			// Can't place a memo that is too large.
			assert_noop!(
				Crowdloan::add_memo(Origin::signed(1), para_1, vec![123; 123]),
				Error::<Test>::MemoTooLarge,
			);
			// Adding a memo to an existing contribution works
			assert_ok!(Crowdloan::add_memo(Origin::signed(1), para_1, b"hello, world".to_vec()));
			assert_eq!(Crowdloan::contribution_get(0u32, &1), (100, b"hello, world".to_vec()));
			// Can contribute again and data persists
			assert_ok!(Crowdloan::contribute(Origin::signed(1), para_1, 100, None));
			assert_eq!(Crowdloan::contribution_get(0u32, &1), (200, b"hello, world".to_vec()));
		});
	}

	#[test]
	fn poke_works() {
		new_test_ext().execute_with(|| {
			let para_1 = new_para();

			assert_ok!(TestAuctioneer::new_auction(5, 0));
			assert_ok!(Crowdloan::create(Origin::signed(1), para_1, 1000, 1, 1, 9, None));
			// Should fail when no contributions.
			assert_noop!(
				Crowdloan::poke(Origin::signed(1), para_1),
				Error::<Test>::NoContributions
			);
			assert_ok!(Crowdloan::contribute(Origin::signed(2), para_1, 100, None));
			run_to_block(6);
			assert_ok!(Crowdloan::poke(Origin::signed(1), para_1));
			assert_eq!(Crowdloan::new_raise(), vec![para_1]);
			assert_noop!(
				Crowdloan::poke(Origin::signed(1), para_1),
				Error::<Test>::AlreadyInNewRaise
			);
		});
	}
Shawn Tabrizi's avatar
Shawn Tabrizi committed

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking {
	use super::{Pallet as Crowdloan, *};
	use frame_support::{assert_ok, traits::OnInitialize};
Shawn Tabrizi's avatar
Shawn Tabrizi committed
	use frame_system::RawOrigin;
	use sp_runtime::traits::{Bounded, CheckedSub};
Shawn Tabrizi's avatar
Shawn Tabrizi committed
	use sp_std::prelude::*;

	use frame_benchmarking::{account, benchmarks, whitelisted_caller};
Shawn Tabrizi's avatar
Shawn Tabrizi committed

	fn assert_last_event<T: Config>(generic_event: <T as Config>::Event) {
		let events = frame_system::Pallet::<T>::events();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
		let system_event: <T as frame_system::Config>::Event = generic_event.into();
		// compare to the last event record
		let frame_system::EventRecord { event, .. } = &events[events.len() - 1];
		assert_eq!(event, &system_event);
	}

	fn create_fund<T: Config>(id: u32, end: T::BlockNumber) -> ParaId {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
		let cap = BalanceOf::<T>::max_value();
		let (_, offset) = T::Auctioneer::lease_period_length();
		// Set to the very beginning of lease period index 0.
		frame_system::Pallet::<T>::set_block_number(offset);
		let now = frame_system::Pallet::<T>::block_number();
		let (lease_period_index, _) = T::Auctioneer::lease_period_index(now).unwrap_or_default();
		let first_period = lease_period_index;
		let last_period =
			lease_period_index + ((SlotRange::LEASE_PERIODS_PER_SLOT as u32) - 1).into();
		let para_id = id.into();
		let caller = account("fund_creator", id, 0);
		CurrencyOf::<T>::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
		// Assume ed25519 is most complex signature format
		let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());

		let head_data = T::Registrar::worst_head_data();
		let validation_code = T::Registrar::worst_validation_code();
		assert_ok!(T::Registrar::register(caller.clone(), para_id, head_data, validation_code));
		T::Registrar::execute_pending_transitions();

		assert_ok!(Crowdloan::<T>::create(
			RawOrigin::Signed(caller).into(),
			para_id,
			cap,
			first_period,
			last_period,
	fn contribute_fund<T: Config>(who: &T::AccountId, index: ParaId) {
		CurrencyOf::<T>::make_free_balance_be(&who, BalanceOf::<T>::max_value());
Shawn Tabrizi's avatar
Shawn Tabrizi committed
		let value = T::MinContribution::get();

		let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
		let payload = (index, &who, BalanceOf::<T>::default(), value);
		let sig = crypto::create_ed25519_signature(&payload.encode(), pubkey);

		assert_ok!(Crowdloan::<T>::contribute(
			RawOrigin::Signed(who.clone()).into(),
			index,
			value,
			Some(sig)
		));
Shawn Tabrizi's avatar
Shawn Tabrizi committed
	}

	benchmarks! {
		create {
			let para_id = ParaId::from(1);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			let cap = BalanceOf::<T>::max_value();
			let first_period = 0u32.into();
			let last_period = 3u32.into();
			let (lpl, offset) = T::Auctioneer::lease_period_length();
			let end = lpl + offset;
Shawn Tabrizi's avatar
Shawn Tabrizi committed

			let caller: T::AccountId = whitelisted_caller();
			let head_data = T::Registrar::worst_head_data();
			let validation_code = T::Registrar::worst_validation_code();

			let verifier = account("verifier", 0, 0);
			CurrencyOf::<T>::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
			T::Registrar::register(caller.clone(), para_id, head_data, validation_code)?;
			T::Registrar::execute_pending_transitions();
		}: _(RawOrigin::Signed(caller), para_id, cap, first_period, last_period, end, Some(verifier))
Shawn Tabrizi's avatar
Shawn Tabrizi committed
		verify {
			assert_last_event::<T>(Event::<T>::Created(para_id).into())
Shawn Tabrizi's avatar
Shawn Tabrizi committed
		}

		// Contribute has two arms: PreEnding and Ending, but both are equal complexity.
		contribute {
			let (lpl, offset) = T::Auctioneer::lease_period_length();
			let end = lpl + offset;
			let fund_index = create_fund::<T>(1, end);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			let caller: T::AccountId = whitelisted_caller();
			let contribution = T::MinContribution::get();
			CurrencyOf::<T>::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
			assert!(NewRaise::<T>::get().is_empty());

			let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
			let payload = (fund_index, &caller, BalanceOf::<T>::default(), contribution);
			let sig = crypto::create_ed25519_signature(&payload.encode(), pubkey);

		}: _(RawOrigin::Signed(caller.clone()), fund_index, contribution, Some(sig))
Shawn Tabrizi's avatar
Shawn Tabrizi committed
		verify {
			// NewRaise is appended to, so we don't need to fill it up for worst case scenario.
			assert!(!NewRaise::<T>::get().is_empty());
			assert_last_event::<T>(Event::<T>::Contributed(caller, fund_index, contribution).into());
			let (lpl, offset) = T::Auctioneer::lease_period_length();
			let end = lpl + offset;
			let fund_index = create_fund::<T>(1337, end);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
			let caller: T::AccountId = whitelisted_caller();
			let contributor = account("contributor", 0, 0);
			contribute_fund::<T>(&contributor, fund_index);
			frame_system::Pallet::<T>::set_block_number(200u32.into());
Shawn Tabrizi's avatar
Shawn Tabrizi committed
		}: _(RawOrigin::Signed(caller), contributor.clone(), fund_index)
		verify {
			assert_last_event::<T>(Event::<T>::Withdrew(contributor, fund_index, T::MinContribution::get()).into());
		// Worst case: Refund removes `RemoveKeysLimit` keys, and is fully refunded.
			let k in 0 .. T::RemoveKeysLimit::get();
			let (lpl, offset) = T::Auctioneer::lease_period_length();
			let end = lpl + offset;
			let fund_index = create_fund::<T>(1337, end);
Shawn Tabrizi's avatar
Shawn Tabrizi committed

			// Dissolve will remove at most `RemoveKeysLimit` at once.
			for i in 0 .. k {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
				contribute_fund::<T>(&account("contributor", i, 0), fund_index);
			}

			let caller: T::AccountId = whitelisted_caller();
			frame_system::Pallet::<T>::set_block_number(200u32.into());
		}: _(RawOrigin::Signed(caller), fund_index)
		verify {
			assert_last_event::<T>(Event::<T>::AllRefunded(fund_index).into());