← All standards
CAP-0010Source ↗

Fee Bump Account

3 min · 686 words

#Simple Summary

Transactions get stuck because of insufficient fee sometimes, especially if the protocol changes!

We partially address this with fee-bumping account.

#Abstract

Transactions get stuck either because of insufficient fees being paid or because min fees increase.

CAP-10 specifies a 64 bit fee-only account (denominated in basefees) which can be used to bump subsequent transactions from the account.

#Motivation

In protocols which transactions are presigned or preauth for long durations, it's a major headache if they can't get in because of fee insuficiency. CAP-0005 helps with this issue a bit, but doesn't fully address the issue for when things are truly stuck because they are now invalid.

#Specification

We extend the Account with a fee_balance account. fee_balance is a receive only account.

struct AccountEntry
{
	// ... truncated
    // reserved for future use
    union switch (int v)
    {
    case 0:
        void;
    case 1:
        struct
        {
            Liabilities liabilities;

            union switch (int v)
            {
            case 0:
                void;
			case 1:
				struct
				{
					int64 fee_balance;
					union switch (int v)
					{
						case 0:
							void;
					} ext;

				} v2;
            }
            ext;
        } v1;
    }
    ext;
};

Transactions should not be rejected for insufficient fee before querying to see if a fee_balance exists and might cover the transaction.

When a transaction executes for a given source account, it bids a fee of fee. In this case, the fee is first charged to the account and then any excess to the fee_balance. If the fee is insufficient for a given ledger, it then bids a fee of fee + fee_balance. In this case, an amount fee paid is first charged against the account, and then any excess is charged against the fee_balance.

On merge, the fees are forwarded to the fee_balance of the merge target.

We also introduce a new operation to add funds to a fee_balance account.

enum OperationType
{
    CREATE_ACCOUNT = 0,
    PAYMENT = 1,
    PATH_PAYMENT = 2,
    MANAGE_OFFER = 3,
    CREATE_PASSIVE_OFFER = 4,
    SET_OPTIONS = 5,
    CHANGE_TRUST = 6,
    ALLOW_TRUST = 7,
    ACCOUNT_MERGE = 8,
    INFLATION = 9,
    MANAGE_DATA = 10,
    BUMP_SEQUENCE = 11,
	BUMP_FEE = 12
};

struct BumpFeeOp
{
    AccountID recipient;
    SequenceNumber unless_passed;
    int64 amount;
};


enum BumpFeeResultCode
{
    // codes considered as "success" for the operation
    BUMP_FEE_SUCCESS = 0,
	BUMP_FEE_INSUFFICIENT_BALANCE = 1,
};

union BumpSequenceResult switch (BumpSequenceResultCode code)
{
case BUMP_FEE_SUCCESS:
    void;
default:
    void;
};


union OperationResult switch (OperationResultCode code)
{
case opINNER:
    union switch (OperationType type)
    {
    case CREATE_ACCOUNT:
        CreateAccountResult createAccountResult;
    case PAYMENT:
        PaymentResult paymentResult;
    case PATH_PAYMENT:
        PathPaymentResult pathPaymentResult;
    case MANAGE_OFFER:
        ManageOfferResult manageOfferResult;
    case CREATE_PASSIVE_OFFER:
        ManageOfferResult createPassiveOfferResult;
    case SET_OPTIONS:
        SetOptionsResult setOptionsResult;
    case CHANGE_TRUST:
        ChangeTrustResult changeTrustResult;
    case ALLOW_TRUST:
        AllowTrustResult allowTrustResult;
    case ACCOUNT_MERGE:
        AccountMergeResult accountMergeResult;
    case INFLATION:
        InflationResult inflationResult;
    case MANAGE_DATA:
        ManageDataResult manageDataResult;
    case BUMP_SEQUENCE:
        BumpSequenceResult bumpSeqResult;
    case BUMP_FEE:
        BumpFeeResult bumpFeeResult;
    }
    tr;
default:
    void;
};

The semantics of which are as follows:

If the account does not exist, nothing happens.

If the account sequence number is greater than unless_passed, nothing happens.

Otherwise, min(op.amount, op.source.balance - op.source.reserved) is deducted from the source account and added to the destination's fee_balance.

#Rationale

It's not too expensive to add new entries to accounts, so we add a full 64 bits. Alternatively, we could store a separate table for accounts with a fee_balance as a subentry, but this is an implementer's choice.

We don't allow to recover the funds from the fee_balance, but we do allow them to transfer during merge. This is a sort of "fee monad", which prevents malapropriation of fee subsidies paid by third parties.

We asess fees against the account's balance first because the fee_balance should only be touched in cases where the balance was insufficient to cover the expressed transaction fee, or the expressed transaction fee was insufficient for inclusion.

#Backwards Compatibility

Transactions which don't pay base fee rate must now query to see if a fee_balance exists rather than being outright invalid.

Previously, a user might expect that transactions signed with 0 fee would not be valid. Under this proposal, they may be. This doesn't really change the status quo because users are not given a guarantee about transaction fees never decreasing.

#Implementation

None yet.