Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce and Implement VestedTransfer Trait #5630

Merged
merged 27 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
79671a4
introduce vested transfer trait
shawntabrizi Sep 6, 2024
d47c240
impl VestedTransfer to vesting pallet
shawntabrizi Sep 6, 2024
75da32e
fix test comment formatting
shawntabrizi Sep 6, 2024
7e825db
Create pr_5630.prdoc
shawntabrizi Sep 6, 2024
24b2832
better docs
shawntabrizi Sep 6, 2024
e784ecb
fix apis
shawntabrizi Sep 7, 2024
65ae2c2
Update substrate/frame/support/src/traits/tokens/currency/lockable.rs
shawntabrizi Sep 8, 2024
39be304
add transactional layer within vested transfer trait
shawntabrizi Sep 11, 2024
c690ab8
add tests
shawntabrizi Sep 11, 2024
011c6ae
Update lib.rs
shawntabrizi Sep 11, 2024
03efc85
Merge branch 'master' into shawntabrizi-vesting-treasury
shawntabrizi Sep 11, 2024
3760a94
fix benchmarks
shawntabrizi Sep 11, 2024
0843474
remove let _
shawntabrizi Sep 11, 2024
e7ac025
Merge branch 'master' into shawntabrizi-vesting-treasury
shawntabrizi Sep 11, 2024
cfe000f
Update substrate/frame/vesting/src/lib.rs
shawntabrizi Sep 14, 2024
2a66893
add back debug assert
shawntabrizi Sep 14, 2024
e5ecc12
Update substrate/frame/vesting/src/lib.rs
shawntabrizi Sep 14, 2024
57bdb07
Merge branch 'master' into shawntabrizi-vesting-treasury
bkchr Sep 18, 2024
27ce63b
Merge branch 'master' into shawntabrizi-vesting-treasury
shawntabrizi Sep 18, 2024
31724ac
Merge branch 'master' into shawntabrizi-vesting-treasury
shawntabrizi Oct 2, 2024
016b209
Merge branch 'master' into shawntabrizi-vesting-treasury
shawntabrizi Oct 2, 2024
2b83654
Merge branch 'master' into shawntabrizi-vesting-treasury
shawntabrizi Oct 3, 2024
a66f794
Update lockable.rs
shawntabrizi Oct 3, 2024
3a1cf6a
Merge branch 'master' into shawntabrizi-vesting-treasury
shawntabrizi Oct 3, 2024
5a8fcb9
Merge branch 'master' into shawntabrizi-vesting-treasury
shawntabrizi Oct 4, 2024
62a3676
Merge branch 'master' into shawntabrizi-vesting-treasury
shawntabrizi Oct 5, 2024
2b216c0
Merge branch 'master' into shawntabrizi-vesting-treasury
shawntabrizi Oct 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions prdoc/pr_5630.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json

title: Introduce and Implement the `VestedTransfer` Trait

doc:
- audience: Runtime Dev
description: |
This PR introduces a new trait `VestedTransfer` which is implemented by `pallet_vesting`. With this, other pallets can easily introduce vested transfers into their logic.

crates:
- name: frame-support
bump: minor
- name: pallet-vesting
bump: minor
3 changes: 2 additions & 1 deletion substrate/frame/support/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ pub mod tokens;
pub use tokens::{
currency::{
ActiveIssuanceOf, Currency, InspectLockableCurrency, LockIdentifier, LockableCurrency,
NamedReservableCurrency, ReservableCurrency, TotalIssuanceOf, VestingSchedule,
NamedReservableCurrency, ReservableCurrency, TotalIssuanceOf, VestedTransfer,
VestingSchedule,
},
fungible, fungibles,
imbalance::{Imbalance, OnUnbalanced, SignedImbalance},
Expand Down
4 changes: 3 additions & 1 deletion substrate/frame/support/src/traits/tokens/currency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ use sp_runtime::{traits::MaybeSerializeDeserialize, DispatchError};
mod reservable;
pub use reservable::{NamedReservableCurrency, ReservableCurrency};
mod lockable;
pub use lockable::{InspectLockableCurrency, LockIdentifier, LockableCurrency, VestingSchedule};
pub use lockable::{
InspectLockableCurrency, LockIdentifier, LockableCurrency, VestedTransfer, VestingSchedule,
};

/// Abstraction over a fungible assets system.
pub trait Currency<AccountId> {
Expand Down
49 changes: 49 additions & 0 deletions substrate/frame/support/src/traits/tokens/currency/lockable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,52 @@ pub trait VestingSchedule<AccountId> {
/// NOTE: This doesn't alter the free balance of the account.
fn remove_vesting_schedule(who: &AccountId, schedule_index: u32) -> DispatchResult;
}

/// A vested transfer over a currency. This allows a transferred amount to vest over time.
pub trait VestedTransfer<AccountId> {
/// The quantity used to denote time; usually just a `BlockNumber`.
type Moment;

/// The currency that this schedule applies to.
type Currency: Currency<AccountId>;
gui1117 marked this conversation as resolved.
Show resolved Hide resolved

// Execute a vested transfer from `source` to `target` with the given schedule:
// - `locked`: The amount to be transferred and for the vesting schedule to apply to.
// - `per_block`: The amount to be unlocked each block. (linear vesting)
// - `starting_block`: The block where the vesting should start. This block can be in the past
// or future, and should adjust when the tokens become available to the user.
//
// Example: Assume we are on block 100. If `locked` amount is 100, and `per_block` is 1:
// - If `starting_block` is 0, then the whole 100 tokens will be available right away as the
// vesting schedule started in the past and has fully completed.
// - If `starting_block` is 50, then 50 tokens are made available right away, and 50 more
// tokens will unlock one token at a time until block 150.
// - If `starting_block` is 100, then each block, 1 token will be unlocked until the whole
// balance is unlocked at block 200.
// - If `starting_block` is 200, then the 100 token balance will be completely locked until
// block 200, and then start to unlock one token at a time until block 300.
shawntabrizi marked this conversation as resolved.
Show resolved Hide resolved
fn vested_transfer(
source: &AccountId,
target: &AccountId,
locked: <Self::Currency as Currency<AccountId>>::Balance,
per_block: <Self::Currency as Currency<AccountId>>::Balance,
starting_block: Self::Moment,
) -> DispatchResult;
}

// An no-op implementation of `VestedTransfer` for pallets that require this trait, but users may
// not want to implement this functionality.
impl<AccountId> VestedTransfer<AccountId> for () {
type Moment = ();
type Currency = ();

fn vested_transfer(
_source: &AccountId,
_target: &AccountId,
_locked: <Self::Currency as Currency<AccountId>>::Balance,
_per_block: <Self::Currency as Currency<AccountId>>::Balance,
_starting_block: Self::Moment,
) -> DispatchResult {
Err(sp_runtime::DispatchError::Unavailable.into())
}
}
45 changes: 29 additions & 16 deletions substrate/frame/vesting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ use frame_support::{
ensure,
storage::bounded_vec::BoundedVec,
traits::{
Currency, ExistenceRequirement, Get, LockIdentifier, LockableCurrency, VestingSchedule,
WithdrawReasons,
Currency, ExistenceRequirement, Get, LockIdentifier, LockableCurrency, VestedTransfer,
VestingSchedule, WithdrawReasons,
},
weights::Weight,
};
Expand Down Expand Up @@ -534,32 +534,25 @@ impl<T: Config> Pallet<T> {
if !schedule.is_valid() {
return Err(Error::<T>::InvalidScheduleParams.into())
};

let target = T::Lookup::lookup(target)?;
let source = T::Lookup::lookup(source)?;

// Check we can add to this account prior to any storage writes.
Self::can_add_vesting_schedule(
&target,
schedule.locked(),
schedule.per_block(),
schedule.starting_block(),
)?;

T::Currency::transfer(
&source,
&target,
schedule.locked(),
ExistenceRequirement::AllowDeath,
)?;

// We can't let this fail because the currency transfer has already happened.
let res = Self::add_vesting_schedule(
// If adding this vesting schedule fails, all storage changes are undone due to FRAME's
// default transactional layers.
Self::add_vesting_schedule(
&target,
schedule.locked(),
schedule.per_block(),
schedule.starting_block(),
);
debug_assert!(res.is_ok(), "Failed to add a schedule when we had to succeed.");
shawntabrizi marked this conversation as resolved.
Show resolved Hide resolved
)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that everyone calling this will need to use transactional to revert the changes if there is an error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

everyone in frame uses transactional? what am I missing?

All code inside the runtime now assume we can error at any point in the code. when this was written, it wasn't the case, hence the need to "double check"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But VestedTransfer::vested_transfer function is not a pallet call. If someone implements an error recovery logic it can get messy I suppose.

Maybe it is standard practice now, but I would add a small comment on top of do_vesting_transfer that the storage is dirty in case the method becomes public in the future.
And in the doc of VestedTransfer::vested_transfer I would also write that the storage can be dirty when error is returned.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But VestedTransfer::vested_transfer function is not a pallet call. If someone implements an error recovery logic it can get messy I suppose.

Exactly. If someone handles the error, it will not be transactional. Or you do the transactional handling explicitly in the implementation of the trait.

Copy link
Member Author

@shawntabrizi shawntabrizi Sep 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I really didn't consider this. I have explicitly added a transactional layer to the trait implementation.

I feel there must be many traits in Polkadot SDK which are vulnerable to the same problem, right?

Copy link
Member Author

@shawntabrizi shawntabrizi Sep 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay i brought back this check, not looking to cause an issue in Polkadot due to small optimization.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that in general people should probably rarely recover from a dispatch error, and it would be simpler to require the caller to rollback.

Maybe we can make a transition with a new type: DispatchErrorDirty or DispatchErrorToRollback which means that the storage is dirty and should be rollbacked to handle the error manually.

Or maybe everybody already assume that a DispatchError can have a dirty storage.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah this kind of direction would be nice overall imo

shawntabrizi marked this conversation as resolved.
Show resolved Hide resolved

Ok(())
}
Expand Down Expand Up @@ -751,8 +744,7 @@ where
Ok(())
}

// Ensure we can call `add_vesting_schedule` without error. This should always
// be called prior to `add_vesting_schedule`.
/// Checks if `add_vesting_schedule` would work against `who`.
fn can_add_vesting_schedule(
who: &T::AccountId,
locked: BalanceOf<T>,
Expand Down Expand Up @@ -784,3 +776,24 @@ where
Ok(())
}
}

impl<T: Config> VestedTransfer<T::AccountId> for Pallet<T>
where
BalanceOf<T>: MaybeSerializeDeserialize + Debug,
{
type Currency = T::Currency;
type Moment = BlockNumberFor<T>;

fn vested_transfer(
source: &T::AccountId,
target: &T::AccountId,
locked: BalanceOf<T>,
per_block: BalanceOf<T>,
starting_block: BlockNumberFor<T>,
) -> DispatchResult {
let schedule = VestingInfo::new(locked, per_block, starting_block);
let source = <T::Lookup as StaticLookup>::unlookup(source.clone());
let target = <T::Lookup as StaticLookup>::unlookup(target.clone());
shawntabrizi marked this conversation as resolved.
Show resolved Hide resolved
Self::do_vested_transfer(source, target, schedule)
}
}
21 changes: 13 additions & 8 deletions substrate/frame/vesting/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,13 +280,14 @@ fn extra_balance_should_transfer() {
// Account 1 has only 5 units vested at block 1 (plus 150 unvested)
assert_eq!(Vesting::vesting_balance(&1), Some(45));
assert_ok!(Vesting::vest(Some(1).into()));
assert_ok!(Balances::transfer_allow_death(Some(1).into(), 3, 155)); // Account 1 can send extra units gained
// Account 1 can send extra units gained
assert_ok!(Balances::transfer_allow_death(Some(1).into(), 3, 155));

// Account 2 has no units vested at block 1, but gained 100
assert_eq!(Vesting::vesting_balance(&2), Some(200));
assert_ok!(Vesting::vest(Some(2).into()));
assert_ok!(Balances::transfer_allow_death(Some(2).into(), 3, 100)); // Account 2 can send extra
// units gained
// Account 2 can send extra units gained
assert_ok!(Balances::transfer_allow_death(Some(2).into(), 3, 100));
});
}

Expand All @@ -295,14 +296,16 @@ fn liquid_funds_should_transfer_with_delayed_vesting() {
ExtBuilder::default().existential_deposit(256).build().execute_with(|| {
let user12_free_balance = Balances::free_balance(&12);

assert_eq!(user12_free_balance, 2560); // Account 12 has free balance
// Account 12 has liquid funds
// Account 12 has free balance
assert_eq!(user12_free_balance, 2560);
// Account 12 has liquid funds
assert_eq!(Vesting::vesting_balance(&12), Some(user12_free_balance - 256 * 5));

// Account 12 has delayed vesting
let user12_vesting_schedule = VestingInfo::new(
256 * 5,
64, // Vesting over 20 blocks
// Vesting over 20 blocks
64,
10,
);
assert_eq!(VestingStorage::<Test>::get(&12).unwrap(), vec![user12_vesting_schedule]);
Expand Down Expand Up @@ -630,8 +633,10 @@ fn merge_ongoing_schedules() {

let sched1 = VestingInfo::new(
ED * 10,
ED, // Vest over 10 blocks.
sched0.starting_block() + 5, // Start at block 15.
// Vest over 10 blocks.
ED,
// Start at block 15.
sched0.starting_block() + 5,
);
assert_ok!(Vesting::vested_transfer(Some(4).into(), 2, sched1));
assert_eq!(VestingStorage::<Test>::get(&2).unwrap(), vec![sched0, sched1]);
gui1117 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Loading