Open runtian-zhou opened 3 months ago
@runtian-zhou Generally I think overloading is a useful extension that will come in handy for folks trying to implement royalties, gamification, etc., and as long as it is optional then I generally support efforts for this kind of support
My major concern stems from what could be a misinterpretation of the following:
overloaded_fungible_asset.move
. This module will serve as the new entry point for the fungible asset.
Basically, if an issuer decides to overload, does this mean that the overloading logic gets injected into every transfer for the affected asset? Or just for those who call through the overload wrappers in overloaded_fungible_asset.move
?
I am not sure how to reconcile the two, as it seems like a trade-off between a) breaking DeFi (or making it prohibitively complex to trade overloaded assets), and b) allowing circumvention of overloading
Please advise
cc @lightmark
if an issuer decides to overload, does this mean that the overloading logic gets injected into every transfer for the affected asset
I think it's the former. The issuer get to decide if they want to insert logic into transfer. Would like to understand how this could affect DeFi space a bit more. My thinking is that we will be able to provide the same api as regular fungible_asset.move. So a function for withdraw
that extract out a FungibleAsset
from storage, and a deposit
to merge a FungibleAsset
into storage. The DeFi protocol can still call the traditional value
to determine how much is left in this FungibleAsset
. In this case I suppose the DeFi protocol will need to update their code in order to work with this new type of overloadable asset?
@runtian-zhou
My thinking is that we will be able to provide the same api as regular fungible_asset.move
Yes I understand that this will apply for the new wrapper, but will DeFi protocols still be able to use the non-overloaded functions in the existing FA source code? If not, and if they are forced to use overloaded functions, this is where the accounting disaster comes in:
In this case I suppose the DeFi protocol will need to update their code in order to work with this new type of overloadable asset?
In the general case, supporting arbitrary, turing complete callback extensions to simple transfers is a nightmare for a DeFi protocol designer.
An example of a simple yet nondeterministic overload: say someone implements a lottery royalty system transfer extension: whenever a transfer happens, a random amount between .1 and .5% gets taken as commission: then a DeFi protocol that has a pool with the asset has no way of calculating the effective amount of collateral in the pool, because it is impossible to predict how much will get taken as commission on any given swap. That means that a DeFi protocol has to bake in some kind of stochastic prediction model for collateralization based on what the expected commission would be, etc. for each transfer.
Another example with a complexly deterministic overload: 1% of token x gets taken as commission if recipient holds a different token y, but 2% if they don't hold it. Then does this mean that the matching engine for an x/USDC order book has to also monitor the y holdings of every limit order holder in order to calculate effective limit price?
@MoonShiesty you might be interested in this
Thanks for the examples here! My question is how is that going to be different from what ethereum already have in their DeFi ecosystem? The coin contract only provide the interface for transfering some value from one account to the other. It's up to the coin contract to decide what actually needs to be done during this transfer. In that case, the semantic of transfer is also not predictable. Although I wasn't completely sure if anyone would do that in reality but it is indeed implementable?
Commision prediction is indeed an interesting topic. With an overloadable withdraw, it is indeed impossible to predict what the side effects could be for a given transfer.
Also in the current proposed api, the comission fee is actually a bit interesting. In the overloaded function, the signature made sure that you only have an address
instead of signer
, so you wouldn't be able to perform things like paying commission with another token, as this would require a signer
to be passed in. In this sense, it is still limited on what you can do during this transfer. This is a design decision we need to sort out and clarify though.
@runtian-zhou thanks just now for the call to chat through this live. Summary of main points:
public fun transfer_fixed_send<T: key>(
sender: &signer,
from: Object<T>,
to: Object<T>,
send_amount: u64,
)
public fun transfer_fixed_receive<T: key>(
sender: &signer,
from: Object<T>,
to: Object<T>,
receive_amount: u64,
)
SHALL
implement both of the above traits.ensures
the receive_amount
is actually received by the to
store, and similarly in the send_amount
-based API@runtian-zhou thanks just now for the call to chat through this live. Summary of main points:
- An asset issuer will be able to optionally define overloading at asset init time.
- In the case of an asset with defined overloading, standard FA APIs will abort, and only the overloaded APIs will work.
- In the case of an asset without defined overloading, standard FA APIs will work.
- Rust trait-like interfaces allow greater composability for FA transfers, but introduce complexities where collateralization guarantees are needed.
- A major concern for DeFi protocols is ensuring that if an overloaded transfer happens, the amount in/out of a pool/vault etc. is deterministic and simple to account for.
DeFi protocols could ideally offload the complexity/nondeterminism of a taxed transfer outside the bounds of their logic, which could be ensured through the offering of two APIs:
- A transfer with a fixed amount debited from the sender:
public fun transfer_fixed_send<T: key>( sender: &signer, from: Object<T>, to: Object<T>, send_amount: u64, )
- A transfer with a fixed amount credited to the recipient:
public fun transfer_fixed_receive<T: key>( sender: &signer, from: Object<T>, to: Object<T>, receive_amount: u64, )
- In the case of a non-overloaded FA transfer, these APIs will be exhibit identical behavior.
- An asset defining an overload interface
SHALL
implement both of the above traits.- Note that a Move prover specification may be useful for the two APIs, e.g. one specification that
ensures
thereceive_amount
is actually received by theto
store, and similarly in thesend_amount
-based API
Wish we had spoken in person. I think the above request isn't really practical. What you're effectively asking for is an additional set of native calls and more logic on the overloaded contracts for reasons that aren't super clear to me. It also implies that certain assets have to conform deterministically, which isn't a requirement at all. In fact, this model allows for arbitrary behavior.
So I think there are the following cases:
I think the concern your sharing is that with an overloaded asset, a user could specify overloaded::withdraw(X)
, but the fa::withdraw
is completely arbitrary. Similarly the deposit is completely arbitrary. How is this handled in Eth or Solana Token2022 with extensions? I imagine in eth, there's no real guarantee as the developer is just implementing an interface. In Solana, given it is more rigid, maybe it is enumerable?
What I'd like to propose is that at least a withdraw cannot withdraw more than the user indicated. Specifically, we check the balance before the withdraw and after and ensure that that is the amount requested. That eliminates a lot of quirks where a protocol can be excessively arbitrary. It should also help ensure that a pool is able to adequately track fund movements out of it. In terms of deposit, we already don't know how much was removed during the withdraw taxing, so a depositing taxing has no additional implications. This must be monitored outside the dapp.
Thoughts @alnoki?
@davidiw the fixed send/receive amounts are so make DeFi accounting pragmatic and user friendly
Consider the case where there is no fixed receive, and a user wants to deposit 1000 X into a pool, because the pool will abort unless it actually receives 1000 X. The basic "provide liquidity" function will simply call a 1000 X transfer, but if there is some kind of tax on it then the txn aborts. And even if it is deterministic that means someone else has to write an API that ensures the deposit works smoothly by specifying some amount above 1000 X
But this is solved for a fixed receive function
On the converse side once the assets are in the pool, the user who wants to withdraw specifies how much comes from the pool, rather than how much they get, for the same reason that the pool accounting must not be altered by transfer tax effects. Hence a fixed send function
Specifically, we check the balance before the withdraw and after and ensure that that is the amount requested.
So you are already enforcing that withdrawn_amount == withdraw_amount_passed_to_api
? I don't see why this can't also be done for deposit on a fixed receive counterpart
@davidiw the fixed send/receive amounts are so make DeFi accounting pragmatic and user friendly
Consider the case where there is no fixed receive, and a user wants to deposit 1000 X into a pool, because the pool will abort unless it actually receives 1000 X. The basic "provide liquidity" function will simply call a 1000 X transfer, but if there is some kind of tax on it then the txn aborts. And even if it is deterministic that means someone else has to write an API that ensures the deposit works smoothly by specifying some amount above 1000 X
But this is solved for a fixed receive function
On the converse side once the assets are in the pool, the user who wants to withdraw specifies how much comes from the pool, rather than how much they get, for the same reason that the pool accounting must not be altered by transfer tax effects. Hence a fixed send function
Specifically, we check the balance before the withdraw and after and ensure that that is the amount requested.
So you are already enforcing that
withdrawn_amount == withdraw_amount_passed_to_api
? I don't see why this can't also be done for deposit on a fixed receive counterpart
The further we go down this path, we end up with a more complicated and restrictive API. The intent here is to allow for rather arbitrary asset types. Though this conversation better belongs in the AIP than in code. We most certainly will not ship painful to use code. So let's treat this as a higher level discussion first and foremost :).
So if we compare to ERC-20, we are already more explicit, see https://eips.ethereum.org/EIPS/eip-20 where as our withdraws must extract explicitly the expected amount.
If we want to have relatively arbitrary behavior, there's no way to also give the behavior you seek. Instead we need to make a relatively fixed set of operations that can be used independent of the token withdraw / deposit or we increase the number of functions that need to be dispatched.
DeFi solutions already have verified and unverified pools, which I hope would ensure that only those assets that either play by the natural rules are authorized and those that do not have appropriate harnesses around them. I worry that trying to be exhaustive will increase development time and have lesser outcomes.
What happens if/when Aptos offers broader dynamic dispatch? Will the exposure to framework an core library functions require such considerations?
Here are a set of operations that could exist:
Maybe we could have an additional two overriden functions:
amount_after_withdraw(fa_meta, account, amount)
amount_after_deposit(fa_meta, account, amount)
or alternatively we could have
amount_to_withdraw(...)
amount_to_deposit(...)
for inverse functionsThese can be enforced on the respective wrapper for withdraw and deposit.
However, each additional override costs more computationally and cognitively. Personally not aware of how this is handled in ETH and it seems like Solana Token 2022 has somewhat arbitrary behaviors too. I'll look at Solana more carefully.
Hmm... now if you just want a transfer function that has these asserts in place for convenience, I think we could do that. However if we want the flexibility with learning how much to move around to get to a certain value, it becomes more and more expensive and limiting. These are operations that may be better suited for off-chain computation.
@davidiw
if you just want a transfer function that has these asserts in place for convenience, I think we could do that
That would resolve most of the issues
off-chain computation
I'd rather rely on purely onchain to eliminate trust requirements
Maybe we could have an additional two overriden functions:
amount_after_withdraw(fa_meta, account, amount)
amount_after_deposit(fa_meta, account, amount)
or alternatively we could have
amount_to_withdraw(...)
amount_to_deposit(...)
for inverse functions
Yes please, this should do it
I think what makes sense here is:
🚀 Feature Request: Dispatchable Fungible Asset Standard
Summary
Right now the Aptos Framework defines one single
fungible_asset.move
as our fungible asset standard, making it hard for other developers to customize the logic they need. With this AIP, we hope that developers can define their custom way of withdrawing and deposit for their fungible asset, allowing for a much more extensible way of using our Aptos Framework.This proposal will be submitted as an AIP. Using issue here to track and start the discussion.
Goals
The goal is to allow for third party developers to inject their custom logic during fungible asset deposit and withdraw. This would allow for use cases such as:
Note that all the logics mentioned above can be developed and extended by any developer on Aptos! This would greatly increase the extensivity of our framework.
Out of Scope
We will not be modifying any core Move VM/file format logic. We will use this AIP as the predecessor work for the future dynamic/static dispatch we are planning to support in the future Move versions.
The AIP here can potentially be applied to our NFT standard as well. However, we are not going to worry about such use case in the scope of this AIP.
Motivation
Right now the Aptos Framework governs the whole logic of what fungible asset means, and every defi module will need to be statically linked against such module. We probably won't be able to meet the various functionality need coming from all of our developers, so an extensible fungible asset standard is a must on our network.
Impact
We want to offer token developers the flexibility to inject customized logic during token withdraw and deposit. This would have some downstream impact to our defi developers as well.
Alternative solutions
We are using this AIP as the precurssor work of the future dispatch support in Move on Aptos. So we will have a limit scoped dispatch function implemented via a native function instead of a full set of changes in Move compiler and VM so that we will have more time to assess the security implication of dispatching logic in Move.
For the proposed
overloaded_fungible_asset.move
, another alternative solution would be to add the dispatch functionality directly in the existingfungible_asset.move
. However, that would be pretty unusable right out of the box with the existing runtime rule proposed. In order for such dispatch function to be usable, we need an exception for the runtime safety rule where re-entrancy intofungible_asset.move
is allowed. This would require the framework developers to be particularly cautious about the potential re-entrancy problem.Specification
We will be adding two modules in the Aptos Framework.
function_info.move
. This module will simulate a runtime function pointer that could be used as dispatching. The module will look like the following:public fun new( module_address: address, module_name: string, function_name: string, ): FunctionInfo
// Check the function signature of lhs is equal to rhs. // This could serve as the type checker to make sure the dispatcher function have the same type as the dispatching function public(friend) native fun check_dispatch_function_info_compatible( lhs: &FunctionInfo, rhs: &FunctionInfo, ): bool;
[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct WithdrawalFunctionStore has key { // Each distinct Metadata can have exactly one predicate function: FunctionInfo }
// Dispatchable call based on the first argument. // // MoveVM will use the FunctionInfo to determine the dispatch target. native fun dispatchable_withdraw( function: &FunctionInfo, // security conversation: Do we need this owner field here? owner: [address,&signer], store: Object,
amount: u64,
): FungibleAsset;
// The dispatched version of withdraw. This withdraw will call the predicate function instead of the default withdraw function in fungible_asset.move public fun withdraw(
owner: &signer,
store: Object,
amount: u64,
): FungibleAsset acquires FungibleStore
// Store the function info so that withdraw can invoke the customized dispatchable_withdraw public fun register_withdraw_epilogue( owner: &ConstrutorRef, withdraw_function: FunctionInfo, )
public fun supply(): u64 acquires Lending { borrow_global(@address).supply
}
public fun borrow(amount: u64) { let supply = supply();
// call functions from other module another_module::bar();
supply += amount; set_supply(supply) }
module 0x1.A { import 0x1.signer; struct T1 has key {v: u64} struct T2 has key {v: u64}
}
public fun borrow(amount: u64) { let supply = borrow_global_mut(@address).supply
// Call into the dispatch version of fungible asset. // // MoveVM will direct control flow to the
dispatchable_withdraw
function mentioned above. aptos_framework::overloadable_fungible_asset::withdraw()supply += amount; set_supply(supply) }
public fun dispatchable_withdraw(...) { // Two mutable references created let supply_2 = borrow_global_mut(@address).supply
}
[view]
public fun supply(): u64 acquires Lending { borrow_global(@address).supply
}
public fun borrow(amount: u64) { let supply = supply();
// call functions from other module ()
supply += amount; set_supply(supply) }
public fun dispatchable_withdraw(...) { // Mutate the supply field ... }
[ some_module::borrow, aptos_framework::overloadable_fungible_asset::withdraw, some_module::dispatchable_withdraw, <-- backedge formed. As
some_module
is already on top of the call stack ][ A::some_function aptos_framework::fungible_asset::withdraw, third_party_token::dispatchable_withdraw, aptos_framework::fungible_asset::split, <-- backedge formed. As
fungible_asset
is already on top of the call stack ]