Fee Delegation Explained

Idea

Fee Delegation (FD) is a mechanism allowing a user to delegate his or her gas fee to a third party. Gas fee incurred from a transaction of type TxTypeFeeDelegated* will be charged to fee payer whose address and signature are present in the transaction. Refer the following list of transaction types for available FD features:

  • TxTypeFeeDelegatedValueTransfer
  • TxTypeFeeDelegatedValueTransferWithMemo
  • TxTypeFeeDelegatedSmartContractDeploy
  • TxTypeFeeDelegatedSmartContractExecution
  • TxTypeFeeDelegatedAccountUpdate
  • TxTypeFeeDelegatedCancel

Note that partial fee delegation is also possible (with type suffix WithRatio).


FD in Action

Scenario

Suppose Alice (with address 0xALICE) wants to send 1 KLAY to Bob (with address 0xBOB). Because her balance is exactly 1 KLAY, Alice decides to ask Fred (with address 0xFRED) to pay her fees.

Transaction Flow

1. Alice

Alice creates a transaction transferring 1 KLAY from her account to Bob’s, and signs it with her private key. The transaction must have correct type. In this case it has to be TxTypeFeeDelegatedValueTrasfer. Alice’s transaction would look like the following:

{
   "from" : "0xALICE",
   "to": "0xBOB",
   "type": "TxTypeFeeDelegatedValueTransfer",
   "value": "0xde0b6b3a7640000", // 1 KLAY
   "signatures": [ { "R": "0x...", "S": "0x...", "V": "0x..." } ],
   "gas" : "0x2dc6c0", // 3,000,000
   "gasPrice": "0x5d21dba00" // 25 Ston
   ...
}

When Alice is done signing, she does not send it to an EN she knows. Rather, she sends her signed raw transaction to Fred. How? I don’t know :roll_eyes: Using whatever method she prefers, Alice must deliver her transaction to Fred.

Alice ---> Fred

2. Fred

Once he receives Alice’s transaction, Fred adds his address, signs the transaction, and sends the outcome to an EN. Notice that because Alice’s transaction has already been signed, Fred cannot alter the content of the transaction but to add more information.

Alice ---> Fred ---> [[ Klaytn ]]

3. Klaytn

The transaction Fred sends to Klaytn looks like this:

{
   "from" : "0xALICE",
   "to": "0xBOB",
   "feePayer": "0xFRED",
   "feePayerSignatures": [ { "R": "0x...", "S": "0x...", "V": "0x..." } ],
   "type": "TxTypeFeeDelegatedValueTransfer",
   "value": "0xde0b6b3a7640000", // 1 KLAY
   "signatures": [ { "R": "0x...", "S": "0x...", "V": "0x..." } ],
   "gas" : "0x2dc6c0", // 3,000,000
   "gasPrice": "0x5d21dba00" // 25 Ston
   ...
}

Assuming the rest of the transaction is correct (e.g., nonce), Klaytn validates and executes this transaction as follows:

  1. Check the type
  2. If the type starts with TxTypeFeeDelegated, check the following fields: from, signatures, feePayer, and feePayerSignatures
  3. If all four fields are correct, then deducts gas * gasPrice from the account pointed by 0xFRED and executes the transaction
  4. Once the transaction is executed, refunds the remaining fees ((gas - gasUsed) * gasPrice) to Fred’s account at 0xFRED.

Finally, when the transaction gets included in a block, Bob will see the 1 KLAY Alice sent in his account.


FD in Javascript

Okay, so how do we use FD with Klaytn SDK? Let’s take a look at the following code snippet:

const Caver = require('caver-js');
const caver = new Caver('https://api.baobab.klaytn.net:8651/');

let sender = caver.klay.accounts.wallet.add("0xALICE_PRIV_KEY");
let payer = caver.klay.accounts.wallet.add("0xFRED_PRIV_KEY");

let { rawTransaction: senderRawTransaction } = await caver.klay.accounts.signTransaction({
        type: 'FEE_DELEGATED_VALUE_TRANSFER',
        from: sender.address,
        to: '0xBOB',
        gas: '3000000',
        value: caver.utils.toPeb('1', 'KLAY'),
    }, sender.privateKey);

// Alice ---> Fred
// : Alice somehow delivers raw tx to Fred

let { rawTransaction: finalTx } = await caver.klay.accounts.signTransaction({
        senderRawTransaction: senderRawTransaction,
        feePayer: payer.address
    }, payer.privateKey);

caver.klay.sendSignedTransaction(finalTx).then((err, receipt) => {
   console.log(receipt);
});

The above code is very unlikely (unless Fred totally trusts Alice), but I’m just trying to make a point here.

Alice

Alice encodes her transactions. Notice that FEE_DELEGATED_VALUE_TRANSFER is used, not TxTypeFeeDelegatedValueTransfer. When using caver-js,

  1. remove TxType
  2. indicate letters in all caps, and
  3. separate words with underscores

If this is too complicated, refer to this page and find the transaction type you want to use.

Once done signing her transaction, Alice sends the raw transaction to Fred.

Fred

Fred signs Alice’s transaction with one additional field — feePayer. The final outcome finalTx is RLP encoded transaction containing Fred’s address and his signature (we signed with payer.privateKey).

Fred uses sendSignedTransaction to deliver finalTx to a Klaytn EN.

Wrap up

That was easy, right? If you want to see a more realistic example, take a look at this. This one at least has courtesy to separate client and server :stuck_out_tongue:


FD in Java

Here’s a code snippet which does exactly the same thing as the one shown above in Javascript.

/* Omitted package statement and import statements for brevity */

public class FeeDelegatedValueTransfer {
    private static final BigInteger GAS_LIMIT = BigInteger.valueOf(3000000L);
    private static final BigInteger GAS_PRICE = BigInteger.valueOf(25000000000L);
    private static DefaultBlockParameter BLK_PARAM = DefaultBlockParameterName.LATEST;

    private static int CHAIN_ID;
    private static Caver caver;

    private static KlayCredentials senderCredential;
    private static KlayCredentials payerCredential;

    @BeforeClass
    public static void setup() {
        // Baobab setting
        CHAIN_ID = ChainId.BAOBAB_TESTNET;
        caver = Caver.build("https://api.baobab.klaytn.net:8651/");
        senderCredential = KlayCredentials.create("0xALICE_PRIV_KEY");
        payerCredential = KlayCredentials.create("0xFRED_PRIV_KEY");
    }

    @Test
    public void feeDelegatedValueTransferTest() throws IOException {
        BigInteger nonce = caver.klay().getTransactionCount(senderCredential.getAddress(), BLK_PARAM).send().getValue();
        TxTypeFeeDelegatedValueTransfer tx = TxTypeFeeDelegatedValueTransfer
                .createTransaction(
                        nonce,
                        GAS_PRICE,
                        GAS_LIMIT,
                        "0xBOB",
                        BigInteger.ONE,
                        senderCredential.getAddress()
                );
        String rawTx = tx.sign(senderCredential, CHAIN_ID).getValueAsString();

        FeePayerManager feePayerManager = new FeePayerManager.Builder(caver, payerCredential)
                .setTransactionReceiptProcessor(new PollingTransactionReceiptProcessor(caver, 1000, 10))
                .setChainId(CHAIN_ID)
                .build();

        KlayTransactionReceipt.TransactionReceipt transactionReceipt = feePayerManager.executeTransaction(rawTx);
    }
}

1 Like