# Request for comments - Contract compositon for Core Solidity
This document is a request for comments on how Core Solidity should support
contract composition. It does not settle on a single answer. It presents two
candidate surfaces, inheritance and composition; works both of them out in
enough detail to compare; and argues that neither requires a new object model,
only sugar over the type-class and instance representation the compiler already
uses for contracts. Which surface the language should adopt, and in what form,
is the question we are putting to the community rather than one we are
answering.
The document opens with a survey of the contract-composition patterns that can
be found in popular Solidity libraries. It then follows a single running
example, a vault share token, across several encodings. It starts from a
self-contained Classic Solidity contract that exercises several of those
patterns at once. It then shows the example in Core Solidity, first in the
type-class and instance representation that the compiler uses for contracts,
then using a contract-inheritance feature built on top of that representation,
and finally using an `export`/`import` composition feature offered as an
alternative to inheritance. Two dedicated sections explain how each of these two
surfaces is modeled with data types, classes and instances, and a last one gives
the same treatment to modifiers.
We invite the community to participate. Comments, objections, alternative
encodings and concrete counterexamples are all welcome, and the proposals here
are expected to change in response. The comparison between Sections 4/5 and
Sections 4B/5B is the crux: inheritance is the familiar surface but needs
`super` as an extra construct, whereas composition drops `super`, makes
subtyping an explicit opt-in and turns the diamond problem into instance
uniqueness, all at the cost of asking authors to write `export` and `import`
lists by hand. Whether that trade is worth making, whether the two can coexist,
and what the surface syntax should look like are the open questions.
## 1. Types of contract composition in real repositories
Before the running example, this section surveys the composition patterns that
occur across widely used Solidity codebases. Each pattern is shown with a short
excerpt from a real library and a link to the file it comes from. The excerpts
are pinned to specific released versions so that the links stay stable. The
running example in the rest of the document combines several of these patterns
at once.
### Inheritance and mixins
The most visible form of composition is inheritance. A contract lists several
base contracts, and Solidity merges them into one through C3 linearization,
producing a single flat storage layout. Such a base contract is usually a
_mixin_: a contract that is not meant to be deployed on its own, but that
bundles one slice of state together with the behavior operating on that state,
so that a concrete contract can acquire the whole slice by naming it. Composing
a contract then amounts to choosing which slices it is made of, and the
mechanisms below are the different ways a language can offer that choice.
OpenZeppelin's `ERC20` is itself assembled this way, from a context helper and a
set of interfaces:
```solidity
abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
```
Found in OpenZeppelin
[`contracts/token/ERC20/ERC20.sol`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.6.1/contracts/token/ERC20/ERC20.sol).
### Virtual hooks and `super`
Inheritance becomes open composition when a base routes its behavior through a
single `virtual` function that derived contracts override and extend with
`super`. OpenZeppelin's `ERC20` sends every balance change through `_update`,
and `ERC20Pausable` overrides that one seam to add a guard before delegating
back:
```solidity
abstract contract ERC20Pausable is ERC20, Pausable {
function _update(address from, address to, uint256 value) internal virtual override whenNotPaused {
super._update(from, to, value);
}
}
```
Found in OpenZeppelin
[`contracts/token/ERC20/extensions/ERC20Pausable.sol`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.6.1/contracts/token/ERC20/extensions/ERC20Pausable.sol).
### Modifiers as reusable guards
Access control and similar pre-checks are composed through modifiers, which a
base contract defines once and any function attaches by name. OpenZeppelin's
`Ownable` contributes `onlyOwner`, which a derived contract uses without knowing
how the check is implemented:
```solidity
modifier onlyOwner() {
_checkOwner();
_;
}
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
```
Found in OpenZeppelin
[`contracts/access/Ownable.sol`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.6.1/contracts/access/Ownable.sol).
### Libraries attached with `using ... for`
A library bundles functions that operate on a given type, and `using L for T`
makes them read as methods on values of that type. OpenZeppelin's `SafeERC20`
wraps raw token calls and is attached with `using SafeERC20 for IERC20`:
```solidity
library SafeERC20 {
function safeTransfer(IERC20 token, address to, uint256 value) internal {
if (!_safeTransfer(token, to, value, true)) {
revert SafeERC20FailedOperation(address(token));
}
}
}
```
Found in OpenZeppelin
[`contracts/token/ERC20/utils/SafeERC20.sol`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.6.1/contracts/token/ERC20/utils/SafeERC20.sol).
### Interfaces and callbacks
Composition also runs in the opposite direction. A contract calls back into an
arbitrary counterparty through an interface, inverting control. Uniswap V2
invokes `uniswapV2Call` on the caller during a flash swap, so the borrower
supplies the logic that runs in the middle of the pair's own function:
```solidity
interface IUniswapV2Callee {
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external;
}
```
Found in Uniswap
[`v2-core/contracts/interfaces/IUniswapV2Callee.sol`](https://github.com/Uniswap/v2-core/blob/master/contracts/interfaces/IUniswapV2Callee.sol).
### Delegation through proxies
Rather than inheriting code, a contract can forward every call to another
contract with `delegatecall`, running the target's code against its own storage.
This is the basis of upgradeable proxies and minimal-proxy clones.
OpenZeppelin's `Proxy` carries the core of the pattern:
```solidity
function _delegate(address implementation) internal virtual {
assembly {
calldatacopy(0x00, 0x00, calldatasize())
let result := delegatecall(gas(), implementation, 0x00, calldatasize(), 0x00, 0x00)
returndatacopy(0x00, 0x00, returndatasize())
switch result
case 0 { revert(0x00, returndatasize()) }
default { return(0x00, returndatasize()) }
}
}
```
Found in OpenZeppelin
[`contracts/proxy/Proxy.sol`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.6.1/contracts/proxy/Proxy.sol).
### Factories and deploy-time composition
Finally, contracts compose at deployment time. A factory creates new instances,
often with `create2` for a deterministic address, and wires them together.
Uniswap V2's factory deploys each pair this way:
```solidity
function createPair(address tokenA, address tokenB) external returns (address pair) {
// ... ordering and existence checks ...
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
IUniswapV2Pair(pair).initialize(token0, token1);
// ... registry bookkeeping ...
}
```
Found in Uniswap
[`v2-core/contracts/UniswapV2Factory.sol`](https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Factory.sol).
### Putting the patterns together
These patterns are not exclusive, and a single contract usually mixes them. The
running example in the next section does exactly that, combining inheritance, a
virtual hook with `super`, modifiers, a library, an interface callback and a
CREATE2 factory in one vault token.
## 2. The example in Classic Solidity
The example is a vault share token that exercises every composition mechanism
Solidity offers at once: multiple inheritance with a shared virtual hook, a
small library used through `using ... for`, an interface plus a callback, a
factory that deploys with CREATE2, modifiers for access control, and state
segregated per mixin. It depends on no external library.
```solidity
pragma solidity ^0.8.24;
library Math {
function addChecked(uint256 a, uint256 b) internal pure returns (uint256 c) {
c = a + b;
}
function subChecked(uint256 a, uint256 b) internal pure returns (uint256) {
require(a >= b, "underflow");
return a - b;
}
}
interface IFlashBorrower {
function onFlashMint(address initiator, uint256 amount, bytes calldata data)
external returns (bytes32);
}
abstract contract Ownable {
address private _owner;
error NotOwner();
constructor() { _owner = msg.sender; }
modifier onlyOwner() { if (msg.sender != _owner) revert NotOwner(); _; }
function owner() public view returns (address) { return _owner; }
function transferOwnership(address to) public onlyOwner { _owner = to; }
}
abstract contract Pausable {
bool private _paused;
error Paused();
function paused() public view returns (bool) { return _paused; }
function _setPaused(bool p) internal { _paused = p; }
modifier whenNotPaused() { if (_paused) revert Paused(); _; }
}
abstract contract ERC20Base {
using Math for uint256;
mapping(address => uint256) public balanceOf;
uint256 public totalSupply;
event Transfer(address indexed from, address indexed to, uint256 value);
function _update(address from, address to, uint256 amount) internal virtual {
if (from == address(0)) {
totalSupply = totalSupply.addChecked(amount);
} else {
balanceOf[from] = balanceOf[from].subChecked(amount);
}
if (to == address(0)) {
totalSupply = totalSupply.subChecked(amount);
} else {
balanceOf[to] = balanceOf[to].addChecked(amount);
}
emit Transfer(from, to, amount);
}
function transfer(address to, uint256 amount) public returns (bool) {
_update(msg.sender, to, amount);
return true;
}
}
contract VaultToken is Ownable, Pausable, ERC20Base {
bytes32 private constant FLASH_OK = keccak256("IFlashBorrower.onFlashMint");
constructor(uint256 initialSupply) {
_update(address(0), msg.sender, initialSupply);
}
function _update(address from, address to, uint256 amount)
internal override whenNotPaused
{
super._update(from, to, amount);
}
function setPaused(bool p) external onlyOwner { _setPaused(p); }
function mint(address to, uint256 amount) external onlyOwner {
_update(address(0), to, amount);
}
function flashMint(IFlashBorrower borrower, uint256 amount, bytes calldata data)
external
{
_update(address(0), address(borrower), amount);
bytes32 ok = borrower.onFlashMint(msg.sender, amount, data);
require(ok == FLASH_OK, "callback failed");
_update(address(borrower), address(0), amount);
}
}
contract VaultTokenFactory {
event Created(address token, bytes32 salt);
function create(uint256 initialSupply, bytes32 salt) external returns (address token) {
token = address(new VaultToken{salt: salt}(initialSupply));
VaultToken(token).transferOwnership(msg.sender);
emit Created(token, salt);
}
function predict(uint256 initialSupply, bytes32 salt) external view returns (address) {
bytes32 h = keccak256(abi.encodePacked(
bytes1(0xff), address(this), salt,
keccak256(abi.encodePacked(type(VaultToken).creationCode, abi.encode(initialSupply)))
));
return address(uint160(uint256(h)));
}
}
```
### What the composition is doing
The line `contract VaultToken is Ownable, Pausable, ERC20Base` is the heart of
the example. It composes three independent pieces into one contract, and
Solidity resolves it through C3 linearization, producing a single flat storage
layout that holds `_owner`, `_paused`, `balanceOf` and `totalSupply` together.
Each mixin contributes its own slice of state and never sees the others.
`ERC20Base` declares `_update` as `virtual` and routes `transfer`, `mint` and
the flash-mint through it, so `_update` is the one seam every token movement
passes. `VaultToken` overrides `_update`, attaches the `whenNotPaused` modifier,
and then calls `super._update`. The `super` call is what makes the composition
open: the override adds the pause check and delegates the actual ledger movement
to the next contract in the linearization, without copying its code. If a fourth
mixin were stacked on top, it could override `_update` again and chain another
`super` call, and the order would follow the linearization automatically.
The modifiers `onlyOwner` and `whenNotPaused` are the access-control face of the
same idea. They are reusable guards contributed by `Ownable` and `Pausable`, and
the final contract attaches them to `mint`, `setPaused` and the overridden
`_update` without knowing how the check is implemented.
The remaining mechanisms are lighter. The `Math` library is attached with
`using
Math for uint256` so that `totalSupply.addChecked(amount)` reads as a
method call. The interface `IFlashBorrower` plus the `onFlashMint` callback
invert control: the token hands freshly minted units to an arbitrary borrower
and trusts it to return a magic value before the units are burned back. The
factory composes at deployment time, using `new VaultToken{salt: salt}(...)` to
obtain a deterministic address that `predict` can compute off-chain.
The rest of the document keeps the same behavior and re-expresses this
composition in Core Solidity, where there is no native inheritance and no
`super`.
## 3. The example in the Core Solidity representation
Core Solidity represents a contract not as a primitive construct but as a type
class together with one instance over the contract's storage. This is the same
representation the compiler already produces when it desugars field access and
contracts. Core does not yet have interfaces, so this section expresses the
composition with the features the compiler accepts today: a single contract that
declares all of the state and shares behavior through ordinary methods.
The representation rests on two ideas.
First, a contract's storage is a value of a generated type. For a contract `C`
the compiler emits a singleton tag `data CCxt = CCxt;`, and the storage object
has type `ContractStorage(CCxt)`. Each field becomes a selector singleton
together with a `CStructField` instance that records the field's storage type
and its offset, which is the product of the types of all preceding fields. A
field read becomes `RVA.acc` over a
`MemberAccessProxy(ContractStorage(CCxt), selector)`, and a field write becomes
`Assign.assign(LVA.acc(...), rhs)`. So a method body that manipulates fields is
ordinary code over these proxies, and the receiver is the explicit
`self : ContractStorage(CCxt)`.
Second, the contract itself becomes a type class with a single instance on
`ContractStorage(CCxt)`. The instance carries the method bodies, and because
there is exactly one instance per storage type, every call resolves to it.
In this representation the vault is one contract that declares all of the state
the three Classic Solidity mixins used to hold separately, and the behaviors
they contributed become ordinary methods on that one contract. The single
virtual hook becomes one internal method, `update`, that every token movement
routes through, with the pause guard inlined at that seam.
```solc
import std.{*};
import std.dispatch.{*};
import std.opcodes.{caller}; // EVM opcode functions; caller() -> word
// One flat contract holding all the state the three mixins used to hold
// separately, with the shared hook as a single internal method.
contract VaultToken {
owner_ : address;
paused_ : uint256; // 0 = not paused, 1 = paused (bool is not storable yet)
totalSupply_ : uint256;
balances : mapping(address, uint256);
constructor(initialSupply : uint256) {
owner_ = address(caller());
paused_ = uint256(0);
update(address(0), address(caller()), initialSupply); // initial mint through the seam
}
// ownership face (the Ownable mixin)
public function owner() -> address { return owner_; }
public function transferOwnership(newOwner : address) -> () {
requireOwner();
owner_ = newOwner;
}
function requireOwner() -> () {
require(address(caller()) == owner_, Error(0x12b0c500)); // onlyOwner
}
// pause face (the Pausable mixin)
public function paused() -> bool { return paused_ != uint256(0); }
public function setPaused(flag : uint256) -> () {
requireOwner();
paused_ = flag;
}
// ERC-20 face (the ERC20Base mixin)
public function totalSupply() -> uint256 { return totalSupply_; }
public function balanceOf(account : address) -> uint256 { return balances[account]; }
public function transfer(to : address, amount : uint256) -> bool {
update(address(caller()), to, amount);
return true;
}
public function mint(to : address, amount : uint256) -> () {
requireOwner();
update(address(0), to, amount);
}
// the single seam every movement flows through: the pause check and the
// ledger movement live together in the one body the calls reach
function update(from : address, to : address, amount : uint256) -> () {
require(paused_ == uint256(0), Error(0x9e87fac8)); // whenNotPaused / Paused()
if (from != address(0)) { balances[from] = Num.sub(balances[from], amount); }
else { totalSupply_ = Num.add(totalSupply_, amount); }
if (to != address(0)) { balances[to] = Num.add(balances[to], amount); }
else { totalSupply_ = Num.sub(totalSupply_, amount); }
// emit Transfer(from, to, amount); // events are a separate feature
}
}
```
The compiler turns the contract into the storage infrastructure and one
instance. The four fields, in declaration order, produce four selectors whose
offsets are the nested product of the preceding field types:
```solc
data VaultTokenCxt = VaultTokenCxt;
data VaultToken_owner_sel = VaultToken_owner_sel;
instance StructField(ContractStorage(VaultTokenCxt), VaultToken_owner_sel)
: CStructField(storage(address), ()) {}
data VaultToken_paused_sel = VaultToken_paused_sel;
instance StructField(ContractStorage(VaultTokenCxt), VaultToken_paused_sel)
: CStructField(storage(uint256), (address, ())) {}
data VaultToken_totalSupply_sel = VaultToken_totalSupply_sel;
instance StructField(ContractStorage(VaultTokenCxt), VaultToken_totalSupply_sel)
: CStructField(storage(uint256), (address, (uint256, ()))) {}
data VaultToken_balances_sel = VaultToken_balances_sel;
instance StructField(ContractStorage(VaultTokenCxt), VaultToken_balances_sel)
: CStructField(storage(mapping(address, uint256)), (address, (uint256, (uint256, ())))) {}
```
The contract becomes a type class, and a single instance on
`ContractStorage(VaultTokenCxt)` carries the bodies, with
`self : ContractStorage(VaultTokenCxt)` as the receiver and field access
expressed through the proxies:
```solc
class self : VaultToken {
function owner(self : self) -> address;
function transferOwnership(self : self, newOwner : address) -> ();
function paused(self : self) -> bool;
function setPaused(self : self, flag : uint256) -> ();
function totalSupply(self : self) -> uint256;
function balanceOf(self : self, account : address) -> uint256;
function transfer(self : self, to : address, amount : uint256) -> bool;
function mint(self : self, to : address, amount : uint256) -> ();
function update(self : self, from : address, to : address, amount : uint256) -> ();
}
instance ContractStorage(VaultTokenCxt) : VaultToken {
function owner(self : ContractStorage(VaultTokenCxt)) -> address {
return RVA.acc(MemberAccessProxy(ContractStorage(VaultTokenCxt), VaultToken_owner_sel));
}
function transfer(self : ContractStorage(VaultTokenCxt), to : address, amount : uint256) -> bool {
update(self, address(caller()), to, amount);
return true;
}
// ... the remaining methods, each over the VaultToken_*_sel selectors
}
```
This already reproduces the Classic Solidity behavior. The state composition is
done by declaring the fields together, and the behavior composition is done by
sharing ordinary methods, with the one internal `update` seam standing in for
the virtual hook. What it does not yet provide is a way to reuse the field
declarations and the method bodies of a separate base, which is the role of
inheritance.
## 4. The same example using inheritance
Inheritance lets a contract extend another, reusing its fields and its method
bodies. The surface adds three keywords: `interface`, `implements` and
`inherits`. A contract inherits from at most one parent and may implement any
number of interfaces. Inheriting copies the parent's fields into the child, so
storage stays flat, and it makes the parent's methods callable on the child's
storage.
The vault has three stateful mixins. Since a contract inherits from one parent,
we arrange them as a chain, which is the faithful encoding of Solidity's
linearized storage: `Ownable`, then `Pausable inherits Ownable`, then
`ERC20Base inherits
Pausable`, then `VaultToken inherits ERC20Base`. Each layer
keeps its own state and reuses the inherited state directly.
```solc
import std.{*};
import std.dispatch.{*};
import std.opcodes.{caller}; // EVM opcode functions; caller() -> word
interface IOwnable {
function owner() -> address;
function transferOwnership(newOwner : address) -> ();
}
interface IPausable { function paused() -> bool; }
interface IERC20 {
function totalSupply() -> uint256;
function balanceOf(account : address) -> uint256;
function transfer(to : address, amount : uint256) -> bool;
}
// root layer: ownership state and the guard
contract Ownable implements IOwnable {
owner_ : address;
constructor(initialOwner : address) { owner_ = initialOwner; }
function owner() -> address { return owner_; }
function transferOwnership(newOwner : address) -> () {
require(address(caller()) == owner_, Error(0x12b0c500)); // onlyOwner
owner_ = newOwner;
}
}
// pause layer: adds a flag, reuses the inherited owner_ for the guard
contract Pausable inherits Ownable implements IPausable {
paused_ : bool;
function paused() -> bool { return paused_; }
function setPaused(p : bool) -> () {
require(address(caller()) == owner_, Error(0x12b0c500)); // owner_ inherited from Ownable
paused_ = p;
}
}
// ledger layer: balances, supply and the single update seam
contract ERC20Base inherits Pausable implements IERC20 {
balances : mapping(address, uint256);
totalSupply_ : uint256;
function totalSupply() -> uint256 { return totalSupply_; }
function balanceOf(account : address) -> uint256 { return balances[account]; }
function transfer(to : address, amount : uint256) -> bool {
update(address(caller()), to, amount);
return true;
}
// the one seam every movement flows through
function update(from : address, to : address, amount : uint256) -> () {
if (from != address(0)) { balances[from] = Num.sub(balances[from], amount); }
else { totalSupply_ = Num.add(totalSupply_, amount); }
if (to != address(0)) { balances[to] = Num.add(balances[to], amount); }
else { totalSupply_ = Num.sub(totalSupply_, amount); }
}
}
// final contract: overrides the seam, adds mint and flash-mint
contract VaultToken inherits ERC20Base {
constructor(initialSupply : uint256) {
owner_ = address(caller());
update(address(0), address(caller()), initialSupply);
}
// Pausable dresses the inherited hook
function update(from : address, to : address, amount : uint256) -> () {
require(not(paused()), Error(0x9e87fac8)); // whenNotPaused
super.update(from, to, amount); // delegate to ERC20Base.update
}
function mint(to : address, amount : uint256) -> () {
require(address(caller()) == owner_, Error(0x12b0c500)); // onlyOwner
update(address(0), to, amount);
}
}
```
Compared with section 3, the state composition is no longer done by hand: the
chain accumulates `owner_`, `paused_`, `balances` and `totalSupply_` into the
final storage automatically. The hook is overridden the way Classic Solidity
does it, with a `whenNotPaused` check followed by `super.update`, and the
override is resolved against a single instance, so a call to `update` from the
inherited `transfer` reaches the overriding body. The only construct that does
not come for free is `super`, which the next section addresses as part of the
representation strategy.
## 4B. Composition instead of inheritance
```solc
import std.{*};
import std.dispatch.{*};
import std.opcodes.{caller}; // EVM opcode functions; caller() -> word
interface IOwnable {
function owner() -> address;
function transferOwnership(newOwner : address) -> ();
}
interface IPausable { function paused() -> bool; }
interface IERC20 {
function totalSupply() -> uint256;
function balanceOf(account : address) -> uint256;
function transfer(to : address, amount : uint256) -> bool;
}
// root layer: ownership state and the guard
contract Ownable implements IOwnable {
owner_ : address;
constructor(initialOwner : address) { owner_ = initialOwner; }
function owner() -> address { return owner_; }
function transferOwnership(newOwner : address) -> () {
require(address(caller()) == owner_, Error(0x12b0c500)); // onlyOwner
owner_ = newOwner;
}
}
// pause layer: adds a flag; it declares no owner state of its own
contract Pausable implements IPausable {
// import Ownable if onlyOwner functionality is needed
export {paused, setPaused};
paused_ : bool;
function paused() -> bool { return paused_; }
function setPaused(p : bool) -> () {
paused_ = p;
}
}
// ledger layer: balances, supply and the single update seam
contract ERC20Base implements IERC20 {
// two independent mixins: Ownable, Pausable
import Ownable.{*};
import Pausable.{*}; // we might omit .{*} or use some other ergonomic syntax
// re-export the mixins, so a consumer of ERC20Base also sees owner_ and paused()
export {totalSupply, balanceOf, transfer, update, Ownable.{*}, Pausable.{*}};
balances : mapping(address, uint256);
totalSupply_ : uint256;
function totalSupply() -> uint256 { return totalSupply_; }
function balanceOf(account : address) -> uint256 { return balances[account]; }
function transfer(to : address, amount : uint256) -> bool {
update(address(caller()), to, amount);
return true;
}
// the one seam every movement flows through
function update(from : address, to : address, amount : uint256) -> () {
if (from != address(0)) { balances[from] = Num.sub(balances[from], amount); }
else { totalSupply_ = Num.add(totalSupply_, amount); }
if (to != address(0)) { balances[to] = Num.add(balances[to], amount); }
else { totalSupply_ = Num.sub(totalSupply_, amount); }
}
}
// final contract: overrides the seam, adds mint
contract VaultToken {
import ERC20Base.{*}; // brings Ownable and Pausable along, since ERC20Base re-exports them
constructor(initialSupply : uint256) {
owner_ = address(caller());
update(address(0), address(caller()), initialSupply);
}
// the pause guard dresses the seam
function update(from : address, to : address, amount : uint256) -> () {
require(not(paused()), Error(0x9e87fac8)); // whenNotPaused
ERC20Base.update(from, to, amount); // no `super`: the base body still has a name
}
function mint(to : address, amount : uint256) -> () {
require(address(caller()) == owner_, Error(0x12b0c500)); // onlyOwner
update(address(0), to, amount);
}
}
```
Three details separate this from Section 4. `VaultToken` no longer says
`inherits`; it `import`s, and there is no `super`, because the overridden body
is still reachable under its own qualified name `ERC20Base.update`. `Pausable`
no longer borrows `owner_` from a parent, so its `setPaused` is unguarded unless
it declares that it requires `Ownable`. And `ERC20Base` must say what it
re-exports, because importing a mixin does not automatically republish it.
Section 5B shows that each of these three details is exactly one construct in
the class and instance encoding.
## 5. Representing inheritance with data types, classes and instances
Inheritance is the sum of four capabilities, and each one maps onto a Core
Solidity construct already used by the contract representation. The mapping is
what makes inheritance a desugaring rather than a new primitive.
### State, with data types
A contract's storage is a product of its field types, tagged by a singleton data
type. Inheriting re-includes the parent's fields into the child, so the child's
storage is the product of the parent's fields followed by the child's own. The
selector and `CStructField` instances are generated for all fields, parent and
child alike, with offsets recomputed against the flattened layout. For the vault
chain this yields four selectors on `ContractStorage(VaultTokenCxt)`, with
offsets that are the nested product of the preceding field types:
```solc
data VaultTokenCxt = VaultTokenCxt;
instance StructField(ContractStorage(VaultTokenCxt), VaultToken_owner_sel)
: CStructField(storage(address), ()) {}
instance StructField(ContractStorage(VaultTokenCxt), VaultToken_paused_sel)
: CStructField(storage(bool), address) {}
instance StructField(ContractStorage(VaultTokenCxt), VaultToken_balances_sel)
: CStructField(storage(mapping(address, uint256)), pair(address, bool)) {}
instance StructField(ContractStorage(VaultTokenCxt), VaultToken_totalSupply_sel)
: CStructField(storage(uint256), pair(pair(address, bool), mapping(address, uint256))) {}
```
This is exactly Solidity's flat-storage semantics, made explicit by the offset
records.
### Types and subtyping, with classes
An interface is a class, and a contract is a class whose superclasses are the
interfaces it implements and the parent it inherits. The superclass relation is
the subtyping relation: any type that satisfies `VaultToken` satisfies
`ERC20Base`, hence `Pausable`, `Ownable` and the interfaces, so a value of the
child storage type is accepted wherever an ancestor capability is required. For
the chain, the classes line up as a tower:
```solc
forall self . self:IOwnable => class self : Ownable { /* owner, transferOwnership */ }
forall self . self:Ownable, self:IPausable => class self : Pausable { function setPaused(self : self, p : bool) -> (); }
forall self . self:Pausable, self:IERC20 => class self : ERC20Base { function update(self : self, from : address, to : address, amount : uint256) -> (); }
forall self . self:ERC20Base => class self : VaultToken { function mint(self : self, to : address, amount : uint256) -> (); }
```
### Implementation reuse, with instances
Each contract contributes one instance of its class on the storage type, with
the method bodies expressed over the selectors. Inheriting generates, on the
child's storage type, an instance of the parent class whose bodies are the
parent's, with the parent's selectors rewritten to the child's. So the child
gains the parent's implementations without copying source. Because there is
exactly one instance of a given class for a given storage type, method
resolution is never ambiguous, and the linearization questions that arise in
Classic Solidity do not appear.
### Open recursion, with override and a named base body
Two pieces complete the picture. Override is selection: when the child
re-declares a method, the generated instance simply holds the overriding body,
and since `transfer` and `update` resolve through the same single instance, a
call from the inherited `transfer` to `update` reaches the override. This gives
virtual dispatch with no runtime table.
The one element that needs care is `super`. Once the override replaces the base
body inside the single instance, the base body would be lost, so the strategy is
to preserve and name it. When `VaultToken` overrides `update`, the desugarer
keeps the inherited body under a generated name and makes `super.update` resolve
to it:
```solc
// the override, holding the dressed body
function update(self : ContractStorage(VaultTokenCxt), from : address, to : address, amount : uint256) -> () {
require(not(paused(self)), Error(0x9e87fac8));
super$update(self, from, to, amount); // the preserved base body
}
// the preserved ERC20Base.update body, generated alongside the instance
function super$update(self : ContractStorage(VaultTokenCxt), from : address, to : address, amount : uint256) -> () {
if (from != address(0)) { /* debit */ } else { /* mint */ }
if (to != address(0)) { /* credit */ } else { /* burn */ }
}
```
For a longer chain the desugarer names one body per overriding layer and threads
each `super` call to the immediately preceding one. That generated thread is a
linearized chain of method bodies, produced mechanically from the `inherits`
chain. It is the single feature the example forces beyond the plain
representation, and it is local to the inheritance pass.
In summary, data types carry the inherited state, classes carry the subtyping,
instances carry the reused implementations, and override plus a named base body
carry open recursion. The vault is reproduced in full, with only `super` added
on top of machinery the contract representation already uses.
## 5B. Representing composition with classes and instances
Section 5 translated `inherits`. This section translates the `export` and
`import` surface of Section 4B, using that section's code as the running
example. The two surfaces share the storage representation of Section 3 and
diverge only in what they generate on top of it. The whole translation follows
one rule:
> **What a mixin provides becomes a class and an instance. What a mixin requires becomes a context.**
And a mixin requires two different things, which become two different kinds of
constraint. It requires _state from its host_, which becomes a `CStructField`
constraint. It requires _other mixins_, which becomes a superclass constraint.
The one move that makes this work is that a mixin's method bodies are **not**
written over a concrete storage type. They are polymorphic in the storage,
constrained by the fields they touch. This is not new machinery: `std/std.solc`
already writes exactly this shape, quantifying the `LVA` and `RVA` instances
over `cxt`, `offsetType`, `storageType` and `loadType` and discharging them
through `StructField(ContractStorage(cxt), fieldSelector) : CStructField(...)`.
### Fields, with globally named selectors
Each field a mixin declares yields a selector singleton, exactly as in
Section 3. The difference is that the selector belongs to the _mixin_, not to
any storage type, so it is a single global name shared by every host:
```solc
data Ownable_owner_sel = Ownable_owner_sel;
data Pausable_paused_sel = Pausable_paused_sel;
data ERC20Base_balances_sel = ERC20Base_balances_sel;
data ERC20Base_totalSupply_sel = ERC20Base_totalSupply_sel;
```
Section 5 spoke of "the parent's selectors rewritten to the child's". Here
nothing is rewritten. The selector is shared and only the `CStructField`
instance, which carries the offset, differs from host to host.
### Mixin bodies, with storage-polymorphic constrained functions
Every method a mixin declares becomes a top-level function, generic in the
storage type, whose context lists the fields it reads or writes. `Pausable` from
Section 4B becomes:
```solc
forall self off .
StructField(self, Pausable_paused_sel) : CStructField(storage(bool), off),
off : StorageSize,
storage(bool) : CanStore(bool)
=> function Pausable$paused(self : self) -> bool {
return RVA.acc(MemberAccessProxy(self, Pausable_paused_sel));
}
forall self off .
StructField(self, Pausable_paused_sel) : CStructField(storage(bool), off),
off : StorageSize
=> function Pausable$setPaused(self : self, p : bool) -> () {
Assign.assign(LVA.acc(MemberAccessProxy(self, Pausable_paused_sel)), p);
}
```
Read the context as a sentence: _"I work over any storage that carries a `bool`
field named `paused_`, at whatever offset."_ The mixin never learns which host
it was composed into.
### `export`, with a class
The export list is literally the class signature. Nothing else is exported,
because nothing else becomes a method:
```solc
// export {paused, setPaused};
forall self . class self : Pausable {
function paused(self : self) -> bool;
function setPaused(self : self, p : bool) -> ();
}
```
A helper the mixin keeps to itself simply stays a generic function and never
enters a class, so it is unreachable from the host. In this encoding `export`
stops being a visibility annotation and becomes the question _which class
exists_.
### `import`, with field instances and a forwarding instance
This is where composition happens. An `import M.{*}` inside a host generates two
things, and no source is copied.
First, `M`'s fields are added to the host's flat layout, as `CStructField`
instances at the host's recomputed offsets. For `ERC20Base`, which imports both
`Ownable` and `Pausable`:
```solc
data ERC20BaseCxt = ERC20BaseCxt;
// fields contributed by the imported mixins
instance StructField(ContractStorage(ERC20BaseCxt), Ownable_owner_sel)
: CStructField(storage(address), ()) {}
instance StructField(ContractStorage(ERC20BaseCxt), Pausable_paused_sel)
: CStructField(storage(bool), (address, ())) {}
// the host's own fields, after them
instance StructField(ContractStorage(ERC20BaseCxt), ERC20Base_balances_sel)
: CStructField(storage(mapping(address, uint256)), (address, (bool, ()))) {}
instance StructField(ContractStorage(ERC20BaseCxt), ERC20Base_totalSupply_sel)
: CStructField(storage(uint256), (address, (bool, (mapping(address, uint256), ())))) {}
```
Second, one instance per imported mixin, whose bodies merely forward to the
mixin's generic functions:
```solc
instance ContractStorage(ERC20BaseCxt) : Pausable {
function paused(self : ContractStorage(ERC20BaseCxt)) -> bool {
return Pausable$paused(self);
}
function setPaused(self : ContractStorage(ERC20BaseCxt), p : bool) -> () {
Pausable$setPaused(self, p);
}
}
instance ContractStorage(ERC20BaseCxt) : Ownable { /* likewise, through Ownable$* */ }
```
The field instances are precisely what discharges the `CStructField` constraints
the forwarded bodies carry. Retargeting a mixin onto a host's storage is
therefore done by _constraint solving_, not by rewriting or copying a body. This
is also why `import` gives the host both field access and method calls at once:
the first generated group makes `owner_` resolve, the second makes `owner()`
resolve.
### The seam, with a class method and a default body
Section 4B's `transfer` calls `update`, and `VaultToken` overrides `update`. For
the override to be reached from the inherited `transfer`, the call must be
virtual. The encoding gets this from an ordinary type-class default method, and
the distinction is worth stating precisely:
- The **class method** is the virtual entry point. Calls _between_ mixin methods
go through it.
- The **generic function** is the non-virtual default body, reachable by its
qualified name.
So `ERC20Base`'s two methods translate differently. `update` touches fields
directly, and is the default body:
```solc
forall self off1 off2 .
StructField(self, ERC20Base_balances_sel) : CStructField(storage(mapping(address, uint256)), off1),
StructField(self, ERC20Base_totalSupply_sel) : CStructField(storage(uint256), off2),
off1 : StorageSize, off2 : StorageSize
=> function ERC20Base$update(self : self, from : address, to : address, amount : uint256) -> () {
if (from != address(0)) { /* debit balances */ } else { /* mint into totalSupply_ */ }
if (to != address(0)) { /* credit balances */ } else { /* burn from totalSupply_ */ }
}
```
whereas `transfer` reaches `update` _through the class_, so it is constrained by
`self : ERC20Base` rather than by the fields:
```solc
forall self . self : ERC20Base
=> function ERC20Base$transfer(self : self, to : address, amount : uint256) -> bool {
ERC20Base.update(self, address(caller()), to, amount); // virtual: resolves at the host
return true;
}
```
`VaultToken`'s instance then chooses a different body for the one method it
overrides, and takes the default for the rest:
```solc
data VaultTokenCxt = VaultTokenCxt;
instance ContractStorage(VaultTokenCxt) : ERC20Base {
// inherited unchanged: forward to the default body
function transfer(self : ContractStorage(VaultTokenCxt), to : address, amount : uint256) -> bool {
return ERC20Base$transfer(self, to, amount);
}
// overridden: the pause guard, then the base body by name
function update(self : ContractStorage(VaultTokenCxt), from : address, to : address, amount : uint256) -> () {
require(not(Pausable$paused(self)), Error(0x9e87fac8));
ERC20Base$update(self, from, to, amount);
}
}
```
Now trace a `transfer` on `VaultToken`. It enters `ERC20Base$transfer`, which
calls the class method `ERC20Base.update` on `ContractStorage(VaultTokenCxt)`,
which resolves to the instance above, which is the override, which calls the
default body. Open recursion, with no runtime dispatch table, and (the point of
this section) **no `super`**. Section 5 had to preserve the overridden body
under a generated `super$update` name, because the override destroyed it inside
the single instance. Under composition the base body was never inside the
instance to begin with: it is `ERC20Base$update`, a name that always existed.
`super.update` is just qualified naming, `ERC20Base.update`, which is why the
Section 4B surface can drop the keyword.
### Requirements between mixins, with superclass constraints
The comment in Section 4B,
`// import Ownable if onlyOwner functionality is
needed`, is a constraint
waiting to be written down. A mixin that needs another mixin says so in its
class context:
```solc
forall self . self : Ownable => class self : Pausable {
function setPaused(self : self, p : bool) -> ();
}
```
and its default body may then call `Ownable.owner(self)` to restore the guard
that Section 4's `Pausable` got from its parent. The dependency becomes a
checked part of the type rather than a comment, and a host that imports
`Pausable` without `Ownable` fails instance resolution instead of silently
compiling an unguarded setter.
### Subtyping is opt-in, and that is the point
`import` generates an _instance_; `inherits` generated a _superclass edge_. The
two are not the same, and the difference is exactly `has-a` versus `is-a`.
Because `ERC20Base` merely has
`instance ContractStorage(ERC20BaseCxt) : Ownable`, a function polymorphic as
`forall s . s : ERC20Base => ...` cannot call `owner(s)`. Republishing is a
separate decision, and it is the `export` clause that Section 4B writes as
`export {..., Ownable.{*}, Pausable.{*}}`. In the encoding, re-exporting an
imported mixin adds it to the class context:
```solc
forall self . self : Ownable, self : Pausable, self : IERC20 => class self : ERC20Base {
function totalSupply(self : self) -> uint256;
function balanceOf(self : self, account : address) -> uint256;
function transfer(self : self, to : address, amount : uint256) -> bool;
function update(self : self, from : address, to : address, amount : uint256) -> ();
}
```
This is what lets `VaultToken`, which imports only `ERC20Base`, still write
`owner_` and call `paused()`. So the surface has two orthogonal knobs where
inheritance had one: `import` decides _what you can use_, `export` decides _what
you become_.
### Diamonds become instance uniqueness
Suppose two imported mixins each imported `Ownable`. The host would try to
generate `instance ContractStorage(HostCxt) : Ownable` twice, and the second
would be rejected as a duplicate instance. The desugarer therefore deduplicates,
and it can do so soundly because both the field and the class are globally
named: `Ownable_owner_sel` is one singleton, so the field is contributed once,
and `(Ownable, ContractStorage(HostCxt))` is one key, so the instance is
generated once.
What C3 linearization does with an order, the constraint solver does with a set
union. There is no order to get wrong, the result is confluent, and a genuine
conflict, namely two _different_ mixins exporting the same method name, surfaces
as an overlapping-instance error rather than being resolved silently by position
in the `is` list.
The converse case is a mixin a host wants _twice_, with independent state, which
`import Ownable as Admin` would express. Deduplication by global name is then
precisely the wrong thing, so the alias must mint a fresh selector, and the
mixin must be indexed by it. The class gains a parameter:
```solc
forall self sel off .
StructField(self, sel) : CStructField(storage(address), off), off : StorageSize
=> class self : Ownable(sel) {
function owner(self : self) -> address;
function transferOwnership(self : self, newOwner : address) -> ();
}
```
so that `import Ownable` yields
`instance ContractStorage(Cxt) : Ownable(Ownable_owner_sel)` and
`import Ownable as Admin` yields
`instance ContractStorage(Cxt) : Ownable(Admin_owner_sel)`. Multi-parameter
classes already exist, and this is the same idea as named instances.
### Constructors
Each mixin's `constructor` becomes an `init` method of its class, with the
generic body as its default. The host's constructor calls the `init` of every
imported mixin explicitly. Composition therefore makes initialization order an
ordinary sequence of calls the author writes, where inheritance derived it from
the linearization.
### Summary
Selectors carry the mixin's state, generic constrained functions carry its
implementation, `CStructField` constraints carry its demand on the host's state,
superclass constraints carry its demand on other mixins, `export` is the class,
`import` is the pair of generated instances, class methods give virtual
dispatch, and default bodies give `super` for free. Composition needs strictly
less machinery than inheritance: it is the same encoding minus the one
construct, `super`, that Section 5 had to add.
## 6. Modeling modifiers as higher-order functions
Section 1 listed modifiers as one of the composition mechanisms, and the vault
in Section 2 used `onlyOwner` for access control. The Core Solidity
representation in Section 3 reproduced that guard by hand: every owner-gated
method opens with a call to an internal `requireOwner()`. This section shows
that the by-hand form is exactly what a modifier would compile to, and proposes
a surface for modifiers that desugars into a higher-order function. As with
inheritance, the construct is sugar over machinery the compiler already has, not
a new primitive.
A modifier is the "around" pattern. It wraps the body of the function it
decorates, running code before and after a hole written `_;`, which stands for
the modified body. That hole is the key: it is a piece of deferred computation
handed to the modifier, which is precisely a function value. So a modifier is
naturally a function that takes the body as a thunk and decides when to run it,
and `_;` is the call to that thunk.
### The proposed surface
A modifier is declared once, with `_;` marking where the modified body runs:
```solc
modifier onlyOwner() {
require(address(caller()) == owner_, Error(0x12b0c500)); // OwnableUnauthorizedAccount()
_;
}
```
A function attaches it by name, after the return type, in the place Classic
Solidity attaches modifiers to a signature:
```solc
public function transferOwnership(newOwner : address) -> () onlyOwner {
owner_ = newOwner;
}
```
### The desugaring
The translation has three steps, and all three land on constructs the compiler
already lowers: surface lambdas `lam(...) { ... }`, application of a function
value, and the `Invokable` type class that application resolves through during
defunctionalization.
First, the modifier becomes an ordinary higher-order function whose extra
parameter is the body thunk. The code before `_;` stays, and `_;` becomes a call
to the thunk. The result type is a fresh type variable, so the same modifier
wraps any function:
```solc
forall r . function onlyOwner(body : () -> r) -> r {
require(address(caller()) == owner_, Error(0x12b0c500));
return body(()); // _; becomes invoking the body thunk
}
```
Because `onlyOwner` is a contract method, it already receives the storage
context, so `owner_` resolves exactly as the hand-written `requireOwner()` does
today. A modifier with code after `_;`, such as a reentrancy guard, binds the
thunk result and returns it afterwards:
```solc
forall r . function nonReentrant(body : () -> r) -> r {
require(locked == uint256(0), Error(0x37ed32e8));
locked = uint256(1);
let result = body(());
locked = uint256(0);
return result;
}
```
Second, the decorated function lifts its body into a thunk and hands it to the
modifier. The thunk is a lambda that closes over the function's parameters and
the storage context:
```solc
public function transferOwnership(newOwner : address) -> () {
return onlyOwner(lam(ignore) { owner_ = newOwner; });
}
```
Stacked modifiers nest outermost-first, which matches Solidity's left-to-right
`_;` chaining: `f ... modA modB { BODY }` becomes
`return modA(lam(i) { return modB(lam(j) { BODY }); });`. A modifier that takes
arguments, such as `onlyAfter(deadline)`, gains them as leading parameters,
`forall r . function onlyAfter(deadline : uint256, body : () -> r) -> r`, and
the call site passes them alongside the thunk,
`onlyAfter(deadline, lam(i) { BODY })`. This matches Classic Solidity, where
modifier arguments are evaluated in the function's scope.
Third, the existing passes finish the lowering with no new support. The `lam`
thunk and the application `body(...)` are defunctionalized into a closure data
type that captures the free variables, an `Invokable` instance, and a lifted
body function, the same shape the compiler already produces for any lambda.
Specialization then fixes the result type at `()` and resolves the `Invokable`
instance to a direct call. What remains is first-order code operationally
identical to `requireOwner(); owner_ = newOwner;`, which is the manual guard
Section 3 wrote out. The modifier surface adds a name for that pattern; it does
not add a mechanism. The `Invokable` encoding that makes this work is the same
one exercised by `test/examples/invokable/028modifier.solc`, where a modifier is
already written as a higher-order function by hand.
### Scope
The scheme covers a single `_;`, which is enough for `onlyOwner`,
`whenNotPaused` and `nonReentrant`. The one capability it leans on beyond the
simplest lambda examples is a closure that captures the storage context and
writes a field, and the demo confirms that field writes inside the thunk compile
and run.
Application still carries an argument, so the thunk is written as taking a unit
value and `_;` passes `()`. This costs nothing at runtime, because unit is erased
by the backend: in the generated Yul, both the `Invokable` `invoke` function and
the lifted lambda body simply lose the parameter, rather than threading an
ignored dummy word through them. Compare the two lowerings of the same thunk,
with a `uint256` placeholder on the left and unit on the right:
```yul
function usr$lambda_impl$..._uint256 (env, ignore) { ... } // dummy word threaded
function usr$lambda_impl$..._unit (env) { ... } // parameter gone
```
Zero-argument application, were it available, would therefore only remove the
`()` from the source; it would not change the code that is emitted.
One cosmetic gap remains on the surface side: there is no `modifier` keyword yet,
so the demo writes the desugared higher-order function directly. That gap does
not change the strategy either: a modifier is a higher-order function, and `_;`
is the call to its body.
## 7. Conclusion
The document carried one contract, a vault share token, through three encodings
to show that contract composition is not a primitive that Core Solidity must
grow, but a use of machinery it already has.
Section 2 set the bar in Classic Solidity. The vault leaned on every composition
mechanism at once: multiple inheritance with C3 linearization, a single virtual
hook that every token movement routes through, `super` to chain the override
onto the inherited body, modifiers for access control, a library, an interface
with a callback, and a CREATE2 factory. The state of three mixins ends up in one
flat layout, and the hook is the one open seam.
Section 3 expressed the same behavior in the Core Solidity the compiler accepts
today. A contract is a type class with a single instance over its storage,
fields become selectors with `CStructField` instances and are read and written
through proxies, and the three mixins collapse into one flat contract whose
shared logic is ordinary methods, with the virtual hook becoming one internal
`update` method and the pause check inlined at that seam. This version is not a
sketch: it lives at `test/examples/valt/VaultToken.solc`, compiles through
`sol-core` and `yule`, and runs on the EVM, with `totalSupply` and the `test`
entry point returning the expected values.
Sections 4 and 5 added a contract-inheritance surface on top of that
representation and showed that it is a desugaring rather than a new primitive.
State reuse is field re-inclusion into a flat layout, subtyping is a superclass
edge, implementation reuse is a generated instance, and overriding is
replacement inside the single instance, which yields virtual dispatch with no
runtime table. The only construct the example forces beyond the plain
representation is `super`, and even that is local: the desugarer preserves the
overridden body under a generated name and threads each `super` call to the
preceding layer.
Sections 4B and 5B replaced that surface with composition, and found that it
asks for less. A mixin becomes a class whose bodies are generic in the storage
type and constrained by the fields they touch; `export` is the class, `import`
generates the field instances plus one forwarding instance, class methods give
virtual dispatch, and default bodies give `super` for free. Subtyping stops
being implied by reuse and becomes the separate, explicit act of re-exporting,
and the diamond problem reduces to instance uniqueness, which the constraint
solver already enforces without needing an order.
Section 6 did the same for modifiers. A modifier is the "around" pattern, so it
desugars into a higher-order function that takes the decorated body as a thunk,
with `_;` becoming the call to that thunk. Defunctionalization and
specialization then reduce it to the same first-order code the manual
`requireOwner()` guard produces, and the rewritten vault at
`test/examples/valt/VaultTokenModifier.solc` compiles and runs with identical
results, confirming that modifiers too are sugar over the lambda and `Invokable`
machinery already present.
The throughline is that the type-class and instance encoding the compiler uses
for field access is already enough to express mixins, shared hooks, access
guards and open recursion. What today's surface still lacks is mostly sugar and
a few orthogonal features, an interface and `inherits` syntax, several stateful
parents with their linearization, events, storable and ABI-decodable `bool`, and
message context, none of which change the underlying strategy. Composition in
Core Solidity is therefore a matter of naming, not of adding a new object model.