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

fee: v0 support for multi-asset fees #4320

Merged
merged 2 commits into from
May 6, 2024
Merged

fee: v0 support for multi-asset fees #4320

merged 2 commits into from
May 6, 2024

Conversation

hdevalence
Copy link
Member

@hdevalence hdevalence commented May 5, 2024

Describe your changes

This commit implements a v0 of multi-asset fees (on the protocol side).
Multi-asset fees are important because they allows users who send tokens into
Penumbra over IBC to start using the chain immediately.

It does this by extending the GasPrices struct to include the asset ID of the
fee token the prices are for. This allows natural handling of multi-asset fees
by just having multiple sets of GasPrices.

In the past, we've laid the groundwork for this (by allowing other assets in
the Fee structure, even if it defaults to the staking token) but haven't
prioritized it, due to concerns about some of the details of the
implementation, particularly:

A: How does the chain know that the prices of non-native fee tokens are correct?
B: What happens to the non-native fee tokens?
C: What are the privacy implications for users paying with non-native fee tokens?

This design punts on (A) by only allowing non-native fee tokens with prices
explicitly specified using governance. In the future, Penumbra could own DEX as
a price oracle, but this would be better to do after it's live and we have real
data about it, rather than worrying about edge cases now (e.g., if someone can
temporarily manipulate a price for their own trades, that's not a big deal in
itself, but hooking it to the fee mechanism used for resource usage could make
it one).

The approach to (C) is that these explicitly specified prices should be
substantially higher (10x) than those of the native token. This means that
regardless of price variability, it should remain the case that the native fee
token is cheaper, so that the majority of users will use the same fee token and
their transactions will not be distinguishable. On the other hand, letting it
be possible to use non native fee tokens means that users who send IBC tokens
into Penumbra can access the protocol, and even if one of their first acts is
to swap some de minimis amount of the native token, they can do that within the
system.

This implementation currently does not properly handle (B), in that it silently
burns all fees, regardless of token. It is not appropriate to burn any tokens
other than the native token, whose supply is managed by the chain. Instead, the
transaction-level fee check should move into check_and_execute, and call a
method on the fee component that records the fee amount in the object store. At
the end of the block, all non-native fees should be swapped into the native
token and then burned. Emitted events should record:

  • an EventFee for each transaction, with the transaction's gas cost, the gas
    prices used to compute the base fee, the base fee, and the actual fee;
  • an EventFeeBurn at the end of the block, with the total amount of native
    fees burned as well as, for each alt fee token used to pay fees in that
    block, how much of the alt token was paid and how much of the native token it
    was traded for.

Client support is still mostly missing, though I have manually tested this on a
local devnet, by

  1. Creating the devnet (with a short voting period, configured with pd testnet generate)
  2. Executing a parameter change proposal to set dexParams.fixedAltGasPrices to "[{\"assetId\":{\"altBaseDenom\":\"ugm\"},\"blockSpacePrice\":\"80000\",\"compactBlockSpacePrice\":\"80000\",\"verificationPrice\":\"80000\",\"executionPrice\":\"80000\"}]"
  3. Hardcoding at the top of TxCmd::exec a change to the gas_prices to add the gm asset ID and multiplying all the prices by 10.

To properly support clients, we need to

  • Record the gas_prices data for alt fee tokens that is newly included in the
    CompactBlock in the view server's storage (probably in a new gas_prices
    table that maps a fee token asset ID to the latest price);

  • Use that data to populate the view server's gas prices RPC response (instead
    of the current Vec::new stub that reports any alt fee tokens as
    non-existing);

  • Change the pcli tx command so that the existing --fee-tier and a new
    --fee-token parameters move to pcli tx, and apply to all subcommands
    (i.e., pcli tx --fee-tier ... --fee-token gm send ...) so that TxCmd::exec
    can pull the right GasPrices out of the RPC response.

Checklist before requesting a review

  • If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason:

    this is consensus-breaking, but not state-breaking

Base automatically changed from gov-refactor to main May 5, 2024 17:42
@TalDerei
Copy link
Collaborator

TalDerei commented May 5, 2024

references #4313

@hdevalence hdevalence added consensus-breaking breaking change to execution of on-chain data A-fees Area: Relates to transaction fees labels May 6, 2024
@hdevalence hdevalence marked this pull request as ready for review May 6, 2024 06:08
Comment on lines +21 to +36
// When we implement dynamic gas pricing, we will want
// to read the prices we computed. But until then, we need to
// read these from the _fee params_ instead, since those are
// the values that will get updated by governance.
let params = self.get_fee_params().await?;
Ok(params.fixed_gas_prices)
}

/// Gets the current gas prices for alternative fee tokens.
async fn get_alt_gas_prices(&self) -> Result<Vec<GasPrices>> {
// When we implement dynamic gas pricing, we will want
// to read the prices we computed. But until then, we need to
// read these from the _fee params_ instead, since those are
// the values that will get updated by governance.
let params = self.get_fee_params().await?;
Ok(params.fixed_alt_gas_prices)
Copy link
Member Author

Choose a reason for hiding this comment

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

This fixes a critical bug in the original fee component logic, which would prevent fees from ever being changed post-genesis.

@hdevalence hdevalence changed the title wip: multi-asset fees fee: v0 support for multi-asset fees May 6, 2024
@erwanor erwanor self-requested a review May 6, 2024 14:24
@erwanor erwanor added this to the Sprint 6 milestone May 6, 2024
Copy link
Member

@erwanor erwanor left a comment

Choose a reason for hiding this comment

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

ACK, this looks great, testing/stretching manually

);

// We split the check to provide granular error messages.
let transaction_base_fee = current_gas_prices.fee(&transaction.gas_cost());

ensure!(
Copy link
Member

Choose a reason for hiding this comment

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

ACK, we:

  1. Extract the gas price vector based on the user supplied asset id
  2. Compute the base fee relative to those prices and the transaction gas cost

user_supplied_fee.amount() >= transaction_base_fee.amount(),
"fee must be greater than or equal to the transaction base price (supplied: {}, base: {})",
user_supplied_fee.amount(),
transaction_base_fee.amount(),
);
Copy link
Member

Choose a reason for hiding this comment

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

ACK, this work because both the user supplied base fee and the transaction's are computed based on the same gas prices.

@@ -23,8 +23,6 @@ impl Component for Fee {
match app_state {
Some(genesis) => {
state.put_fee_params(genesis.fee_params.clone());
// Put the initial gas prices
state.put_gas_prices(genesis.fee_params.fixed_gas_prices);
Copy link
Member

Choose a reason for hiding this comment

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

ACK, the gas prices are part of the fee parameters so no need to write them separately

hdevalence added 2 commits May 6, 2024 12:17
This commit implements a v0 of multi-asset fees (on the protocol side).
Multi-asset fees are important because they allows users who send tokens into
Penumbra over IBC to start using the chain immediately.

It does this by extending the `GasPrices` struct to include the asset ID of the
fee token the prices are for.  This allows natural handling of multi-asset fees
by just having multiple sets of `GasPrices`.

In the past, we've laid the groundwork for this (by allowing other assets in
the `Fee` structure, even if it defaults to the staking token) but haven't
prioritized it, due to concerns about some of the details of the
implementation, particularly:

A: How does the chain know that the prices of non-native fee tokens are correct?
B: What happens to the non-native fee tokens?
C: What are the privacy implications for users paying with non-native fee tokens?

This design punts on (A) by only allowing non-native fee tokens with prices
explicitly specified using governance. In the future, Penumbra could own DEX as
a price oracle, but this would be better to do after it's live and we have real
data about it, rather than worrying about edge cases now (e.g., if someone can
temporarily manipulate a price for their own trades, that's not a big deal in
itself, but hooking it to the fee mechanism used for resource usage could make
it one).

The approach to (C) is that these explicitly specified prices should be
substantially higher (10x) than those of the native token.  This means that
regardless of price variability, it should remain the case that the native fee
token is cheaper, so that the majority of users will use the same fee token and
their transactions will not be distinguishable.  On the other hand, letting it
be _possible_ to use non native fee tokens means that users who send IBC tokens
into Penumbra can access the protocol, and even if one of their first acts is
to swap some de minimis amount of the native token, they can do that within the
system.

This implementation currently does not properly handle (B), in that it silently
burns all fees, regardless of token.  It is not appropriate to burn any tokens
other than the native token, whose supply is managed by the chain. Instead, the
transaction-level fee check should move into `check_and_execute`, and call a
method on the fee component that records the fee amount in the object store. At
the end of the block, all non-native fees should be swapped into the native
token and then burned.  Emitted events should record:

- an `EventFee` for each transaction, with the transaction's gas cost, the gas
  prices used to compute the base fee, the base fee, and the actual fee;
- an `EventFeeBurn` at the end of the block, with the total amount of native
  fees burned as well as, for each alt fee token used to pay fees in that
  block, how much of the alt token was paid and how much of the native token it
  was traded for.

Client support is still mostly missing, though I have manually tested this on a
local devnet, by

1. Creating the devnet
2. Executing a parameter change proposal to set `dexParams.fixedAltGasPrices` to `"[{\"assetId\":{\"altBaseDenom\":\"ugm\"},\"blockSpacePrice\":\"80000\",\"compactBlockSpacePrice\":\"80000\",\"verificationPrice\":\"80000\",\"executionPrice\":\"80000\"}]"`
3. Hardcoding at the top of `TxCmd::exec` a change to the `gas_prices` to add the `gm` asset ID and multiplying all the prices by 10.

To properly support clients, we need to

- Record the `gas_prices` data for alt fee tokens that is newly included in the
  `CompactBlock` in the view server's storage (probably in a new `gas_prices`
  table that maps a fee token asset ID to the latest price);

- Use that data to populate the view server's gas prices RPC response (instead
  of the current `Vec::new` stub that reports any alt fee tokens as
  non-existing);

- Change the `pcli tx` command so that the existing `--fee-tier` and a new
  `--fee-token` parameters move to `pcli tx`, and apply to all subcommands
  (i.e., `pcli tx --fee-tier ... --fee-token gm send ...`) so that `TxCmd::exec`
  can pull the right `GasPrices` out of the RPC response.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-fees Area: Relates to transaction fees consensus-breaking breaking change to execution of on-chain data
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

3 participants