Наследование, множественное наследование, интерфейсы, абстрактные контракты, virtual/override
Solidity поддерживает множественное наследование с разрешением коллизий через C3-линеаризацию — понимание этого механизма критически важно при работе с OpenZeppelin
Ключевое слово is создаёт наследование. Дочерний контракт получает все public и internal переменные и функции родителя.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0), "Zero address");
owner = newOwner;
}
}
contract Pausable is Ownable {
bool public paused;
modifier whenNotPaused() {
require(!paused, "Paused");
_;
}
function pause() external onlyOwner {
paused = true;
}
function unpause() external onlyOwner {
paused = false;
}
}
// Множественное наследование
contract MyToken is Pausable {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) external whenNotPaused {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
}MyToken наследует и Pausable, и (транзитивно) Ownable. Это позволяет вызывать myToken.pause(), myToken.transferOwnership() и использовать модификаторы onlyOwner и whenNotPaused.
Чтобы функция могла быть переопределена в дочернем контракте, она должна быть помечена virtual. Переопределение помечается override.
contract Base {
function greet() public virtual returns (string memory) {
return "Hello from Base";
}
}
contract Child is Base {
function greet() public virtual override returns (string memory) {
return "Hello from Child";
}
}
contract GrandChild is Child {
function greet() public override returns (string memory) {
// super вызывает ближайшего родителя в цепочке MRO
string memory parentGreet = super.greet();
return string.concat(parentGreet, " and GrandChild");
}
}Если дочерний контракт хочет и сам быть переопределяемым, нужно virtual override вместе.
Solidity разрешает множественное наследование, но порядок перечисления родителей важен. Используется алгоритм C3-линеаризации, который определяет MRO (Method Resolution Order).
Правило Solidity: родителей нужно перечислять от «более базового» к «более производному».
contract A {
function foo() public virtual returns (string memory) { return "A"; }
}
contract B is A {
function foo() public virtual override returns (string memory) { return "B"; }
}
contract C is A {
function foo() public virtual override returns (string memory) { return "C"; }
}
// Порядок важен: B и C — оба наследуют A, B указан первым
contract D is B, C {
function foo() public override(B, C) returns (string memory) {
return super.foo(); // вызовет C.foo() по MRO: D → C → B → A
}
}При конфликте (две функции с одним именем в разных родителях) Solidity требует явного override(B, C) — это явное объявление, что вы знаете о конфликте и намеренно его разрешаете.
Интерфейс определяет контракт без реализации. Все функции в интерфейсе external и не имеют тела. Интерфейс не может иметь state variables, constructor или не-external функции.
Интерфейс используется для: определения стандарта (ERC-20, ERC-721), типизации переменных других контрактов, взаимодействия с известными внешними контрактами.
// Стандарт ERC-20 как интерфейс
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// Использование интерфейса для взаимодействия с любым ERC-20 токеном
contract Staking {
IERC20 public immutable stakingToken;
constructor(address tokenAddress) {
stakingToken = IERC20(tokenAddress);
}
function deposit(uint256 amount) external {
// Вызываем transferFrom на произвольном ERC-20 токене
require(stakingToken.transferFrom(msg.sender, address(this), amount), "Transfer failed");
}
}Абстрактный контракт — промежуточное звено между интерфейсом и обычным контрактом. Может иметь реализацию части функций, но содержит хотя бы одну virtual функцию без реализации (или объявленную без тела).
abstract contract BaseAuction {
address public highestBidder;
uint256 public highestBid;
// Абстрактная функция — наследники ОБЯЗАНЫ реализовать
function calculateMinIncrement() public virtual returns (uint256);
// Конкретная функция с общей логикой
function bid() external payable {
uint256 minBid = highestBid + calculateMinIncrement();
require(msg.value >= minBid, "Bid too low");
highestBidder = msg.sender;
highestBid = msg.value;
}
}
contract EnglishAuction is BaseAuction {
function calculateMinIncrement() public pure override returns (uint256) {
return highestBid / 10; // Каждая ставка минимум на 10% выше
}
}Контракт, помеченный abstract, нельзя задеплоить напрямую — только через наследника, который реализует все абстрактные функции.
Выбор между interface и abstract contract: если нужно задать только API (как в ERC стандартах) — интерфейс. Если нужна общая логика с возможностью переопределения части — абстрактный контракт.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.