
Analyse approfondie de l'incident de vol sur Optimism : attaque par rejeu lors du déploiement de contrats sur le réseau Layer2
TechFlow SélectionTechFlow Sélection

Analyse approfondie de l'incident de vol sur Optimism : attaque par rejeu lors du déploiement de contrats sur le réseau Layer2
Cobo analyse et reproduit l'incident de vol sur Optimism, offrant une interprétation technique complète sous plusieurs angles, notamment la chronologie, la méthode d'attaque, la génération des adresses de contrat Ethereum et les détails techniques.
Synthèse de l'incident
Fin mai, la fondation Optimism a embauché le market maker Wintermute pour fournir de la liquidité au token OP. La fondation a transféré 20 millions de tokens OP à l'équipe Wintermute à cette fin. Une erreur de communication est survenue : Wintermute a fourni une adresse de réception sur Layer1 (Ethereum), qui n'était pas encore déployée sur Layer2 (Optimism). Après que la fondation eut envoyé les fonds sur l'adresse Layer2, l'équipe Wintermute a découvert l'erreur. Avant que le compte ne soit corrigé, un attaquant a obtenu les droits de contrôle du compte et a commencé à vendre les tokens OP qu'il contenait.
Chronologie
● 26 et 27 mai - La fondation Optimism envoie 1 OP puis 1 million de OP à l'adresse 0x4f3a120e72c76c22ae802d129f599bfdbc31cb81 fournie par Wintermute, comme test.
● 27 mai - La fondation transfère les 19 millions de OP restants à cette même adresse.
● 30 mai - L'équipe Wintermute découvre l'erreur, contacte la fondation Optimism et l'équipe Gnosis Safe pour demander de l'aide afin de récupérer les fonds. Après consultation avec ces deux équipes, elle conclut que le compte est toujours sécurisé, inaccessible à toute autre partie, et que les fonds peuvent être récupérés avec l'aide de Gnosis Safe. Un plan de correction des permissions est prévu pour le 7 juin.
● 1er juin - L'attaquant déploie un contrat malveillant contenant en dur l'adresse du factory, indiquant qu'il a déjà élaboré toute la procédure d'attaque.
● 5 juin - L'attaquant lance l'attaque, prend le contrôle du compte cible et transfère 1 million de OP vers Tornado Cash pour les échanger.
● 9 juin - Wintermute publie une déclaration assumant l'entière responsabilité de cette erreur, s'engage à racheter tous les tokens vendus par l'attaquant et demande à ce dernier de rendre les tokens restants. Quatre heures après la publication, l'attaquant transfère à nouveau 1 million de OP vers un compte privé.
Parcours d'attaque
1. Tous les contrats de coffre-fort Gnosis Safe sont déployés via un contrat Proxy Factory. Pour prendre le contrôle d'une adresse cible, il faut appeler le Proxy Factory afin de déployer un proxy sur cette adresse.
2. Avant l'attaque, le contrat Proxy Factory n'était pas encore déployé sur Layer2 (Optimism). L'attaquant a réutilisé la transaction de déploiement du factory depuis Layer1 pour déployer un nouveau contrat factory sur Layer2.
3. Sur Layer2, l'attaquant a appelé plusieurs fois la méthode createProxy du contrat factory pour déployer des proxies, augmentant ainsi le nonce du factory jusqu'à ce que le proxy soit déployé exactement à l'adresse cible.
4. Lors de l'appel à createProxy, l'attaquant a défini le paramètre masterCopy comme étant l'adresse de son propre contrat contrôlé, qui devient alors l'implémentation du proxy. Ainsi, l'attaquant obtient le contrôle total de l'adresse cible.
Génération des adresses de contrat Ethereum
Pour comprendre pourquoi cet attaque permet de déployer un contrat à une adresse précise, examinons le mécanisme de génération des adresses de contrat sur Ethereum.
Une adresse de contrat n'a pas de clé privée associée ; elle est déterminée au moment du déploiement. Deux méthodes existent pour déployer un contrat : CREATE et CREATE2. Avec CREATE, l'adresse est calculée par hachage RLP de l'adresse expéditrice et de son nonce, puis hachée avec SHA3. Les 20 derniers octets du hash forment l'adresse du nouveau contrat.
nouvelle_adresse = hash(expéditeur, nonce)
En JavaScript, cela peut s'écrire :
const Web3 = require("web3");
const RLP = require("rlp");
const nonce = 0;
const account = "0xa990077c3205cbdf861e17fa532eeb069ce9ff96";
var e = RLP.encode(
[
account,
nonce,
],
);
const nonceHash = Web3.utils.sha3(Buffer.from(e));
console.log(nonceHash.substring(26));
Pour un compte EOA, chaque transaction augmente le nonce de 1. Pour un contrat, chaque création d'un nouveau contrat augmente aussi le nonce de 1.
CREATE2 est généralement appelé par un contrat intelligent. Son algorithme est le suivant (encodage similaire à CREATE) :
nouvelle_adresse = hash(0xFF, expéditeur, salt, bytecode)
● 0xFF est une constante fixe
● expéditeur : adresse initiatrice du déploiement
● salt : valeur arbitraire choisie par l'expéditeur
● bytecode : code du contrat à déployer
● CREATE2 évite l'utilisation d'un nonce incrémental, en le remplaçant par un « salt » contrôlable, permettant ainsi un meilleur contrôle de l'adresse de déploiement.
Détails techniques
Processus de création du proxy sur Layer1
D'après la section précédente, pour déployer un contrat à une adresse spécifique sur Layer2, il faut d'abord connaître sa méthode de déploiement sur Layer1. Le proxy multisignature Wintermute sur Layer1 a été créé par la transaction https://etherscan.io/tx/0xd705178d68551a6a6f65ca74363264b32150857a26dd62c27f3f96b8ec69ca01, utilisant le Proxy Factory 1.1.1. La méthode utilisée est createProxy. Voici son implémentation :
function createProxy(address masterCopy, bytes memory data)
public
returns (Proxy proxy)
{
proxy = new Proxy(masterCopy);
if (data.length > 0)
// solium-disable-next-line security/no-inline-assembly
assembly {
if eq(call(gas, proxy, 0, add(data, 0x20), mload(data), 0, 0), 0) { revert(0, 0) }
}
emit ProxyCreation(proxy);
}
On voit ici l'utilisation du mot-clé new, qui appelle en interne l'opcode CREATE, et non CREATE2.
Ainsi, en appelant la même méthode createProxy sur Layer2 avec le même Proxy Factory 1.1.1, si le nonce est identique au moment du déploiement, on obtiendra un proxy à la même adresse que sur Layer1.
Réexécution du déploiement du Gnosis Safe Proxy Factory 1.1.1 sur Layer2
Avant l'attaque, le Proxy Factory 1.1.1 n'était pas encore déployé sur Optimism. Il fallait donc d'abord déployer ce contrat à la même adresse que sur Layer1.
Sur Layer1, l'adresse du Proxy Factory 1.1.1 est 0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B, créée en 2019 via la transaction https://etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261.
Le décodage de cette transaction (https://www.ethereumdecoder.com/?search=0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261) donne :
{
"nonce": 2,
"gasPrice": {
"_hex": "0x02540be400"
},
"gasLimit": {
"_hex": "0x114343"
},
"to": "0x00",
"value": {
"_hex": "0x00"
},
"data": "xxxx...xxxx",
"v": 28,
"r": "0xc7841dea9284aeb34c2fb783843910adfdc057a37e92011676fddcc33c712926",
"s": "0x4e59ce12b6a06da8f7ec7c2d734787bd413c284fc3d1be3a70903ebc23945e8c"
}
On observe que v = 28. Selon l'EIP-155, les transactions avec v = 27 ou 28 n'utilisent pas la signature EIP-155, donc le ChainID n'est pas inclus dans la signature.
Cette transaction de déploiement sur Layer1 peut donc être directement réexécutée sur Layer2 (Optimism). La transaction de réexécution est disponible ici : https://optimistic.etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261. Elle déploie un nouveau Gnosis Safe Proxy Factory à la même adresse que sur Layer1 : https://optimistic.etherscan.io/address/0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b.

On voit sur l'image que ce contrat a été créé il y a 4 jours (au début de l'attaque), mais qu'il avait déjà des appels antérieurs avant sa création.
À ce stade, le nonce du compte factory est 0. Chaque déploiement de contrat augmente ce nonce de 1. En appelant répétitivement createProxy, on peut incrémenter le nonce jusqu'à atteindre l'adresse cible.
Déploiement du proxy à l'adresse cible sur Layer2
Contrat d'attaque : 0xe7145dd6287ae53326347f3a6694fcf2954bcd8a[1]
L'attaquant utilise ce contrat pour appeler massivement la méthode createProxy du ProxyFactory[2], en définissant l'adresse masterCopy comme étant celle de son propre contrat. Cela crée de nombreux proxies dont l'implémentation est contrôlée par l'attaquant, lui permettant de prendre le contrôle du compte cible.
Procédure détaillée :
L'attaquant appelle plusieurs fois le contrat d'attaque depuis un compte EOA[3]. Chaque transaction crée 162 proxies. Finalement, dans la transaction https://optimistic.etherscan.io/tx/0x00a3da68f0f6a69cb067f09c3f7e741a01636cbc27a84c603b468f65271d415b, le proxy à l'adresse 0x4f3a120e72c76c22ae802d129f599bfdbc31cb81 est créé.
L'attaquant utilise un second compte EOA[4] comme propriétaire du proxy pour en contrôler les opérations.
> eth_call 0x4f3a120E72C76c22ae802D129F599BFDbc31cb81 owner()
0x0000000000000000000000008bcfe4f1358e50a1db10025d731c8b3b17f04dbb
Leçons apprises
Avec l'émergence de nombreuses solutions Layer2 (comme Optimism, Arbitrum) et de sidechains (comme xDai, Polygon), les actifs et dApps sont désormais dispersés sur différents réseaux. Toutefois, les comportements des comptes EOA et des comptes intelligents comme Gnosis Safe diffèrent considérablement entre les chaînes.
Les comptes EOA, basés sur des clés privées, peuvent être utilisés sur tous les réseaux. En revanche, les comptes contractuels n'ont pas de clé privée et nécessitent un déploiement et une initialisation complexes pour définir leur contrôle et fonctionnalité, les rendant inutilisables directement d'une chaîne à l'autre.
De même, les contrats d'infrastructure ne sont pas automatiquement dupliqués ni disponibles de manière équivalente sur chaque réseau.
Par conséquent, lorsqu'on manipule des contrats sur différents niveaux de réseau, il convient d'être extrêmement prudent. Il faut d'abord vérifier que les adresses, codes et états des contrats sont identiques entre le réseau cible et le réseau principal Ethereum. Dans les cas de logique complexe, il faut même s'assurer de la compatibilité fonctionnelle entre le réseau cible et le contrat. Il ne faut jamais supposer que les adresses Layer1 se reflètent automatiquement sur Layer2 ou les sidechains, afin d'éviter de nouveaux incidents comme celui subi par Wintermute.
Références
[1] Contrat d'attaque : https://optimistic.etherscan.io/address/0xe7145dd6287ae53326347f3a6694fcf2954bcd8a
[2] ProxyFactory : https://optimistic.etherscan.io/address/0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b#code
[3] Compte EOA de l'attaquant 1 : https://optimistic.etherscan.io/address/0x60B28637879B5a09D21B68040020FFbf7dbA5107
[4] Compte EOA de l'attaquant 2 : https://optimistic.etherscan.io/address/0x8bcfe4f1358e50a1db10025d731c8b3b17f04dbb
Bienvenue dans la communauté officielle TechFlow
Groupe Telegram :https://t.me/TechFlowDaily
Compte Twitter officiel :https://x.com/TechFlowPost
Compte Twitter anglais :https://x.com/BlockFlow_News














