Inheritance & Interfaces
SolidCash supports contract inheritance, interfaces, abstract contracts, libraries, and free functions. These features allow you to compose reusable building blocks, define standard APIs, and organize large codebases across multiple files.
Imports
SolidCash provides three import styles:
// Wildcard - imports all top-level definitions
import "./Base.scash";
// Namespace - access via prefix
import "./Base.scash" as Base;
// Usage: Base.MyStruct, Base.myFunc(); constants need a wildcard or selective import
// Selective - import specific items
import { MyStruct, myFunc } from "./Lib.scash";
NPM-Style Imports
import "@solidcash/math/SafeMath.scash";
import { MAX_VALUE } from "my-library/constants.scash";
A relative path resolves against the importing file. Any other path is looked up in node_modules/, walking up
from the importing file, and then next to it. The compiler appends .scash when the path has no extension.
Imports must come first in a file, after any leading pragmas. Files may import each other: a circular import
compiles. A circular inheritance graph (A is B while B is A) is a compile error.
What Can Be Imported
- Constants
- User-defined value types
- Enums and structs
- Events and errors
- Interfaces
- Free functions
- Libraries and contracts
using ... fordirectives
See Imports & Interfaces for what each import form brings into scope.
Contract Inheritance
Contracts can inherit from one or more parent contracts using the is keyword.
contract Ownable(pubkey owner) {
modifier onlyOwner(sig s) {
require(checkSig(s, owner));
_;
}
}
contract Token(pubkey owner, int supply) is Ownable(owner) {
int totalSupply;
constructor() {
this.totalSupply = supply;
}
function mint(bytes20 to, int amount, sig s) onlyOwner(s) {
this.totalSupply = this.totalSupply + amount;
}
}
Inherited functions, modifiers, state variables, and types are all available in the child contract.
A base contract's parameters are supplied in the is list (is Ownable(owner)) or by the child's constructor
(constructor() Ownable(owner) { ... }). Natively, a base listed without arguments passes its parameters on
instead: they are prepended to the child's own parameters, and a child parameter with the same name is merged
with them.
Multiple Inheritance
contract Ownable(pubkey owner) {
modifier onlyOwner(sig s) { require(checkSig(s, owner)); _; }
}
contract Pausable() {
bool paused;
modifier whenNotPaused() { require(!this.paused); _; }
}
contract ManagedToken(pubkey owner) is Ownable(owner), Pausable {
function transfer(bytes20 to, int amount, sig s)
onlyOwner(s) whenNotPaused()
{
// ...
}
}
Virtual & Override
Mark functions as virtual to allow overriding, and override to replace a parent implementation:
contract Base() {
function getValue() virtual returns (int) {
return 1;
}
}
contract Child() is Base {
function getValue() override returns (int) {
return 2;
}
}
virtual is enforced: overriding a function the base did not mark virtual is a compile error.
Both keywords can be combined to allow further overriding down the chain. An intermediate contract must mark
its own implementation virtual as well as override:
contract Base() {
function getValue() virtual returns (int) { return 1; }
}
contract Child() is Base {
function getValue() virtual override returns (int) { return 2; }
}
contract GrandChild() is Child {
function getValue() override returns (int) { return 3; }
}
Diamond Inheritance
Inheritance is flattened at compile time by merging parents. When two parents define the same
function (a diamond), the child must provide an explicit override to resolve the ambiguity;
without it the compiler errors. Functions inherited from a single common ancestor are deduplicated
rather than treated as a conflict. Natively a bare override is enough; under evmCompat the override
must list every base that provides the function, as in Solidity (override(B, C)). The listed form works in
both modes.
contract A() {
function foo() virtual { require(1 == 1); }
}
contract B() is A {
function foo() virtual override { require(2 == 2); }
}
contract C() is A {
function foo() virtual override { require(3 == 3); }
}
contract D() is B, C {
function foo() override(B, C) { require(4 == 4); } // must resolve conflict
}
super.f() and Base.f() resolve along the C3 linearization (the same method resolution order
Python and Solidity use). As in Solidity, bases are listed from the most base-like to the most derived, so the
rightmost base comes first. For the diamond above the order is [D, C, B, A]: super.foo() inside D
reaches C, super.foo() inside C reaches B (the sibling branch) before the shared ancestor A, and
super.foo() inside B reaches A - cooperative super. An inheritance graph with no valid C3 ordering is a
compile error. Use super.f() to call the next implementation in this order, or Base.f() to call a specific
ancestor's directly.
Abstract Contracts
Abstract contracts define partial implementations that must be completed by inheriting contracts:
abstract contract Pausable() {
bool paused;
function pause() internal; // no body = abstract function
function unpause() internal; // must be implemented by child
}
contract PausableToken(pubkey admin) is Pausable {
function pause() internal override {
this.paused = true;
}
function unpause() internal override {
this.paused = false;
}
function act(sig s) public {
require(checkSig(s, admin));
pause();
}
}
- An abstract contract cannot be deployed or created with
new. Compiled on its own, it yields an ABI-only artifact with empty bytecode. - All abstract functions must be implemented in any non-abstract child.
- An implementing child must mark the function
override. Abstract functions are implicitlyvirtual;evmCompatrequires writingvirtualon them, as Solidity does.
Interfaces
Interfaces define a contract's public API without implementation. They are used for external contract interaction and type checking.
interface IERC20 {
event Transfer(bytes20 indexed from, bytes20 to, int amount);
public int totalSupply;
public mapping(bytes20 => int) balances;
function transfer(bytes20 to, int amount) external;
function balanceOf(bytes20 account) external returns (int);
}
Interface Features
- Function signatures (no body)
- State variable declarations, with the visibility keyword first (
public int counter;); the visibility must bepublicorexternal - Mapping and array declarations, with the same rule
- Event and error declarations
- Enum, struct and user-defined value type definitions
using ... fordirectives and a bodylessfallback- Inheritance:
interface IERC721 is IERC165 { }
A contract that lists an interface in its is list must implement the interface's functions.
Interface ID (EIP-165 Style)
interface IERC20 {
function transfer(bytes20 to, int amount) external;
}
contract Probe() {
function check() {
// XOR of all 4-byte function selectors - EIP-165 style (bytes4, like Solidity).
bytes4 id = type(IERC20).interfaceId;
require(id.length == 4);
}
}
SolidCash selectors are sha256-based by default, so the value differs from an EVM contract's; under the
evmCompat compiler option they are real keccak256 over Solidity's canonical signature spelling.
External State Reads
Interfaces enable reading another contract's state:
interface ICounter {
public int counter;
}
contract Reader(address counter) {
int lastSeen;
function readCounter() public {
this.lastSeen = ICounter(counter).counter;
}
}
The reading contract must be stateful (it needs at least one state variable). The read is compiled as a covenant that co-spends the target's state UTXOs, which requires a block layout to place them in.
See Imports & Interfaces and Advanced Features for more on external reads and calls.
Libraries
Libraries are collections of reusable functions:
library MathLib {
function square(int x) returns (int) {
return x * x;
}
function clamp(int value, int lo, int hi) returns (int) {
if (value < lo) return lo;
if (value > hi) return hi;
return value;
}
}
A library function without an explicit visibility is internal, and internal library functions are inlined
where they are called. Compiled on its own, a library like this yields an ABI-only artifact with empty
bytecode. Use libraries via imports or using directives:
import { MathLib } from "./MathLib.scash";
using MathLib for int;
contract Calculator() {
function compute(int x) {
int sq = x.square(); // method syntax via using-for
int clamped = MathLib.clamp(x, 0, 100); // direct call
require(sq >= clamped);
}
}
See Libraries & Using-For for using ... for, operator binding and libraries
with public functions.
Free Functions
Functions defined at file level (outside any contract) are callable from any contract in scope:
int constant MAX_AMOUNT = 10_000;
function validateAmount(int amount) {
require(amount > 0, "Amount must be positive");
require(amount <= MAX_AMOUNT, "Amount too large");
}
contract Vault() {
function deposit(int amount) {
validateAmount(amount);
// ...
require(true);
}
}
Free functions:
- Cannot access contract state (
this.is not available) - Can be imported across files
- Useful for shared validation logic and utilities
File-Level Constants
int constant FEE_RATE = 100;
bytes32 constant ZERO_HASH = bytes32(0);
Constants defined at file level are importable and available to all contracts in the file.