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 7 commits into
base: main
Choose a base branch
from
Open
Changes from 4 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-x.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
snip: $SNIP_ID
Eikix marked this conversation as resolved.
Show resolved Hide resolved
title: Out-of-band fee-on-transfer tokens
author: Moody Salem <[email protected]>
status: Living
type: SRC
created: 2024-07-29
---

## Simple Summary
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you add a flow diagram to explain how the normal scenario unfolds?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added some context as well as some methods to the interface to the specification

Could use some feedback from FOT developers on how it is proposed to work

Choose a reason for hiding this comment

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

@moodysalem, thanks for proposing this out-of-band FOT standard. As a FOT developer, I have some thoughts:

Implementation seems feasible. Curious about what specifically makes FOT tokens difficult in Ekubo's architecture?

I think pre-paying tax might add friction, but it's probably manageable. Your approach could prevent 'k' invariant issues in pools. It seems easier to implement since the transfer function doesn't alter token amounts in the pool.


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
- 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
Copy link

Choose a reason for hiding this comment

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

shouldn't this be

"Dapp adds pay_fees_for_sender(user, computed_fees)call" given that the user is the sender? current call defines the Dapp as the sender

Copy link
Contributor Author

Choose a reason for hiding this comment

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

when i say 'dapp adds xyz to list of calls', i mean calls that are made from the user's account. it's usually easier to not include the user's address in the list of calls, so pay_fees would implicitly be for the user's account

Copy link

@TAdev0 TAdev0 Aug 1, 2024

Choose a reason for hiding this comment

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

Oh yes makes sense.. my bad. I understood it as the Dapp making the calls to the token contract that implements the interface with calldata passed by the user (which indeed doesn't make sense as it's the user that pays the fee, not the dapp)

- Dapp adds rest of calls as normal
- Dapp adds `withdraw_fees_paid()` call to return any overpaid transfer fees
moodysalem marked this conversation as resolved.
Show resolved Hide resolved
- 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)`
moodysalem marked this conversation as resolved.
Show resolved Hide resolved
- Dapp adds rest of calls as normal
- Dapp adds `withdraw_fees_paid_for_sender(dapp_contract)`
moodysalem marked this conversation as resolved.
Show resolved Hide resolved

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).