Generic Type Confusion
Category: Abilities & Types · Covered by: Load, once the type says it; yours, to make the type say it · Requires: any Aptos Move version
What this prevents
A value that records a quantity without recording what the quantity is in. A receipt says a hundred is owed, a settlement function checks that a hundred is returned, and nothing in either establishes that the hundred coming back is the same asset as the hundred that went out.
The classic shape is a flash loan: borrow an expensive asset, repay the same count of a cheap one, and the accounting balances. It generalises to anything that hands out a claim, a handle, or an obligation and later redeems it.
Coverage
The verifier enforces type equality exactly, and that is the whole point: this failure is not a missing check, it is a type that was never asked to carry the distinction.
Once the asset is in the receipt’s type, Receipt<Gold> and Receipt<Dust> are different types and
the mismatch is a compile error. Nothing can be done at run time to get past it, and no check needs
writing.
Until then, the type system is doing its job correctly on a type that does not describe the thing it is standing for. That is a design decision, not an enforcement gap, which is why this belongs beside Ability Declarations as an Access Decision: both are cases where the platform is at full strength and the question it is answering is the wrong one.
What is left to you
Deciding what the type has to say.
For each value that stands for a claim on something, ask which asset, which account, which pool, which market. If the answer matters at redemption and is not in the type, it must be carried in a field and checked, and a field check is weaker: it runs at run time, it can be forgotten in one of several settlement paths, and it is only as good as the comparison written.
Reach for phantom when the parameter exists to distinguish types rather than to store anything.
Receipt<phantom T> has no field of type T; the parameter is there so two receipts are not the
same type. Without phantom the compiler requires T to appear in a field, which forces a
meaningless field into the struct.
A single settlement function per asset type is a smell worth noticing. Two functions that differ only in which coin they accept usually means the receipt is not carrying what it should, and unifying them under one generic signature is the fix and the test at once.
Sample and test
See src/sources/GenericTypeConfusion.move, exercised by
src/tests/GenericTypeConfusion_test.move. It carries both designs: a loose receipt with an
amount and no asset, and the same receipt with the asset in its type.
Five cases. Two show the loose receipt behaving normally and still checking the amount. One deliberately passes: a hundred gold is borrowed, a hundred dust settles it, the amount matches, the receipt is consumed, and the borrower walks away holding the gold. That is the failure shown working. Two more show the tied receipt settling in its own asset and still checking amounts.
The defence is a compile-time failure and so cannot be asserted from a test. Attempting the same attack against the tied receipt fails to build:
error: cannot pass `generic_type_confusion::Receipt<generic_type_confusion::Gold>`
to a function which expects argument of type
`generic_type_confusion::Receipt<generic_type_confusion::Dust>`
Key excerpt:
/// Says what is owed and not what it is owed in.
struct LooseReceipt { amount: u64 }
/// The same, with the asset carried in the type. `phantom` because the
/// parameter appears in no field: it exists to make two receipts different
/// types, not to store anything.
struct Receipt<phantom T> { amount: u64 }
Review questions
- For every receipt, ticket, handle or obligation, which asset or account does it refer to, and is that in the type or only in a field?
- Are there several settlement functions differing only in the asset they accept? That usually means the claim is not carrying it.
- Does a generic parameter appear in any field? If not, it should be
phantom; if it does, check that it is the parameter the redemption logic actually depends on. - Where a field carries the distinction instead of the type, is it compared on every path that can settle the obligation?
Trade-offs
- Pro: the check moves from run time to compile time and costs nothing at either. Once the type is right, the mistake is unwritable.
- Con / cost: generic settlement functions are harder to read than concrete ones, and callers must name the type parameter. A protocol supporting many assets carries the parameter through every signature that touches a claim.
- Trust note: the type establishes which asset, not how much or whose. Amount and ownership are still ordinary checks.
Historical note
Flash loans made this class visible, because they compress borrow and repay into one transaction where a mismatch is immediately profitable. The underlying mistake predates them and is not specific to lending: any protocol issuing a claim and redeeming it later can issue one that does not say what it is a claim on.
References
aptos-core,third_party/move/move-bytecode-verifier/src/signature_v2.rs, for constraint checking on generic instantiations- Move on Aptos Book, on
phantomtype parameters and why they exist - Ability Declarations as an Access Decision, for the neighbouring case where the type permits more than the design intends