Deep EVM #8 : Construire un échange de tokens en Yul pur
Engineering Team
Pourquoi construire en Yul pur ?
Jusqu’ici dans cette série, nous avons étudié les opcodes EVM, la mémoire, le gas, la sécurité et les bases de Yul. Il est temps de tout rassembler dans un projet concret : un contrat d’échange de tokens écrit entièrement en Yul — sans une seule ligne de Solidity de haut niveau.
Ce projet sert deux objectifs :
- Consolider votre compréhension de chaque concept couvert précédemment
- Démontrer les économies de gas réalisables quand vous contrôlez chaque opcode
Architecture du contrat
Notre échangeur aura trois fonctions :
swap(address tokenIn, address tokenOut, uint256 amountIn)— Échange un token contre un autregetRate(address tokenA, address tokenB)→uint256— Retourne le taux de changeowner()→address— Retourne le propriétaire du contrat
Dispatch des fonctions
La première chose que fait tout contrat est la dispatch des fonctions — router l’appel vers le bon code basé sur le sélecteur de fonction.
object "TokenSwap" {
code {
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
code {
// Extraire le sélecteur (4 premiers octets du calldata)
let selector := shr(224, calldataload(0))
switch selector
case 0xd004f0f7 /* swap(address,address,uint256) */ {
swap()
}
case 0xf1a05266 /* getRate(address,address) */ {
getRate()
}
case 0x8da5cb5b /* owner() */ {
mstore(0x00, sload(0))
return(0x00, 0x20)
}
default {
revert(0, 0)
}
function swap() {
// Lire les arguments depuis calldata
let tokenIn := calldataload(0x04)
let tokenOut := calldataload(0x24)
let amountIn := calldataload(0x44)
// Vérifier que l'appelant a approuvé le montant
// ... transferFrom tokenIn ...
// ... calculer amountOut ...
// ... transfer tokenOut ...
}
function getRate() {
let tokenA := calldataload(0x04)
let tokenB := calldataload(0x24)
let rate := sload(rateSlot(tokenA, tokenB))
mstore(0x00, rate)
return(0x00, 0x20)
}
function rateSlot(tokenA, tokenB) -> slot {
mstore(0x00, tokenA)
mstore(0x20, tokenB)
slot := keccak256(0x00, 0x40)
}
}
}
}
Interactions ERC-20 en Yul
Appeler transfer et transferFrom sur des tokens ERC-20 depuis Yul nécessite de construire manuellement le calldata :
function safeTransfer(token, to, amount) {
// Préparer calldata: transfer(address,uint256)
mstore(0x00, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
mstore(0x04, to)
mstore(0x24, amount)
let success := call(gas(), token, 0, 0x00, 0x44, 0x00, 0x20)
// Vérifier le succès ET la valeur de retour
if iszero(success) { revert(0, 0) }
// Certains tokens ne retournent pas de booléen (USDT)
if returndatasize() {
if iszero(mload(0x00)) { revert(0, 0) }
}
}
Comparaison de gas
| Opération | Solidity | Yul pur | Économie |
|---|---|---|---|
| Dispatch | ~120 gas | ~45 gas | 63 % |
| Lecture de taux | ~2500 gas | ~2200 gas | 12 % |
| Swap complet | ~85 000 gas | ~62 000 gas | 27 % |
| Taille du bytecode | ~2400 octets | ~800 octets | 67 % |
Le Yul pur élimine : le dispatch if-else de Solidity, les vérifications SafeMath, l’encodage ABI, le pointeur de mémoire libre et le hash de métadonnées.
Leçons apprises
- Le dispatch est le premier gain — Remplacer la chaîne if-else par un switch compact économise du gas à chaque appel.
- La gestion mémoire manuelle paie — Réutiliser les mêmes positions mémoire au lieu d’allouer via le pointeur libre.
- L’interaction avec les tokens est délicate — Les tokens non standard (pas de retour booléen) nécessitent une gestion robuste.
- La lisibilité souffre — Le Yul pur est plus difficile à auditer. Documentez abondamment.
Conclusion
Construire un échange de tokens en Yul pur démontre le potentiel d’optimisation quand vous contrôlez chaque aspect du bytecode. Les 27 % d’économie de gas sur un swap se traduisent en coûts réduits pour chaque utilisateur.
Dans le prochain article, nous passerons à Huff — un niveau d’abstraction encore plus bas que Yul, où vous écrivez directement les opcodes.