interface vs abstract contract in Solidity: When to Use Each
What an interface may contain, what an abstract contract adds, why IERC165 and ERC165 both exist, and why an interface promises the shape of a call and never the behaviour behind it.
TL;DR
- An
interfacemay declare only external functions with no bodies. No state variables, no constructor, no implemented logic. - An abstract contract is an ordinary contract that leaves at least one thing unimplemented. It may hold everything else: state, a constructor, modifiers, finished functions.
- You hold an interface type to call a contract you did not write. You inherit an abstract contract to reuse behaviour you did not want to write twice.
- An interface is a promise about the shape of a call and says nothing about behaviour.
IERC20does not make a token honest, it makes the call compile. - Neither is deployed on its own, and both disappear into something else at build time.
What is the difference between an interface and an abstract contract in Solidity?
An interface may declare only external functions with no bodies, and may not hold state variables or a constructor. An abstract contract is a normal contract that happens to leave at least one function unimplemented, and may contain everything else: state, a constructor, modifiers, and fully written functions.
OpenZeppelin ships a pair that makes the whole distinction visible in fifty lines. First the interface:
interface IERC165 {
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
Then the abstract contract that implements it:
abstract contract ERC165 is IERC165 {
/// @inheritdoc IERC165
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return interfaceId == type(IERC165).interfaceId;
}
}
Same function name, same argument, same return type. The interface ends the line at a semicolon. The abstract contract opens a brace and writes an answer. That single difference, semicolon or brace, is the distinction the rest of this article unpacks.
What can an abstract contract hold that an interface cannot?
Almost everything. An interface is restricted to external function declarations, events, errors, and type definitions. An abstract contract may hold state variables, a constructor, modifiers, internal and private functions, and finished implementations, and it may inherit from other contracts rather than only from interfaces.
interface | abstract contract | |
|---|---|---|
| Function bodies | Not allowed, declarations only | Allowed, and usually most of them |
| State variables | Not allowed | Allowed |
| Constructor | Not allowed | Allowed |
| Function visibility | Must be external | Any visibility |
| Can inherit from | Other interfaces only | Contracts and interfaces |
| Deployed on its own | No, it is a compile-time type | No, only through a contract that completes it |
| Cost at deployment | None, it produces no deployable code | None on its own, its code becomes part of the child |
| What it is for | Describing how to call something else | Sharing behaviour with contracts that build on it |
The row people misread is the last one. An interface points outward, at a contract somewhere else. An abstract contract points downward, at the contracts that will inherit from it.
Why does IERC165 exist when ERC165 already says the same thing?
Because they are aimed at different readers.
IERC165 exists so that any contract, anywhere, can call supportsInterface on an address without knowing anything else about it. It is the calling convention, published separately so that callers can import three lines instead of a whole implementation.
ERC165 exists so that a contract which wants to answer that question does not have to write the answer itself. It is abstract for a precise reason: on its own it reports support for exactly one interface, IERC165, and nothing else. That is not a useful deployed contract. It is a useful starting point, and it is meant to be inherited and extended.
Notice that ERC165 inherits the interface: abstract contract ERC165 is IERC165. That is the normal relationship between the two. The interface states the shape, the abstract contract adopts the shape and fills part of it in, and a concrete contract inherits the abstract one and finishes the job.
Why do you hold an interface type to call a contract you did not write?
Because your contract needs a compiled calling convention for an address it has never seen.
OpenZeppelin's ERC4626 vault holds an underlying token. It has no idea which token, because the address is passed in when the vault is deployed. What it stores is not an address but an interface type:
abstract contract ERC4626 is ERC20, IERC4626 {
using Math for uint256;
IERC20 private immutable _asset;
And here is the call site that makes the point:
function totalAssets() public view virtual returns (uint256) {
return IERC20(asset()).balanceOf(address(this));
}
asset() returns a plain address. Wrapping it in IERC20(...) does not change the address and does not check anything. It tells the compiler which function signatures to encode when it builds the call, so that balanceOf(address) compiles into the right four bytes of calldata and the returned data is read back as a uint256.
That is the entire job of an interface at a call site. It is type information for the compiler, spent at build time, gone by deployment. See what is actually in a .sol file for the other things that vanish between source and bytecode.
Does implementing IERC20 mean a token behaves like a token?
No, and this is the most useful thing to understand about interfaces.
An interface constrains the shape of a call: the function name, the argument types, the return type. It constrains nothing about what happens inside. A token can satisfy IERC20 completely and then do anything at all when called. It may move a different amount than you asked. It may return false instead of reverting, or return nothing at all. It may charge a fee on the way through, so the recipient receives less than the number in the argument.
Here is the whole of the promise, with the documentation stripped out:
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
Read that as a list of calls you are allowed to make, not a list of guarantees you may rely on. Everything an interface promises is here, and behaviour is not in it. Libraries like SafeERC20 exist precisely to wrap calls that the type system already considered valid.
An abstract contract has no such gap. When you inherit ERC165, you get its actual body, so you know what the function does because you can read it.
Why is every building-block library an abstract contract?
Because a building block is defined by what it deliberately leaves undecided.
Ownable is the canonical example. It knows how to store an owner, how to check the caller against it, and how to hand ownership on. What it does not know, and never will, is what you want to restrict:
abstract contract Ownable is Context {
address private _owner;
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
modifier onlyOwner() {
_checkOwner();
_;
}
Everything in that block is impossible in an interface. A state variable, a constructor with logic in it, a modifier. Each one is a decision Ownable has made on your behalf, and none of them can be expressed as a signature.
Note something slightly surprising: Ownable has no unimplemented functions at all. It is abstract because the author declared it so, which is the other route to abstractness. The word says "this is a component, not a product", and the compiler backs it by refusing to deploy the file alone. Its members are marked virtual and internal so children can use and override them, which is the choice covered in public, private, internal, external, through the mechanism covered in inheritance in Solidity.
How do you tell an interface from an abstract contract in one second?
Look for braces after the function signatures.
An interface is a wall of lines ending in semicolons. Scroll it and you will see ); over and over, with no bodies anywhere. If a file looks like a table of contents, it is an interface.
An abstract contract looks like ordinary code, because it is ordinary code. There will be a state variable near the top, function bodies below, and somewhere either a function ending in a semicolon or the word abstract in the declaration line.
Two supporting tells. Interface files are conventionally named with a leading I, so IERC20 and IERC165 announce themselves before you open them. And every function in an interface is external, so if you see internal or private anywhere, you are not in one. Combine that with the state-mutability sweep in view vs pure and you can classify an unfamiliar file before reading a single body.
Delete the line and ask what breaks
- Add a function body to
IERC165and it stops compiling. Interfaces may not implement anything, and the compiler enforces the restriction rather than trusting the convention. - Add a state variable to
IERC20and it fails for the same reason. An interface has no storage, which is why it can describe a contract living at any address without assuming anything about its layout. - Delete
abstractfromERC165and it still compiles, because every function in it has a body. What you lose is the declared intent, and a contract that was meant as a component becomes deployable on its own. - Delete
abstractfrom a contract that genuinely leaves a function unimplemented and compilation fails immediately. The compiler will not let a contract with a hole in it claim to be complete.
The asymmetry is worth noticing. The interface restrictions are absolute, checked on every line. The abstract keyword is sometimes intent and sometimes mechanical necessity, and reading which one you are looking at tells you whether the author chose to hold the contract back or was forced to.
Related questions
Can an interface have a constructor in Solidity? No. An interface has no state and is never deployed on its own, so there is nothing to initialise. A constructor requires an abstract or concrete contract.
Can an abstract contract be deployed? No. The compiler refuses to produce deployment bytecode for it. It reaches the chain only as part of a concrete contract that inherits from it and implements whatever it left open.
Does a contract have to be abstract to have unimplemented functions?
Yes. Any contract with at least one function lacking a body must be declared abstract, and compilation fails otherwise. The reverse is not true: a contract may be declared abstract while implementing everything.
What is the difference between implementing an interface and inheriting a contract? Implementing an interface obliges you to supply bodies for its declarations and gives you no code. Inheriting a contract gives you its state and its written functions, and you override only what you want to change.
Why do interface files start with an I? Convention only, not a language rule. The prefix makes the file's role obvious in imports and in a directory listing, which is why almost every published library follows it.
Can an interface inherit from another interface? Yes, and only from other interfaces. An interface may not inherit from a contract, since it would acquire implementation it is not allowed to hold.
Where to go next
The two words describe the two directions a contract can point. An interface points outward at something you intend to call, carries no code, and promises only that the call will encode correctly. An abstract contract points downward at the contracts that will inherit it, carries as much real code as it likes, and refuses to be deployed until somebody finishes it.
Both rely on the same underlying mechanism, the is keyword and what it actually copies into a contract. That is inheritance in Solidity, and it is the piece that makes the rest of a real codebase legible.
Tagged