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

SRC-21: add out-of-band fee-on-transfer token standard #103

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
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
101 changes: 101 additions & 0 deletions SNIPS/snip-21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
snip: 21
title: Out-of-band fee-on-transfer tokens
author: Moody Salem <[email protected]>
status: Living
type: SRC
created: 2024-07-29
---

## Simple Summary

Defines a standard for fee-on-transfer tokens that maintains the semantics of fungible token transfers and has no
changes to the existing tokens interface.

## Abstract

Fee-on-transfer is the idea of charging a fee whenever fungible tokens are transferred from one address to another. Such
tokens are very popular on other networks. These tokens have inconsistent implementations and do not work well with many
DeFi protocols, but are still popular for the deflationary aspect.

In particular, some DeFi protocols such as Ekubo Protocol do not require tokens to be transferred to swap or add
liquidity. This allows liquidity providers and swappers to entirely circumvent any possible implementation of a fee
taken on transfer--the token contract does not need to be called at all in order to interact with Ekubo pools.

## Motivation

Due to the demand for this functionality, it's important to define a mechanism that makes the best use of Starknet's
capabilities. This SRC defines an implementation of fee-on-transfer tokens that maintains the semantics of fungible
token transfers, allowing it to be broadly compatible with Starknet DeFi, and also requires no changes to the token
interface. This is possible because of Starknet's native account abstraction and multicall.

## Specification

```cairo
#[starknet::interface]
pub trait IFeeOnTransferToken<TContractState> {
// Gets the amount of fees already paid for the given sender
fn get_fees_paid(self: @TContractState, sender: ContractAddress) -> u128;

// Returns the amount of fees required to transfer the specified amount of tokens from the given sender to the receiver.
fn compute_fees_required(
self: @TContractState, sender: ContractAddress, receiver: ContractAddress, amount: u128
) -> u128;

// Pays fees from the given address for the specified sender.
// If `from` is the caller, then it pays from the caller's balance.
// Otherwise, it pays from the allowance of the `from` address to the caller as the spender.
// Returns the total amount of fees paid for the sender
fn pay_fees_verbose(
ref self: TContractState, from: ContractAddress, sender: ContractAddress, amount: u128
) -> u128;

// Same as pay_fees_verbose but always pays `from` the caller address
fn pay_fees_for_sender(ref self: TContractState, sender: ContractAddress, amount: u128) -> u128;

// Same as pay_fees_for_sender, but the sender is always the caller address
fn pay_fees(ref self: TContractState, amount: u128) -> u128;

// Withdraws any fees paid for the given sender to the specified recipient address.
// This can be called by anyone for any address--fees are a transient payment location to enable transfers for a given address. Leftover fees should always be withdrawn in the same transaction.
// Returns the amount of fees that were withdrawn.
fn withdraw_fees_paid_verbose(
ref self: TContractState, sender: ContractAddress, recipient: ContractAddress
) -> u128;

// Same as `withdraw_fees_paid_verbose` but recipient is always the caller
fn withdraw_fees_paid_for_sender(ref self: TContractState, sender: ContractAddress) -> u128;

// Same as `withdraw_fees_paid_for_sender` but the sender and recipient are both set to the caller
fn withdraw_fees_paid(ref self: TContractState) -> u128;
}
```

The expected usage flow is as follows:

- DApp stores off-chain metadata about whether a token is fee-on-transfer, and implements the following logic if it is
Copy link
Collaborator

Choose a reason for hiding this comment

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

syntax: below you write "DApp", "Dapp", "dapp". Better sticking to one choice.

- When depositing this token into a dapp, the `recipient` is the Dapp contract:
- Dapp calls `compute_fees_required(user, dapp_contract, amount)` off-chain
- Dapp adds `pay_fees(computed_fees)` call to the list of calls before all dapp calls
- Dapp adds rest of calls as normal
- Dapp adds `withdraw_fees_paid()` call to return any overpaid transfer fees
- When withdrawing this token from a dapp, the `receiver` is the caller and the `sender` is the dapp
- Dapp calls `pay_fees_for_sender(dapp_contract, amount)`
- Dapp adds rest of calls as normal
- Dapp adds `withdraw_fees_paid_for_sender(dapp_contract)`

There are other ways this standard may be used, such as handling fees off-chain in a peripheral contract. This design
expects there are no intermediate transfers of the token between the dapp contract and the user, i.e. it relies on
`approve` and `transferFrom` in cases where the called contract is not where the tokens are custodied.

## Implementation

TBD

## History

- Created 2024-07-29

## Copyright

Copyright and related rights waived via [MIT](../LICENSE).