From 0f3eb33ce4af413aeb199dd561402e9f406edb0e Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Mon, 29 Jul 2024 12:06:06 -0400 Subject: [PATCH 1/5] add fee on transfer interface --- SNIPS/snip-x.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 SNIPS/snip-x.md diff --git a/SNIPS/snip-x.md b/SNIPS/snip-x.md new file mode 100644 index 0000000..914b1be --- /dev/null +++ b/SNIPS/snip-x.md @@ -0,0 +1,63 @@ +--- +snip: $SNIP_ID +title: Out-of-band fee-on-transfer tokens +author: Moody Salem +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 { + // Gets the amount of fees already paid for the given sender + fn get_fees_paid(self: @TContractState, sender: ContractAddress) -> ContractAddress; + + // Returns the amount of fees required to transfer the specified amount of tokens from the given sender to the given recipient. + fn compute_fees_required( + self: @TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u128 + ) -> u128; + + // Pay 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. + fn pay_fees_from( + ref self: TContractState, from: ContractAddress, sender: ContractAddress + ) -> 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( + ref self: TContractState, sender: ContractAddress, recipient: ContractAddress + ) -> u128; +} +``` + +## Implementation + +TBD + +## History + +- Created 2024-07-29 + +## Copyright + +Copyright and related rights waived via [MIT](../LICENSE). From a4e53916af61de3bbe30440e5962a046f1fadd42 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Mon, 29 Jul 2024 12:25:24 -0400 Subject: [PATCH 2/5] add an overload for paying fees that does not require knowing the caller address --- SNIPS/snip-x.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SNIPS/snip-x.md b/SNIPS/snip-x.md index 914b1be..3d2c15b 100644 --- a/SNIPS/snip-x.md +++ b/SNIPS/snip-x.md @@ -40,6 +40,9 @@ pub trait IFeeOnTransferToken { fn pay_fees_from( ref self: TContractState, from: ContractAddress, sender: ContractAddress ) -> u128; + + // Same as pay_fees_from but always pays from the caller address + fn pay_fees(ref self: TContractState, sender: ContractAddress) -> 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. From 1ee7477648e0e5fe735899a65a52cad48243c0fa Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Tue, 30 Jul 2024 11:15:00 -0400 Subject: [PATCH 3/5] clean up --- SNIPS/snip-x.md | 57 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/SNIPS/snip-x.md b/SNIPS/snip-x.md index 3d2c15b..6955971 100644 --- a/SNIPS/snip-x.md +++ b/SNIPS/snip-x.md @@ -9,17 +9,25 @@ 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. +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. +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. +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. +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 @@ -29,30 +37,57 @@ pub trait IFeeOnTransferToken { // Gets the amount of fees already paid for the given sender fn get_fees_paid(self: @TContractState, sender: ContractAddress) -> ContractAddress; - // Returns the amount of fees required to transfer the specified amount of tokens from the given sender to the given recipient. + // 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, recipient: ContractAddress, amount: u128 + self: @TContractState, sender: ContractAddress, receiver: ContractAddress, amount: u128 ) -> u128; // Pay 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. - fn pay_fees_from( - ref self: TContractState, from: ContractAddress, sender: ContractAddress + // 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_from but always pays from the caller address - fn pay_fees(ref self: TContractState, sender: ContractAddress) -> 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( + 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 + - 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 From 02ac2bf86a0781983f5409b7e3de3fcbcc475093 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Wed, 31 Jul 2024 19:35:26 -0400 Subject: [PATCH 4/5] fix tadev comments --- SNIPS/snip-x.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SNIPS/snip-x.md b/SNIPS/snip-x.md index 6955971..6dfa9dd 100644 --- a/SNIPS/snip-x.md +++ b/SNIPS/snip-x.md @@ -35,14 +35,14 @@ interface. This is possible because of Starknet's native account abstraction and #[starknet::interface] pub trait IFeeOnTransferToken { // Gets the amount of fees already paid for the given sender - fn get_fees_paid(self: @TContractState, sender: ContractAddress) -> ContractAddress; + 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; - // Pay fees from the given address for the specified sender. + // 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 From 42c94dd8d82fb6490bd92f210a0acbb85574121e Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Thu, 15 Aug 2024 10:49:24 +0200 Subject: [PATCH 5/5] assign SNIP number to 21 --- SNIPS/{snip-x.md => snip-21.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename SNIPS/{snip-x.md => snip-21.md} (99%) diff --git a/SNIPS/snip-x.md b/SNIPS/snip-21.md similarity index 99% rename from SNIPS/snip-x.md rename to SNIPS/snip-21.md index 6dfa9dd..9eedb47 100644 --- a/SNIPS/snip-x.md +++ b/SNIPS/snip-21.md @@ -1,5 +1,5 @@ --- -snip: $SNIP_ID +snip: 21 title: Out-of-band fee-on-transfer tokens author: Moody Salem status: Living