-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
[aptos-framework] Dispatchable Token Standard #12635
Conversation
⏱️ 2h 47m total CI duration on this PR
🚨 1 job on the last run was significantly faster/slower than expected
|
I tried to separate out the components into each individual commits. So reviewing by commit might be better. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any way that this support can be extended for primary fungible stores?
Asking since those will be the primary use case for FA transfers
/// 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, | ||
) acquires OverloadFunctionStore { | ||
let store_address = object::object_address(&from); | ||
assert!(exists<OverloadFunctionStore>(store_address), error::not_found(EFUNCTION_STORE_NOT_FOUND)); | ||
let overloadable_store = borrow_global<OverloadFunctionStore>(store_address); | ||
let fa = fungible_asset::withdraw_with_ref(&overloadable_store.transfer_ref, from, send_amount); | ||
deposit(to, fa); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
So in this case the deposit function is the one that might implement a tax?
Asking because I don't see an assert for EAMOUNT_MISMATCH
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the assumption here is we use withdraw_with_ref to get the exact amount from the sender side. So we don't need EAMOUNT_MISMATCH
let fa = withdraw(sender, from, receive_amount); | ||
let store_address = object::object_address(&from); | ||
let overloadable_store = borrow_global<OverloadFunctionStore>(store_address); | ||
assert!(fungible_asset::amount(&fa) == receive_amount, error::aborted(EAMOUNT_MISMATCH)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So here, the assumption is that once a hot potato FA has been withdrawn, the exact amount will necessarily end up in the receiver store
Correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes because we use deposit_with_ref so it has no custom behavior.
public fun withdraw<T: key>( | ||
owner: &signer, | ||
store: Object<T>, | ||
amount: u64, | ||
): FungibleAsset acquires OverloadFunctionStore { | ||
let metadata_addr = object::object_address(&fungible_asset::store_metadata(store)); | ||
let owner_address = signer::address_of(owner); | ||
assert!(exists<OverloadFunctionStore>(metadata_addr), error::not_found(EFUNCTION_STORE_NOT_FOUND)); | ||
let overloadable_store = borrow_global<OverloadFunctionStore>(metadata_addr); | ||
dispatchable_withdraw( | ||
owner_address, | ||
store, | ||
amount, | ||
&overloadable_store.transfer_ref, | ||
&overloadable_store.withdraw_function, | ||
) | ||
} | ||
|
||
/// Deposit `amount` of the fungible asset to `store`. | ||
/// | ||
/// The semantics of deposit will be governed by the function specified in OverloadFunctionStore. | ||
public fun deposit<T: key>( | ||
store: Object<T>, | ||
fa: FungibleAsset | ||
) acquires OverloadFunctionStore { | ||
let metadata_addr = object::object_address(&fungible_asset::store_metadata(store)); | ||
assert!(exists<OverloadFunctionStore>(metadata_addr), error::not_found(EFUNCTION_STORE_NOT_FOUND)); | ||
let overloadable_store = borrow_global<OverloadFunctionStore>(metadata_addr); | ||
dispatchable_deposit( | ||
store, | ||
fa, | ||
&overloadable_store.transfer_ref, | ||
&overloadable_store.deposit_function, | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's an interesting case. To avoid double assess maybe we should just use the transfer api so at most one side pays the tax?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the issue? I already pay two taxes in the US ... income and sales 😭
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davidiw my main concern here is ensuring that a transfer can have either a known amount sent or received
else the accounting for DeFi breaks, since you don't have a known amount deposited into a pool (known receive amount) or withdrawn from a pool (known send amount)
ff16f49
to
8328a2c
Compare
fungible_asset::deposit_with_ref(&overloadable_store.transfer_ref, to, fa); | ||
} | ||
|
||
native fun dispatchable_withdraw<T: key>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, we cannot specify this function because, at the static time, function
is unknown, and we do not know the exact behavior of this native function dispatchable_withdraw
.
It would be useful to observe some instances of overloaded functions
and see what properties we can specify for them.
|
||
#[resource_group_member(group = aptos_framework::object::ObjectGroup)] | ||
struct OverloadFunctionStore has key { | ||
withdraw_function: FunctionInfo, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe these should be optional and we need balance and something else, @movekevin
public fun withdraw<T: key>( | ||
owner: &signer, | ||
store: Object<T>, | ||
amount: u64, | ||
): FungibleAsset acquires OverloadFunctionStore { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a wrapper around withdraw, the happy path is that the FA isn't frozen and that this isn't used. We'll also need to see performance of the global freeze.
public fun withdraw<T: key>( | ||
owner: &signer, | ||
store: Object<T>, | ||
amount: u64, | ||
): FungibleAsset acquires OverloadFunctionStore { | ||
let metadata_addr = object::object_address(&fungible_asset::store_metadata(store)); | ||
let owner_address = signer::address_of(owner); | ||
assert!(exists<OverloadFunctionStore>(metadata_addr), error::not_found(EFUNCTION_STORE_NOT_FOUND)); | ||
let overloadable_store = borrow_global<OverloadFunctionStore>(metadata_addr); | ||
dispatchable_withdraw( | ||
owner_address, | ||
store, | ||
amount, | ||
&overloadable_store.transfer_ref, | ||
&overloadable_store.withdraw_function, | ||
) | ||
} | ||
|
||
/// Deposit `amount` of the fungible asset to `store`. | ||
/// | ||
/// The semantics of deposit will be governed by the function specified in OverloadFunctionStore. | ||
public fun deposit<T: key>( | ||
store: Object<T>, | ||
fa: FungibleAsset | ||
) acquires OverloadFunctionStore { | ||
let metadata_addr = object::object_address(&fungible_asset::store_metadata(store)); | ||
assert!(exists<OverloadFunctionStore>(metadata_addr), error::not_found(EFUNCTION_STORE_NOT_FOUND)); | ||
let overloadable_store = borrow_global<OverloadFunctionStore>(metadata_addr); | ||
dispatchable_deposit( | ||
store, | ||
fa, | ||
&overloadable_store.transfer_ref, | ||
&overloadable_store.deposit_function, | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the issue? I already pay two taxes in the US ... income and sales 😭
) acquires OverloadFunctionStore { | ||
let metadata_addr = object::object_address(&fungible_asset::store_metadata(from)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
something seems off here where is the ownership check?
fda30ca
to
637170e
Compare
@@ -100,6 +100,11 @@ module aptos_framework::fungible_asset { | |||
project_uri: String, | |||
} | |||
|
|||
#[resource_group_member(group = aptos_framework::object::ObjectGroup)] | |||
struct GlobalFreeze has key { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
freeze has a different meaning.
Let's call this differently / more descriptively, and not share error codes.
maybe DelegatedToOverloaded? ActionsOverloaded?
also, can only one of the withdraw/deposit be overloaded, or do both need to be simultanously? or if either is, we just ask them to go through the other api anyways?
637170e
to
4d5d593
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
overall looks reasonable to me
@@ -139,9 +142,14 @@ impl Interpreter { | |||
.map_err(|e| self.set_location(e))?; | |||
} | |||
|
|||
if let Some(module_id) = function.module_id() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i guess it's impossible to re-enter a script?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea script has to be always on the top most frame only.
mut arguments: VecDeque<Value>, | ||
) -> SafeNativeResult<SmallVec<[Value; 1]>> { | ||
let (module_name, func_name) = extract_function_info(&mut arguments)?; | ||
Err(SafeNativeError::CallFunction { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems pretty hacky
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is indeed a hack, but it's due to a limitation of our current implementation -- native functions lack access to the real gas meter, so to do the metering properly, we exit from the native function and then instruct the Move VM to do the actual work.
We have considered various alternatives and non of them is trivial.
string::utf8(b"dispatchable_withdraw"), | ||
); | ||
// Verify that caller type matches callee type so wrongly typed function cannot be registered. | ||
assert!(function_info::check_dispatch_type_compatibility( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we ensure the function is public? otherwise it can be upgraded and even change the function signature
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want to be even stricter, and require it to be un-upgradeable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it needs to be a public function -- otherwise this check needs to be performed each transfer.
I think it ought to be upgradeable -- not sure why we'd restrict it that way.
string::utf8(b"dispatchable_withdraw"), | ||
); | ||
// Verify that caller type matches callee type so wrongly typed function cannot be registered. | ||
assert!(function_info::check_dispatch_type_compatibility( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want to be even stricter, and require it to be un-upgradeable?
aptos-move/framework/aptos-framework/sources/fungible_asset.move
Outdated
Show resolved
Hide resolved
aptos-move/framework/aptos-framework/sources/dispatchable_fungible_asset.move
Show resolved
Hide resolved
4d5d593
to
6d6a293
Compare
22c88ae
to
dd6450c
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@@ -45,3 +45,13 @@ proposals: | |||
secrecy_threshold_in_percentage: 50 | |||
reconstruct_threshold_in_percentage: 66 | |||
fast_path_secrecy_threshold_in_percentage: 67 | |||
- name: step_5_dispatchable_token |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't add features in the default release yaml file anymore, they will be enabled separately. if you want you can create a dispatchable.yaml file in the same folder
f5bec0c
to
417e662
Compare
7d9b67b
to
2e0f00c
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
2e0f00c
to
984aa7e
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
✅ Forge suite
|
✅ Forge suite
|
@@ -263,20 +267,25 @@ impl SharedTestingConfig { | |||
VMResult<Vec<Vec<u8>>>, | |||
TestRunInfo, | |||
) { | |||
let move_vm = MoveVM::new(self.native_function_table.clone()).unwrap(); | |||
let mut config = VMConfig::default(); | |||
config.paranoid_type_checks = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reverts commit bbf569a.
Description
This PR provides a reference implementation for AIP-73. See discussion thread: #12490.
The PR mainly have the following four parts:
function_info.move
that mocks the function pointeroverloadable_fungible_asset.move
that utilizes the function info and dispatch native to allow for a custom withdraw operationType of Change
Which Components or Systems Does This Change Impact?
How Has This Been Tested?
Added tests for all logics added.
Key Areas to Review
A few areas we should focus on:
Checklist