15 KiB
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 deField::Keylorsque le tag vaut3, puisvalue;RemoveKey:key, après l'octet booléenidempotent.
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.tomldéclare le processorspl-token-2022 11.0.0;- sa dépendance path demande l'interface
3.1.0, satisfaite par le package workspace3.1.1; - le
Cargo.locksource résoutspl-token-group-interface 0.7.2,spl-token-metadata-interface 1.0.0etspl-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 :
PodTokenInstruction, discriminantu8;- si ce décodage échoue,
TokenMetadataInstruction, discriminant SPL de huit octets ; - si ce décodage échoue,
TokenGroupInstruction, discriminant SPL de huit octets ; - 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 :
InitializeMintetInitializeMint2: 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.rsetinterface/src/extension/**/instruction.rs;interface/src/extension/mod.rset les états d'extension ;program/src/processor.rsetprogram/src/extension/**/processor.rs;- dépôt
solana-program/token-metadata, commit552935e00710478541e3394a8b2624c9ba7f503f, taginterface@v1.0.0; - dépôt
solana-program/token-group, commit2645064953937118572735993ed0ac1d07db5f14, taginterface@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 :
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.