This commit is contained in:
2026-07-23 16:37:12 +02:00
parent 99c345f2f2
commit 0da75c1311
2159 changed files with 230833 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
<!-- file: docs/SPL_TOKEN_2022_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.