# Contrôle d'Accès en Solidity — Patterns et Bonnes Pratiques
Le contrôle d'accès mal implémenté est l'une des vulnérabilités les plus courantes et les plus coûteuses en développement de smart contracts. Des fonctions d'administration exposées, des vérifications de permissions manquantes, ou une logique de rôle défectueuse peuvent permettre aux attaquants de drainer des fonds, de prendre le contrôle de contrats, ou de manipuler l'état critique.
Dans ce guide, nous allons explorer les patterns de contrôle d'accès — du simple onlyOwner aux systèmes RBAC (Role-Based Access Control) sophistiqués — et apprendre à les implémenter de manière sécurisée.
Pourquoi le Contrôle d'Accès est Important
Les smart contracts sont immuables et gèrent souvent des actifs de grande valeur. Sans contrôle d'accès approprié :
- N'importe qui peut appeler des fonctions d'administration
- Les attaquants peuvent modifier les paramètres critiques
- Les fonds peuvent être retirés par des parties non autorisées
- La logique métier peut être contournée
Cas réel : Le hack Parity Wallet (2017) a gelé $150M en ETH parce qu'une fonction initWallet non protégée permettait à quiconque de devenir propriétaire.
Pattern 1 : Modificateur Ownable Simple
Le pattern le plus basique : un seul propriétaire avec des privilèges administratifs.
contract Ownable {
address public owner;
constructor() {
owner = msg.sender; // Déployeur devient propriétaire
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0), "Invalid address");
owner = newOwner;
}
}
contract MyContract is Ownable {
uint256 public criticalValue;
function setCriticalValue(uint256 _value) external onlyOwner {
criticalValue = _value;
}
}
Implémentation OpenZeppelin Ownable
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is Ownable {
// Le constructeur d'Ownable définit msg.sender comme propriétaire
function adminFunction() external onlyOwner {
// Seul le propriétaire peut appeler ceci
}
}
Améliorations d'OpenZeppelin :
- Transfert de propriété en deux étapes (Ownable2Step)
- Renonciation à la propriété
- Events pour les changements de propriétaire
import "@openzeppelin/contracts/access/Ownable2Step.sol";
contract SecureContract is Ownable2Step {
function transferOwnership(address newOwner) public override onlyOwner {
_transferOwnership(newOwner); // Nécessite acceptation du nouveau propriétaire
}
function renounceOwnership() public override onlyOwner {
_transferOwnership(address(0)); // Supprime le propriétaire
}
}
Pattern 2 : Control d'Accès Basé sur les Rôles (RBAC)
Pour des systèmes plus complexes, plusieurs rôles avec différentes permissions.
Implémentation Manuelle Basique
contract RoleBasedAccess {
mapping(address => bool) public admins;
mapping(address => bool) public moderators;
address public owner;
constructor() {
owner = msg.sender;
admins[msg.sender] = true;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier onlyAdmin() {
require(admins[msg.sender], "Not admin");
_;
}
modifier onlyModerator() {
require(moderators[msg.sender], "Not moderator");
_;
}
function addAdmin(address _admin) external onlyOwner {
admins[_admin] = true;
}
function addModerator(address _mod) external onlyAdmin {
moderators[_mod] = true;
}
function adminFunction() external onlyAdmin {
// Logique admin
}
function modFunction() external onlyModerator {
// Logique modérateur
}
}
OpenZeppelin AccessControl
La bibliothèque AccessControl d'OpenZeppelin fournit un système RBAC robuste.
import "@openzeppelin/contracts/access/AccessControl.sol";
contract AdvancedDAO is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MODERATOR_ROLE = keccak256("MODERATOR_ROLE");
bytes32 public constant VOTER_ROLE = keccak256("VOTER_ROLE");
constructor() {
// Le déployeur obtient le rôle admin par défaut
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(ADMIN_ROLE, msg.sender);
// Définir la hiérarchie des rôles
_setRoleAdmin(MODERATOR_ROLE, ADMIN_ROLE);
_setRoleAdmin(VOTER_ROLE, MODERATOR_ROLE);
}
function createProposal(string memory description)
external
onlyRole(VOTER_ROLE)
{
// Logique de création de proposition
}
function moderateContent(uint256 proposalId)
external
onlyRole(MODERATOR_ROLE)
{
// Logique de modération
}
function setSystemParameter(uint256 value)
external
onlyRole(ADMIN_ROLE)
{
// Logique admin
}
function grantVoterRole(address account) external {
grantRole(VOTER_ROLE, account); // Vérifie automatiquement que msg.sender a MODERATOR_ROLE
}
}
Fonctionnalités clés :
grantRole(bytes32 role, address account): Assigner un rôle
revokeRole(bytes32 role, address account): Révoquer un rôle
hasRole(bytes32 role, address account): Vérifier un rôle
renounceRole(bytes32 role, address account): Renoncer à un rôle
_setRoleAdmin(bytes32 role, bytes32 adminRole): Hiérarchie de rôles
Pattern 3 : Liste Blanche (Allowlist)
Restreindre l'accès à un ensemble spécifique d'adresses.
contract Allowlist {
mapping(address => bool) public allowedAddresses;
modifier onlyAllowed() {
require(allowedAddresses[msg.sender], "Not allowed");
_;
}
function addToAllowlist(address _address) external {
// Logique de contrôle d'accès pour ajouter
allowedAddresses[_address] = true;
}
function removeFromAllowlist(address _address) external {
allowedAddresses[_address] = false;
}
function restrictedFunction() external onlyAllowed {
// Seules les adresses autorisées peuvent appeler
}
}
Cas d'usage :
- Lancements privés / accès early
- Programmes KYC/AML
- Accès restreint aux partenaires
Pattern 4 : Timelock / Delay d'Exécution
Imposer un délai entre la proposition d'une action admin et son exécution.
contract TimelockController {
uint256 public constant DELAY = 2 days;
struct PendingAction {
uint256 executeAfter;
bool executed;
}
mapping(bytes32 => PendingAction) public pendingActions;
function proposeAction(bytes32 actionId) external onlyOwner {
require(pendingActions[actionId].executeAfter == 0, "Already proposed");
pendingActions[actionId] = PendingAction({
executeAfter: block.timestamp + DELAY,
executed: false
});
}
function executeAction(bytes32 actionId) external onlyOwner {
PendingAction storage action = pendingActions[actionId];
require(action.executeAfter != 0, "Not proposed");
require(block.timestamp >= action.executeAfter, "Too early");
require(!action.executed, "Already executed");
action.executed = true;
// Exécuter l'action
}
}
OpenZeppelin TimelockController :
import "@openzeppelin/contracts/governance/TimelockController.sol";
contract GovernedContract {
TimelockController public timelock;
constructor(address timelockAddress) {
timelock = TimelockController(payable(timelockAddress));
}
function criticalFunction() external {
require(msg.sender == address(timelock), "Must go through timelock");
// Logique critique
}
}
Pattern 5 : Multisig / Gnosis Safe
Nécessiter plusieurs signatures pour les actions critiques.
contract SimpleMultisig {
address[] public owners;
uint256 public required;
mapping(uint256 => mapping(address => bool)) public confirmations;
uint256 public transactionCount;
constructor(address[] memory _owners, uint256 _required) {
require(_owners.length >= _required, "Invalid required count");
owners = _owners;
required = _required;
}
function submitTransaction(address to, uint256 value, bytes memory data)
external
returns (uint256)
{
uint256 txId = transactionCount++;
// Stocker la transaction
return txId;
}
function confirmTransaction(uint256 txId) external {
require(isOwner(msg.sender), "Not owner");
confirmations[txId][msg.sender] = true;
if (isConfirmed(txId)) {
executeTransaction(txId);
}
}
function isConfirmed(uint256 txId) public view returns (bool) {
uint256 count = 0;
for (uint256 i = 0; i < owners.length; i++) {
if (confirmations[txId][owners[i]]) count++;
}
return count >= required;
}
function isOwner(address account) public view returns (bool) {
for (uint256 i = 0; i < owners.length; i++) {
if (owners[i] == account) return true;
}
return false;
}
function executeTransaction(uint256 txId) internal {
// Exécuter la transaction
}
}
En production, utilisez Gnosis Safe — battle-tested et audité.
Vulnérabilités de Contrôle d'Accès Courantes
1. Fonction d'Initialisation Non Protégée
// ❌ VULNÉRABLE
contract Vulnerable {
address public owner;
function initialize(address _owner) external {
owner = _owner; // N'importe qui peut appeler !
}
}
Correction :
// ✅ SÉCURISÉ
contract Secure {
address public owner;
bool private initialized;
function initialize(address _owner) external {
require(!initialized, "Already initialized");
owner = _owner;
initialized = true;
}
}
Ou utilisez le pattern Initializable d'OpenZeppelin pour les proxies.
2. Vérification Manquante de tx.origin vs msg.sender
// ❌ VULNÉRABLE au phishing
function withdraw() external {
require(tx.origin == owner, "Not owner"); // JAMAIS utiliser tx.origin !
// ...
}
Correction :
// ✅ SÉCURISÉ
function withdraw() external {
require(msg.sender == owner, "Not owner");
// ...
}
Pourquoi tx.origin est dangereux :
Si le propriétaire appelle un contrat malveillant, ce contrat peut appeler votre fonction withdraw() et tx.origin sera toujours le propriétaire.
3. Oubli de Modificateur
// ❌ VULNÉRABLE
function setAdmin(address newAdmin) external {
admin = newAdmin; // Pas de vérification !
}
Correction :
// ✅ SÉCURISÉ
function setAdmin(address newAdmin) external onlyOwner {
admin = newAdmin;
}
4. État Incohérent Après Transfert de Propriété
// ❌ VULNÉRABLE
function transferOwnership(address newOwner) external onlyOwner {
owner = newOwner; // Ancien propriétaire perd l'accès immédiatement
}
Correction : Transfert en 2 Étapes
// ✅ SÉCURISÉ
address public pendingOwner;
function transferOwnership(address newOwner) external onlyOwner {
pendingOwner = newOwner;
}
function acceptOwnership() external {
require(msg.sender == pendingOwner, "Not pending owner");
owner = pendingOwner;
pendingOwner = address(0);
}
Tests de Contrôle d'Accès
Avec Foundry
// test/AccessControl.t.sol
import "forge-std/Test.sol";
import "../src/MyContract.sol";
contract AccessControlTest is Test {
MyContract public myContract;
address public owner = address(1);
address public user = address(2);
function setUp() public {
vm.prank(owner);
myContract = new MyContract();
}
function testOnlyOwnerCanCallAdminFunction() public {
vm.prank(user);
vm.expectRevert("Not owner");
myContract.adminFunction();
vm.prank(owner);
myContract.adminFunction(); // Devrait réussir
}
}
Avec Hardhat
const { expect } = require("chai");
describe("Access Control", function () {
let contract, owner, user;
beforeEach(async function () {
[owner, user] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("MyContract");
contract = await Contract.deploy();
});
it("Should revert if non-owner calls admin function", async function () {
await expect(
contract.connect(user).adminFunction()
).to.be.revertedWith("Not owner");
});
it("Should allow owner to call admin function", async function () {
await expect(
contract.connect(owner).adminFunction()
).to.not.be.reverted;
});
});
Bonnes Pratiques
✅ Utilisez les bibliothèques éprouvées (OpenZeppelin Ownable, AccessControl)
✅ Implémentez le transfert de propriété en 2 étapes pour éviter les accidents
✅ N'utilisez JAMAIS tx.origin pour l'authentification
✅ Utilisez des events pour tous les changements de rôles
✅ Documentez qui devrait avoir quels rôles
✅ Utilisez timelock pour les opérations critiques
✅ Envisagez multisig pour les protocoles de haute valeur
✅ Testez à la fois les chemins autorisés ET non autorisés
✅ Auditez le code de contrôle d'accès en priorité
✅ Minimisez le nombre de fonctions admin
Outils d'Analyse
- Slither : Détecte les fonctions non protégées
- Mythril : Analyse de contrôle d'accès
- Echidna : Fuzzing des permissions
slither contracts/MyContract.sol --detect unprotected-upgrade
Conclusion
Le contrôle d'accès est la première ligne de défense dans la sécurité des smart contracts. En utilisant des patterns établis comme Ownable, AccessControl, et des fonctionnalités comme timelock et multisig, vous pouvez protéger significativement vos contrats contre les accès non autorisés.
Points clés :
- Utilisez les bibliothèques OpenZeppelin éprouvées
- Implémentez des transferts en 2 étapes
- Testez à la fois les accès autorisés et non autorisés
- Documentez les permissions des rôles
- Envisagez timelock/multisig pour les opérations critiques
Chez Solingo, pratiquez l'implémentation de systèmes de contrôle d'accès à travers des exercices interactifs basés sur des patterns de protocoles DeFi réels.