# Top 30 Questions d'Entretien Web3 pour Développeurs Solidity
Décrocher un poste de développeur Solidity nécessite plus que des connaissances techniques — vous devez démontrer votre expertise sous pression. Ce guide couvre les 30 questions les plus fréquentes posées en entretien, avec des réponses complètes et des exemples de code.
Questions Fondamentales Solidity (1-10)
1. Quelle est la différence entre memory, storage et calldata ?
Réponse attendue :
storage: Données persistantes sur la blockchain, coûteuses en gas. Variables d'état.
memory: Données temporaires, existent uniquement pendant l'exécution de la fonction.
calldata: Comme memory mais en lecture seule, utilisé pour les paramètres de fonction external.
Exemple de code :
contract StorageExample {
uint[] public data; // storage
function addData(uint[] calldata newData) external {
// calldata : read-only, économique en gas
for (uint i = 0; i < newData.length; i++) {
data.push(newData[i]);
}
}
function processData() public view returns (uint[] memory) {
uint[] memory tempData = new uint[](data.length); // memory
for (uint i = 0; i < data.length; i++) {
tempData[i] = data[i] * 2;
}
return tempData;
}
}
Points bonus :
storage= pointeur vers l'état permanent
memory= copie temporaire
calldataéconomise du gas car pas de copie
2. Expliquez le pattern Checks-Effects-Interactions
Réponse attendue :
Pattern de sécurité pour prévenir les attaques de reentrancy :
Mauvais exemple (vulnérable) :
// ❌ VULNERABLE à reentrancy
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount);
// INTERACTION avant EFFECT
(bool success,) = msg.sender.call{value: amount}("");
require(success);
// EFFECT après INTERACTION — trop tard !
balances[msg.sender] -= amount;
}
Bon exemple (sécurisé) :
// ✅ SECURE avec Checks-Effects-Interactions
function withdraw(uint amount) external {
// 1. CHECKS
require(balances[msg.sender] >= amount, "Insufficient balance");
// 2. EFFECTS
balances[msg.sender] -= amount;
// 3. INTERACTIONS
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
3. Qu'est-ce qu'un modifier et donnez un exemple d'usage
Réponse attendue :
Les modifiers sont des fonctions réutilisables qui modifient le comportement d'autres fonctions.
contract AccessControl {
address public owner;
mapping(address => bool) public admins;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_; // Le code de la fonction s'exécute ici
}
modifier onlyAdmin() {
require(admins[msg.sender], "Not admin");
_;
}
modifier validAddress(address addr) {
require(addr != address(0), "Invalid address");
_;
}
function addAdmin(address admin)
external
onlyOwner
validAddress(admin)
{
admins[admin] = true;
}
function removeAdmin(address admin) external onlyOwner {
admins[admin] = false;
}
}
Points bonus :
- Modifiers peuvent avoir des paramètres
_;indique où le corps de la fonction s'exécute
- Peuvent être chaînés
4. Quelle est la différence entre transfer, send et call pour envoyer de l'ETH ?
Réponse attendue :
| Méthode | Gas Limit | Retourne | Recommandé |
|---------|-----------|----------|------------|
| transfer | 2300 | Revert si échec | ❌ Déprécié |
| send | 2300 | bool | ❌ Déprécié |
| call | Tout le gas | (bool, bytes) | ✅ Recommandé |
Exemple moderne (2026) :
// ✅ Méthode recommandée
function sendETH(address recipient, uint amount) external {
(bool success,) = recipient.call{value: amount}("");
require(success, "Transfer failed");
}
// ❌ Déprécié (gas limit trop bas)
function oldWay(address payable recipient, uint amount) external {
recipient.transfer(amount); // Peut échouer si recipient a fallback complexe
}
Pourquoi call est préféré :
- Pas de limite de gas artificielle
- Plus flexible
- Protection contre reentrancy avec ReentrancyGuard
5. Expliquez la différence entre view, pure et les fonctions normales
Réponse attendue :
- Normal : Peut lire ET modifier l'état
view: Peut lire l'état mais pas le modifier
pure: Ne peut ni lire ni modifier l'état
contract FunctionTypes {
uint public count;
// Fonction normale : modifie l'état
function increment() external {
count++;
}
// View : lit l'état
function getCount() external view returns (uint) {
return count;
}
// Pure : calcul pur, pas d'accès à l'état
function add(uint a, uint b) external pure returns (uint) {
return a + b;
}
}
Pièges courants :
viewcoûte 0 gas si appelée externe ment mais coûte du gas si appelée d'une fonction payante
purene peut pas lireblock.timestampoumsg.sender
6. Comment fonctionnent les events et pourquoi sont-ils importants ?
Réponse attendue :
Les events permettent de logger des données sur la blockchain de manière économique. Utilisés pour :
- Logging d'activité
- Déclencher des actions off-chain
- Historique immuable
contract TokenEvents {
event Transfer(
address indexed from,
address indexed to,
uint256 amount
);
event Approval(
address indexed owner,
address indexed spender,
uint256 amount
);
function transfer(address to, uint256 amount) external {
// ... logique de transfer ...
emit Transfer(msg.sender, to, amount);
}
}
Mot-clé indexed :
- Maximum 3 paramètres indexed par event
- Permet de filtrer efficacement les events
- Coûte un peu plus de gas
Listening d'events :
const filter = contract.filters.Transfer(null, recipientAddress);
const events = await contract.queryFilter(filter);
7. Qu'est-ce qu'une attaque de reentrancy et comment la prévenir ?
Réponse attendue :
Une attaque où un contrat malveillant rappelle le contrat victime avant que la première exécution ne soit terminée.
Contrat vulnérable :
// ❌ VULNERABLE
contract VulnerableBank {
mapping(address => uint) public balances;
function withdraw() external {
uint balance = balances[msg.sender];
require(balance > 0);
// DANGER : Appel externe AVANT mise à jour de l'état
(bool success,) = msg.sender.call{value: balance}("");
require(success);
balances[msg.sender] = 0; // Trop tard !
}
}
Attaquant :
contract Attacker {
VulnerableBank bank;
receive() external payable {
if (address(bank).balance > 0) {
bank.withdraw(); // Reentrancy !
}
}
function attack() external payable {
bank.deposit{value: msg.value}();
bank.withdraw(); // Premier appel
}
}
Solutions :
1. Checks-Effects-Interactions :
function withdraw() external {
uint balance = balances[msg.sender];
require(balance > 0);
balances[msg.sender] = 0; // Effect AVANT interaction
(bool success,) = msg.sender.call{value: balance}("");
require(success);
}
2. ReentrancyGuard :
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureBank is ReentrancyGuard {
mapping(address => uint) public balances;
function withdraw() external nonReentrant {
uint balance = balances[msg.sender];
require(balance > 0);
balances[msg.sender] = 0;
(bool success,) = msg.sender.call{value: balance}("");
require(success);
}
}
8. Expliquez la différence entre require, assert et revert
Réponse attendue :
| Fonction | Usage | Gas Refund | Cas d'usage |
|----------|-------|------------|-------------|
| require | Validation input/conditions | ✅ Oui | Conditions normales |
| assert | Invariants internes | ❌ Non | Bugs/erreurs impossibles |
| revert | Annulation avec message | ✅ Oui | Logique complexe |
Exemples :
contract ErrorHandling {
uint public totalSupply;
mapping(address => uint) public balances;
function transfer(address to, uint amount) external {
// require : validation des inputs
require(to != address(0), "Invalid recipient");
require(amount > 0, "Amount must be positive");
require(balances[msg.sender] >= amount, "Insufficient balance");
unchecked {
balances[msg.sender] -= amount;
balances[to] += amount;
}
// assert : vérifier l'invariant (totalSupply ne change pas)
assert(balances[msg.sender] + balances[to] == totalSupply);
}
function complexLogic(uint value) external {
if (value > 100) {
revert("Value too high");
}
if (value < 10) {
revert CustomError(value);
}
// ...
}
}
error CustomError(uint providedValue);
9. Qu'est-ce qu'un proxy pattern et pourquoi l'utiliser ?
Réponse attendue :
Les proxies permettent d'upgrade les smart contracts. Le proxy délègue les appels à une implementation contract.
Pourquoi utiliser :
- Upgrade de logique
- Économie de gas (deploy une fois)
- Fix de bugs critiques
Transparent Proxy :
// Proxy (ne change jamais)
contract Proxy {
address public implementation;
address public admin;
constructor(address _implementation) {
implementation = _implementation;
admin = msg.sender;
}
function upgrade(address newImplementation) external {
require(msg.sender == admin);
implementation = newImplementation;
}
fallback() external payable {
address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
// Implementation V1
contract ImplementationV1 {
uint public count;
function increment() external {
count++;
}
}
// Implementation V2 (upgrade)
contract ImplementationV2 {
uint public count;
function increment() external {
count += 2; // Nouvelle logique
}
function decrement() external {
count--; // Nouvelle fonction
}
}
Risques :
- Storage collisions
- Function selector clashes
- Complexité accrue
10. Comment optimiser le gas dans vos contrats ?
Réponse attendue :
Techniques principales :
uint256 au lieu de types plus petits// ❌ Plus coûteux (nécessite conversion)
uint8 a = 1;
uint8 b = 2;
// ✅ Moins coûteux
uint256 a = 1;
uint256 b = 2;
// ❌ 3 slots de storage
contract Unpacked {
uint256 a; // slot 0
uint256 b; // slot 1
uint256 c; // slot 2
}
// ✅ 2 slots seulement
contract Packed {
uint128 a; // slot 0
uint128 b; // slot 0
uint256 c; // slot 1
}
calldata pour les arrays en paramètre// ❌ Copie en memory
function process(uint[] memory data) external {
// ...
}
// ✅ Pas de copie
function process(uint[] calldata data) external {
// ...
}
// ❌ String coûteuse
require(balance >= amount, "Insufficient balance");
// ✅ Custom error économique
error InsufficientBalance(uint requested, uint available);
if (balance < amount) revert InsufficientBalance(amount, balance);
unchecked quand sûr (0.8.0+)// Overflow checking automatique coûte du gas
function increment() external {
for (uint i = 0; i < 100; i++) { // Vérifie overflow à chaque itération
count++;
}
}
// Pas de check si vous êtes certain
function incrementUnchecked() external {
unchecked {
for (uint i = 0; i < 100; i++) {
count++;
}
}
}
// ❌ Variable normale (SLOAD coûteux)
address public owner;
// ✅ Immutable (pas de SLOAD)
address public immutable owner;
// ✅ Constant (inliné dans bytecode)
uint public constant FEE = 100;
Questions Avancées (11-20)
11. Expliquez le fonctionnement de delegatecall
Réponse attendue :
delegatecall exécute le code d'un autre contrat dans le contexte du contrat appelant (même storage, même msg.sender).
contract Storage {
uint public value;
}
contract Logic {
uint public value;
function setValue(uint newValue) external {
value = newValue;
}
}
contract Proxy {
uint public value;
function setValueViaDelegate(address logic, uint newValue) external {
(bool success,) = logic.delegatecall(
abi.encodeWithSignature("setValue(uint256)", newValue)
);
require(success);
// value du Proxy est modifiée, pas celle de Logic
}
}
Différence avec call :
call: exécute dans le contexte du contrat appelé
delegatecall: exécute dans le contexte du contrat appelant
12. Qu'est-ce qu'un Merkle tree et comment l'utiliser pour une whitelist ?
Réponse attendue :
Un Merkle tree permet de prouver l'appartenance à un ensemble de manière efficace en gas.
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
contract WhitelistNFT {
bytes32 public merkleRoot;
constructor(bytes32 _merkleRoot) {
merkleRoot = _merkleRoot;
}
function mint(bytes32[] calldata proof) external {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(MerkleProof.verify(proof, merkleRoot, leaf), "Not whitelisted");
// Mint NFT
}
}
Avantages :
- Gas constant O(log n) au lieu de O(n)
- 1000 addresses = 10 preuves vs 1000 comparaisons
13. Comment gérer les decimals dans les tokens ERC-20 ?
Réponse attendue :
contract Token is ERC20 {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1000000 * 10**decimals());
// 1 million de tokens avec 18 decimals
}
function decimals() public pure override returns (uint8) {
return 18; // Standard Ethereum
}
}
Calculs avec decimals :
// Conversion user amount → wei amount
uint userAmount = 100; // 100 tokens
uint weiAmount = userAmount * 10**decimals(); // 100000000000000000000
// Conversion wei → user
uint displayAmount = weiAmount / 10**decimals(); // 100
14. Expliquez la différence entre ERC-20, ERC-721 et ERC-1155
Réponse attendue :
| Standard | Type | Use Case | Exemple |
|----------|------|----------|---------|
| ERC-20 | Fungible | Monnaies, tokens | USDC, UNI |
| ERC-721 | Non-Fungible | NFT uniques | CryptoPunks, BAYC |
| ERC-1155 | Multi-token | Gaming, collectibles | Enjin, Gods Unchained |
ERC-20 : Tous les tokens sont identiques
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
}
ERC-721 : Chaque token est unique
interface IERC721 {
function ownerOf(uint256 tokenId) external view returns (address);
function transferFrom(address from, address to, uint256 tokenId) external;
}
ERC-1155 : Multiple types dans un contrat
interface IERC1155 {
function balanceOf(address account, uint256 id) external view returns (uint256);
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata amounts,
bytes calldata data
) external;
}
15. Comment implémenteriez-vous un système de staking ?
Réponse attendue :
contract Staking {
IERC20 public stakingToken;
uint public rewardRate = 100; // tokens per block
mapping(address => uint) public stakedAmount;
mapping(address => uint) public startBlock;
function stake(uint amount) external {
require(amount > 0);
if (stakedAmount[msg.sender] > 0) {
claimRewards();
}
stakingToken.transferFrom(msg.sender, address(this), amount);
stakedAmount[msg.sender] += amount;
startBlock[msg.sender] = block.number;
}
function unstake(uint amount) external {
require(stakedAmount[msg.sender] >= amount);
claimRewards();
stakedAmount[msg.sender] -= amount;
stakingToken.transfer(msg.sender, amount);
}
function claimRewards() public {
uint reward = calculateReward(msg.sender);
if (reward > 0) {
stakingToken.transfer(msg.sender, reward);
startBlock[msg.sender] = block.number;
}
}
function calculateReward(address user) public view returns (uint) {
uint blocks = block.number - startBlock[user];
return blocks * stakedAmount[user] * rewardRate / 1e18;
}
}
16-20. Questions Rapides
16. Qu'est-ce que le selfdestruct et pourquoi est-il dangereux ?
- Détruit un contrat et envoie l'ETH
- Dangereux car force l'envoi d'ETH (bypass receive/fallback)
- Déprécié dans les futures versions de Solidity
17. Comment générer un nombre aléatoire sécurisé ?
- Ne PAS utiliser
block.timestampoublockhash(manipulables)
- Utiliser Chainlink VRF
- Alternative : Commit-reveal scheme
18. Qu'est-ce qu'un flashloan ?
- Prêt sans collatéral, remboursé dans la même transaction
- Utilisé pour arbitrage, liquidations
- Vecteur d'attaque si mal géré
19. Expliquez les access control patterns
- Ownable : un seul propriétaire
- AccessControl : rôles multiples (admin, minter, etc.)
- Multi-sig : requiert N/M signatures
20. Qu'est-ce que EIP-2535 (Diamond Standard) ?
- Contrats modulaires et upgradables
- Multiple facettes partageant le même storage
- Taille de contrat illimitée
Questions Système et Architecture (21-25)
21. Comment structureriez-vous un protocole DeFi complexe ?
Réponse attendue :
protocol/
├── core/
│ ├── Pool.sol # Logique principale
│ ├── Vault.sol # Gestion des fonds
│ └── PriceOracle.sol # Prix feeds
├── periphery/
│ ├── Router.sol # Entrée utilisateur
│ └── Helper.sol # Fonctions utilitaires
├── governance/
│ ├── Governor.sol # Votes
│ └── Timelock.sol # Délai d'exécution
└── token/
└── Token.sol # Token de gouvernance
Principes :
- Séparation core/periphery
- Single Responsibility
- Minimize external calls
- Emergency pause mechanism
22. Comment testeriez-vous un smart contract en production ?
Réponse attendue :
1. Tests unitaires :
describe("Token", function() {
it("should transfer tokens correctly", async function() {
await token.transfer(addr1.address, 100);
expect(await token.balanceOf(addr1.address)).to.equal(100);
});
});
2. Tests d'intégration :
- Tester les interactions entre contrats
- Simuler des scénarios réels
3. Fuzzing :
const { ethers } = require("hardhat");
const fc = require("fast-check");
it("fuzz test: never overflow", async function() {
await fc.assert(
fc.asyncProperty(fc.nat(), async (amount) => {
// Test avec montants aléatoires
})
);
});
4. Audit :
- Trail of Bits, OpenZeppelin, Certora
- Slither, Mythril (outils automatiques)
5. Bug bounty :
- Immunefi, Code4rena
- Récompenses pour trouver des bugs
23. Expliquez les différents types de wallets et leurs implications pour les dApps
Réponse attendue :
EOA (Externally Owned Account) :
- Contrôlé par clé privée
- MetaMask, Ledger, Rabby
- Pas de logique custom
Contract Wallets (Smart Wallets) :
- Gnosis Safe, Argent
- Multi-sig, recovery, limits
- ERC-4337 (Account Abstraction)
Implications pour dApps :
// ❌ Suppose que msg.sender est toujours EOA
function restrictedFunction() external {
require(msg.sender == tx.origin); // Bloque les smart wallets !
}
// ✅ Compatible avec smart wallets
function restrictedFunction() external {
require(hasPermission[msg.sender]);
}
24. Comment gérer les upgrades de contrats dans un environnement decentralized ?
Réponse attendue :
Avec gouvernance :
contract GovernedProxy {
address public implementation;
IGovernor public governor;
ITimelock public timelock;
function upgrade(address newImpl) external {
require(msg.sender == address(timelock));
implementation = newImpl;
}
}
// Processus :
// 1. Proposition de vote (governance token holders)
// 2. Vote (3-7 jours)
// 3. Queue dans timelock (2-7 jours délai)
// 4. Exécution de l'upgrade
Alternatives :
- Multi-sig temporaire → décentralisation progressive
- Immutable contracts (pas d'upgrade, deploy nouveau)
25. Expliquez le concept de composabilité dans DeFi
Réponse attendue :
Les protocoles DeFi sont des "money legos" — ils s'assemblent.
Exemple : Yield farming composé
// 1. Deposit USDC dans Aave → aUSDC
aavePool.deposit(usdc, amount);
// 2. Utiliser aUSDC comme collatéral pour emprunter DAI
aavePool.borrow(dai, borrowAmount);
// 3. Swap DAI pour USDC sur Uniswap
uniswapRouter.swapExactTokensForTokens(dai, usdc);
// 4. Loop : redéposer USDC → leverage
Risques de composabilité :
- Cascading failures
- Oracle manipulation
- Flash loan attacks
Questions Sécurité (26-30)
26. Quelles sont les attaques les plus courantes sur les smart contracts ?
Réponse attendue :
27. Comment protégeriez-vous contre le front-running ?
Réponse attendue :
1. Commit-reveal scheme :
contract AntiMEV {
mapping(address => bytes32) public commits;
function commit(bytes32 hash) external {
commits[msg.sender] = hash;
}
function reveal(uint value, bytes32 salt) external {
bytes32 hash = keccak256(abi.encodePacked(value, salt));
require(commits[msg.sender] == hash);
// Execute action avec value
delete commits[msg.sender];
}
}
2. MEV protection services :
- Flashbots Protect
- Private mempools
- Submarine sends
28. Expliquez une attaque par flash loan
Réponse attendue :
Scénario d'attaque :
contract FlashLoanAttack {
function attack() external {
// 1. Emprunter $10M USDC via flash loan (Aave)
aave.flashLoan(10_000_000e6);
}
function executeOperation(uint amount) external {
// 2. Manipuler le prix d'un oracle peu liquide
vulnerablePool.swap(amount);
// 3. Exploiter le prix manipulé
targetProtocol.borrow(); // Emprunte trop grâce au prix gonflé
// 4. Rembourser le flash loan
aave.repay(amount + fee);
// 5. Profit
}
}
Protection :
- Utiliser des oracles décentralisés (Chainlink)
- Time-Weighted Average Price (TWAP)
- Limites de slippage
- Circuit breakers
29. Comment auditer un smart contract ?
Checklist d'audit :
1. Revue manuelle du code
- Logique métier correcte ?
- Access control approprié ?
- Checks-Effects-Interactions respecté ?
2. Outils automatiques
- Slither : détection de patterns dangereux
- Mythril : symbolic execution
- Echidna : fuzzing
3. Tests
- Couverture de code >95%
- Tests d'invariants
- Integration tests
4. Vérifications spécifiques
- Reentrancy protections
- Integer overflow (< 0.8.0)
- Timestamp dependence
- Randomness source
- Gas griefing
30. Décrivez une vulnérabilité que vous avez trouvée (ou question ouverte)
Réponse type :
"J'ai trouvé une vulnérabilité dans un contrat de staking où la fonction unstake ne vérifiait pas si l'utilisateur avait déjà unstake. En appelant unstake deux fois rapidement, un attaquant pouvait retirer deux fois le même montant.
Le fix était simple : ajouter un check et mettre à jour l'état AVANT le transfer (Checks-Effects-Interactions).
J'ai également ajouté un test spécifique pour ce cas et augmenté la couverture de tests à 98%."
Conclusion
Ces 30 questions couvrent l'essentiel de ce qu'un développeur Solidity doit maîtriser. Pour vous préparer :
Sur Solingo, vous pouvez pratiquer tous ces concepts dans un environnement interactif avec feedback immédiat.
Bonne chance pour votre entretien !