v0.1.0-pre.011
This commit is contained in:
251
docs/SPL_TOKEN2022_SOURCE_AUDIT.md
Normal file
251
docs/SPL_TOKEN2022_SOURCE_AUDIT.md
Normal file
@@ -0,0 +1,251 @@
|
||||
<!-- file: docs/SPL_TOKEN2022_SOURCE_AUDIT.md -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# Audit des sources Token-2022 résolues
|
||||
|
||||
## Ancrage reproductible
|
||||
|
||||
L'interface Cargo résolue `spl-token-2022-interface 3.1.1` publie le commit
|
||||
`e18f9c6f9bf6044b934f48e3090e8e59e4820f02`, tagué `interface@v3.1.1` le 26 juin 2026.
|
||||
L'audit est effectué sur ce commit détaché, jamais sur la branche `main` mouvante.
|
||||
|
||||
Le `Cargo.lock` du workspace opérateur, SHA-256
|
||||
`248c02af8c78d3bb6ad747460fee0011f8141feef83364cf697e35e44bd4ceb9`, confirme que
|
||||
`kb_decoder_spl_token_2022 0.4.5` consomme directement les interfaces Token-2022 et registre
|
||||
ElGamal. Pour `spl-token-2022-interface 3.1.1`, il résout exactement :
|
||||
|
||||
- `solana-zk-elgamal-proof-interface 0.1.3` ;
|
||||
- `spl-token-confidential-transfer-proof-extraction 0.6.1` ;
|
||||
- `spl-token-group-interface 0.7.2` ;
|
||||
- `spl-token-metadata-interface 1.0.1` ;
|
||||
- `spl-type-length-value 0.9.1`.
|
||||
|
||||
La sortie `cargo tree -p spl-token-2022-interface -e features` prouve en particulier le chemin de
|
||||
consommation direct de Token Metadata `1.0.1`. Cette version diffère du `1.0.0` figé dans le
|
||||
`Cargo.lock` du processor source audité : les discriminants, layouts et builders devront donc être
|
||||
comparés différentiellement avant le décodeur, sans supposer leur égalité.
|
||||
|
||||
La comparaison officielle clôt cette dérive : `interface@v1.0.1`, publiée le 29 juin 2026 au
|
||||
commit court `7038969`, contient le correctif fusionné `fc97c60` de la pull request `87`. Le diff
|
||||
de production ajoute 78 lignes dans `interface/src/instruction.rs` sans suppression. Les
|
||||
discriminants, structures de payload et builders existants ne changent pas ; `unpack()` valide
|
||||
désormais les longueurs Borsh `u32 LE` avant `try_from_slice` pour :
|
||||
|
||||
- `Initialize` : `name`, `symbol`, `uri` ;
|
||||
- `UpdateField` : la chaîne de `Field::Key` lorsque le tag vaut `3`, puis `value` ;
|
||||
- `RemoveKey` : `key`, après l'octet booléen `idempotent`.
|
||||
|
||||
`UpdateAuthority` et `Emit` ne transportent pas de chaîne et restent inchangés. Le décodeur devra
|
||||
appliquer la garde `1.0.1` avant toute allocation, même si le processor source figé résolvait
|
||||
encore `1.0.0`.
|
||||
|
||||
Au même commit :
|
||||
|
||||
- `program/Cargo.toml` déclare le processor `spl-token-2022 11.0.0` ;
|
||||
- sa dépendance path demande l'interface `3.1.0`, satisfaite par le package workspace `3.1.1` ;
|
||||
- le `Cargo.lock` source résout `spl-token-group-interface 0.7.2`,
|
||||
`spl-token-metadata-interface 1.0.0` et `spl-elgamal-registry-interface 0.2.1` ;
|
||||
- l'égalité du binaire déployé avec cette source reste non prouvée sur Localnet, Devnet et Mainnet.
|
||||
|
||||
## Dispatch réellement implémenté
|
||||
|
||||
`Processor::_process_inner` applique cet ordre :
|
||||
|
||||
1. `PodTokenInstruction`, discriminant `u8` ;
|
||||
2. si ce décodage échoue, `TokenMetadataInstruction`, discriminant SPL de huit octets ;
|
||||
3. si ce décodage échoue, `TokenGroupInstruction`, discriminant SPL de huit octets ;
|
||||
4. sinon `InvalidInstruction`.
|
||||
|
||||
La surface routable contient donc :
|
||||
|
||||
| Famille | Enveloppes ou formes |
|
||||
|------------------------------------------------------|---------------------:|
|
||||
| Tags Token-2022 `u8` | 48 |
|
||||
| Familles d'extensions parmi ces tags | 15 |
|
||||
| Sous-instructions de ces familles | 58 |
|
||||
| Formes Token-2022 hors enveloppes | 33 |
|
||||
| Instructions Token Metadata exécutées par Token-2022 | 5 |
|
||||
| Instructions Token Group exécutées par Token-2022 | 4 |
|
||||
| Formes feuilles routables par le processor | 100 |
|
||||
|
||||
Les 48 tags ne constituent donc pas, seuls, une déclaration de couverture maximale. Les quinze
|
||||
tags d'extension sont des enveloppes et doivent être remplacés par leurs 58 sous-instructions dans
|
||||
le comptage des formes feuilles.
|
||||
|
||||
## Interfaces incorporées au processor
|
||||
|
||||
Les neuf discriminants ci-dessous sont exécutés par Token-2022 lorsqu'ils ciblent le Program ID
|
||||
`TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` :
|
||||
|
||||
| Interface | Instruction | Discriminant hexadécimal |
|
||||
|------------------------|--------------------------|--------------------------|
|
||||
| Token Metadata `1.0.0` | `initialize` | `d2e11ea258b84d8d` |
|
||||
| Token Metadata `1.0.0` | `update_field` | `dde9312db5cadcc8` |
|
||||
| Token Metadata `1.0.0` | `remove_key` | `ea122038598d25b5` |
|
||||
| Token Metadata `1.0.0` | `update_authority` | `d7e4a6e45464567b` |
|
||||
| Token Metadata `1.0.0` | `emit` | `faa6b4fa0d0cb846` |
|
||||
| Token Group `0.7.2` | `initialize_group` | `79716c2736330004` |
|
||||
| Token Group `0.7.2` | `update_group_max_size` | `6c25ab8ff81e126e` |
|
||||
| Token Group `0.7.2` | `update_group_authority` | `a1695801edddd8cb` |
|
||||
| Token Group `0.7.2` | `initialize_member` | `9820deb0dfed7486` |
|
||||
|
||||
Le propriétaire du décodage dépend toujours du Program ID exécutable : une CPI vers un programme
|
||||
tiers implémentant la même interface appartient au décodeur de ce programme tiers. Un TLV pointeur
|
||||
reste un lien et ne transfère pas la propriété du fait métier.
|
||||
|
||||
## Wire public des instructions hors enveloppes
|
||||
|
||||
Les 33 tags qui ne sont pas des enveloppes d'extension ont maintenant une ligne machine-readable
|
||||
dans `baseInstructionWireAudit`. Les tailles incluent toujours le tag `u8`. L'interface `3.1.1`
|
||||
encode les options d'instruction de manière compacte, distincte du `COption` à quatre octets des
|
||||
états de compte : `0` tient sur un octet ; `1` est suivi de 32 octets pour une clé publique ou de
|
||||
huit octets pour un `u64`.
|
||||
|
||||
Ce point donne notamment les tailles exactes suivantes :
|
||||
|
||||
- `InitializeMint` et `InitializeMint2` : 35 octets sans freeze authority, 67 avec ;
|
||||
- `SetAuthority` : 3 octets sans nouvelle autorité, 35 avec ;
|
||||
- `InitializeMintCloseAuthority` : 2 octets sans autorité, 34 avec ;
|
||||
- `UnwrapLamports` : 2 octets sans montant, 10 avec.
|
||||
|
||||
`GetAccountDataSize` et `Reallocate` consomment tout le suffixe par éléments `ExtensionType u16 LE` ;
|
||||
un suffixe impair ou un type inconnu est rejeté par l'unpacker d'interface. `UiAmountToAmount`
|
||||
consomme tout le suffixe comme UTF-8, y compris une chaîne vide au seul niveau de l'interface.
|
||||
`Batch` conserve tout le suffixe comme wire de records, dont les bornes et la validation processor
|
||||
restent à auditer.
|
||||
|
||||
La distinction critique est conservée dans la matrice : `TokenInstruction::unpack()` appelle
|
||||
`unpack_with_rest()` puis ignore le reste retourné. Pour les formes à préfixe fixe ou sans payload,
|
||||
un succès de cet unpacker public ne prouve donc pas la consommation de tout le buffer, ni
|
||||
l'acceptation du suffixe par le runtime. Le futur décodeur devra préserver et diagnostiquer tout
|
||||
suffixe borné jusqu'à ce que le chemin exact du processor figé en établisse la règle variante par
|
||||
variante.
|
||||
|
||||
## Règles de suffixe du processor figé
|
||||
|
||||
La comparaison a ensuite été menée sur les trois fichiers exacts du commit, avec leurs empreintes
|
||||
SHA-256 enregistrées dans la matrice :
|
||||
|
||||
- `interface/src/instruction.rs` : `672b3890…b3a287` ;
|
||||
- `program/src/pod_instruction.rs` : `7b546580…49c7e5` ;
|
||||
- `program/src/processor.rs` : `9d11b834…3032d`.
|
||||
|
||||
Les 33 formes hors enveloppes forment une partition complète et sans doublon :
|
||||
|
||||
| Règle processor | Tags | Nombre |
|
||||
|------------------------------------------------|----------------------------------------|-------:|
|
||||
| longueur totale Pod exacte | `2,3,4,7,8,12,13,14,15,16,18,19,23,35` | 14 |
|
||||
| préfixe Pod + option compacte, suffixe accepté | `0,6,20,25,45` | 5 |
|
||||
| tag seul, suffixe non inspecté et accepté | `1,5,9,10,11,17,22,31,32,38` | 10 |
|
||||
| reste entier interprété comme payload | `21,24,29` | 3 |
|
||||
| flux de records `Batch` | `255` | 1 |
|
||||
|
||||
`decode_instruction_data<T>()` exige exactement `1 + size_of::<T>()` octets. À l'inverse, les
|
||||
helpers privés des options compactes lisent la structure Pod, puis `0`, ou `1` suivi de la valeur,
|
||||
sans vérifier l'épuisement du buffer. Pour les dix branches sans données, le processor n'appelle
|
||||
aucun décodeur après la lecture du premier octet. Ces quinze formes acceptent donc réellement un
|
||||
suffixe, même si celui-ci ne porte aucune sémantique publiée. Le décodeur devra le préserver sous
|
||||
forme bornée et le diagnostiquer explicitement, sans créer de champ métier.
|
||||
|
||||
Pour `GetAccountDataSize` et `Reallocate`, chaque morceau de deux octets doit former un
|
||||
`ExtensionType` connu ; un dernier morceau incomplet ou un type inconnu échoue. Pour
|
||||
`UiAmountToAmount`, tout le reste doit être UTF-8, la chaîne vide restant admise à ce niveau.
|
||||
|
||||
`Batch` ne peut pas être vide : le plus petit wire total contient le tag externe, un header de deux
|
||||
octets et au moins un discriminant interne, soit quatre octets. Chaque record encode en `u8` son
|
||||
nombre de comptes et la longueur totale de son instruction interne. Un header ou payload tronqué
|
||||
échoue ; une longueur interne nulle échoue au dispatch ; un `Batch` interne est rejeté par
|
||||
`_process_inner`. Les fallbacks Token Metadata et Token Group restent en revanche joignables dans
|
||||
un record lorsque leur wire tient dans les 255 octets. Le processor n'impose pas de nombre maximal
|
||||
explicite de records au-delà des limites de transaction et de calcul : le décodeur devra donc poser
|
||||
sa propre borne.
|
||||
|
||||
## Comptes publiés par les builders de base
|
||||
|
||||
La matrice contient désormais une ligne ordonnée pour chacune des 33 formes hors enveloppes. Elle
|
||||
enregistre les metas exactes des 32 helpers Rust publiés : rôle, `writable`, `signer`, compte
|
||||
optionnel et queue multisig. `Batch` reste la seule forme sans helper Rust dédié pour construire
|
||||
ses metas ; `TokenInstruction::pack()` ne produit que ses données.
|
||||
|
||||
Les autorités simples et multisig partagent le contrat classique des builders : l'autorité est
|
||||
signataire lorsque la liste `signer_pubkeys` est vide ; sinon elle ne signe pas et chaque élément
|
||||
de la queue est ajouté en lecture seule avec le flag signataire. Le builder ne borne pas la taille
|
||||
de cette queue pour les opérations d'autorité et conserve ordre et doublons. Le runtime ne peut
|
||||
toutefois satisfaire qu'un multisig stockant de un à onze positions.
|
||||
|
||||
`InitializeMultisig` et `InitializeMultisig2` ont un contrat différent : les clés de configuration
|
||||
ne signent pas. Le builder exige `m` et `n` dans `1..=11` ainsi que `m <= n`. Le processor consomme
|
||||
tous les comptes restants comme clés de configuration, valide séparément `m` et `n`, mais ne
|
||||
répète pas le contrôle `m <= n`. Une instruction manuelle `m > n` peut donc initialiser un état que
|
||||
le builder refuse. Le décodeur doit conserver ce fait et le diagnostiquer sans le normaliser.
|
||||
|
||||
`SyncNative` publie deux helpers : le premier fournit seulement le compte natif modifiable ; le
|
||||
second ajoute Rent en lecture seule. Le processor traite effectivement le deuxième compte comme
|
||||
optionnel, mais ignore ceux qui suivraient. `Batch` découpe les comptes selon le compteur `u8` de
|
||||
chaque record ; les comptes externes restant après le dernier record ne sont pas rejetés. Ils
|
||||
devront rester visibles comme comptes non assignés.
|
||||
|
||||
Le contrat builder de `Reallocate` est fixé — compte à agrandir modifiable, payer modifiable et
|
||||
signataire, System Program, propriétaire conditionnel puis queue multisig — mais la consommation
|
||||
runtime détaillée reste ouverte jusqu'à reconfirmation du module séparé
|
||||
`program/src/extension/reallocate.rs` au même commit. Cette limite empêche de déclarer l'audit
|
||||
processor des comptes entièrement clos.
|
||||
|
||||
## Sources auditées
|
||||
|
||||
- dépôt `solana-program/token-2022`, commit figé ci-dessus ;
|
||||
- `interface/src/instruction.rs` et `interface/src/extension/**/instruction.rs` ;
|
||||
- `interface/src/extension/mod.rs` et les états d'extension ;
|
||||
- `program/src/processor.rs` et `program/src/extension/**/processor.rs` ;
|
||||
- dépôt `solana-program/token-metadata`, commit
|
||||
`552935e00710478541e3394a8b2624c9ba7f503f`, tag `interface@v1.0.0` ;
|
||||
- dépôt `solana-program/token-group`, commit
|
||||
`2645064953937118572735993ed0ac1d07db5f14`, tag `interface@v0.7.2`.
|
||||
|
||||
## Limites restantes
|
||||
|
||||
Cette tranche confirme le wire public, les règles runtime de suffixe et les metas officielles des
|
||||
builders des 33 formes hors enveloppes. Elle ne clôt pas encore l'audit par variante : consommation
|
||||
processor complète des comptes, données de preuve, builders des extensions, statuts de
|
||||
construction et compatibilité cluster doivent encore être renseignés avant le décodeur.
|
||||
|
||||
## Première tranche verticale d'extensions
|
||||
|
||||
Le décodeur structure désormais exactement douze feuilles issues de quatre enveloppes : les six
|
||||
instructions Transfer Fee, les deux Default Account State, les deux Memo Transfer et les deux CPI
|
||||
Guard. Les discriminants, champs numériques, options compactes et règles de longueur proviennent
|
||||
des interfaces figées au commit audité. Les onze autres enveloppes restent opaques et ne sont pas
|
||||
comptées comme décodées sémantiquement.
|
||||
|
||||
Cette tranche ne déclare encore aucune capacité exécuteur d'extension. Conformément à l'ordre du
|
||||
jalon, les projections propriétaires et leurs tests de non-duplication doivent être validés avant
|
||||
d'activer les builders officiels correspondants.
|
||||
|
||||
## Dispatch incorporé Token Metadata et Token Group
|
||||
|
||||
Le dispatch de production ne peut pas se limiter au premier octet de `TokenInstruction`. Après
|
||||
échec du décodage Token-2022 principal, le processor `11.0.0` audité tente successivement les
|
||||
interfaces Token Metadata puis Token Group. Les neuf discriminants déjà inventoriés dans la
|
||||
matrice sont donc désormais reconnus par le décodeur Token-2022 lorsque le Program ID réellement
|
||||
exécuté est `Tokenz...` : cinq feuilles Metadata et quatre feuilles Group.
|
||||
|
||||
Les chaînes Borsh Metadata sont bornées avant allocation et décodage. Les layouts Pod Group sont
|
||||
traités avec une longueur exacte. Cette intégration ne décode pas un programme tiers implémentant
|
||||
les mêmes interfaces : une CPI vers un autre Program ID reste la propriété du décodeur de ce
|
||||
programme.
|
||||
|
||||
L'égalité de surface à maintenir est désormais explicitement compilée et documentée :
|
||||
|
||||
```text
|
||||
33 feuilles hors enveloppes + 58 feuilles d'extensions + 5 Metadata + 4 Group = 100
|
||||
```
|
||||
|
||||
## Audit ElGamal restant
|
||||
|
||||
La présence de `spl-elgamal-registry-interface 0.2.1`, de son Program ID `regVY...` et de la seed
|
||||
`elgamal-registry` ne suffit pas à revendiquer sa couverture. La tranche dédiée doit encore fixer
|
||||
les discriminants et le wire exacts, la dérivation PDA, la relation wallet/owner, les clés ElGamal,
|
||||
les références de preuve, le lifecycle create/update, les autorités et signataires, les builders
|
||||
actuels ou expérimentaux et les preuves de déploiement cluster. Toute référence au registre depuis
|
||||
une instruction confidentielle Token-2022 reste un lien inter-programme et non une instruction du
|
||||
registre.
|
||||
Reference in New Issue
Block a user