Verify First, Write Last

NOTE: As of Polkadot v0.9.25, the Verify First, Write Last practice is no longer required. However, since older versions are still vulnerable and because it is still best practice, it is worth discussing the "Verify First, Write Last" idiom.

Substrate does not cache state prior to extrinsic dispatch. Instead, state changes are made as they are invoked. This is in contrast to a transaction in Ethereum where, if the transaction reverts, no state changes will persist. In the case of Substrate, if a state change is made and then the dispatch throws a DispatchError, the original state change will persist. This unique behavior has led to the "Verify First, Write Last" practice.

#![allow(unused)]
fn main() {
{
  // all checks and throwing code go here

  // ** no throwing code below this line **

  // all event emissions & storage writes go here
}
}

Example

In the pallet-verify-first pallet, the init dispatchable is used to set up the TotalSupply of the token and transfer them to the sender. init should be only called once. Thus, the Init boolean is set to true when it is called initially. If init is called more than once, the transaction will throw an error because Init is already true.

#![allow(unused)]
fn main() {
/// Initialize the token
/// Transfers the total_supply amount to the caller
/// If init() has already been called, throw AlreadyInitialized error
#[pallet::weight(10_000)]
pub fn init(
    origin: OriginFor<T>,
    supply: u64
) -> DispatchResultWithPostInfo {
    let sender = ensure_signed(origin)?;

    if supply > 0 {
        <TotalSupply<T>>::put(&supply);
    }
    // Set sender's balance to total_supply()
    <BalanceToAccount<T>>::insert(&sender, supply);

    // Revert above changes if init() has already been called
    ensure!(!Self::is_init(), <Error<T>>::AlreadyInitialized);

    // Set Init StorageValue to `true`
    Init::<T>::put(true);

    // Emit event
    Self::deposit_event(Event::Initialized(sender));

    Ok(().into())
}
}

However, notice that the setting of TotalSupply and the transfer of funds happens before the check on Init. This violates the "Verify First, Write Last" practice. In an older version of Substrate, this would allow a malicious sender to call init multiple times and change the value of TotalSupply and their balance of the token.

Mitigations

  • Follow the "Verify First, Write Last" practice by doing all the necessary data validation before performing state changes and emitting events.

References

  • https://docs.substrate.io/main-docs/build/runtime-storage/#best-practices