Chapter 5 / 15
External Contract Calls
interface, @non_reentrant, and the CEI pattern.
Calling other contracts is unavoidable in DeFi. Covenant's interface
keyword declares the external ABI, and @non_reentrant inserts a
mutex at the bytecode level. The Checks-Effects-Interactions pattern is
enforced by convention and auditable via OMEGA lint rule CEI-001.
interfaces.cov
// Declare the external contract's ABI
interface IERC20 {
action transfer(to: address, amount: u256) -> bool;
action transferFrom(from: address, to: address, amount: u256) -> bool;
view balanceOf(who: address) -> u256;
view allowance(owner: address, spender: address) -> u256;
}
interface IUniswapV2Pair {
action swap(
amount0Out: u256,
amount1Out: u256,
to: address,
data: bytes
);
view getReserves() -> (u112, u112, u32);
}vault.cov — non-reentrant external calls
record Vault {
token: address;
shares: map(address => u256);
total: u256;
error NothingToWithdraw();
// @non_reentrant inserts an on-chain mutex (SSTORE lock slot)
@non_reentrant
action deposit(amount: u256) {
let erc20 = IERC20(self.token);
// Checks
// (amount > 0 validated by the caller passing a nonzero value)
// Effects — update state BEFORE external call
let new_shares = amount; // simplified 1:1 for illustration
self.shares[msg.sender] += new_shares;
self.total += amount;
emit Deposited(msg.sender, amount, new_shares);
// Interactions — external call LAST
erc20.transferFrom(msg.sender, self.addr(), amount);
}
@non_reentrant
action withdraw() {
let user_shares = self.shares[msg.sender];
if user_shares == 0 {
revert_with NothingToWithdraw();
}
// Effects before Interactions
self.shares[msg.sender] = 0;
self.total -= user_shares;
emit Withdrawn(msg.sender, user_shares);
// Interaction last
IERC20(self.token).transfer(msg.sender, user_shares);
}
view share_of(who: address) -> u256 {
return self.shares[who];
}
event Deposited(who: address, amount: u256, shares: u256);
event Withdrawn(who: address, shares: u256);
}Annotations
interface I { ... } | declares an external contract type. Calling I(addr).method() compiles to a CALL with the correct 4-byte selector. |
@non_reentrant | inserts a transient storage mutex (EIP-1153 where available, SSTORE otherwise). Reverts if the function is re-entered. |
self.addr() | returns the contract's own address — equivalent to Solidity's address(this). |
| CEI pattern | is not enforced at compile time in V0.7, but OMEGA lint rule CEI-001 flags violations. Future versions will enforce it statically. |
Key takeaways
- Declare interfaces explicitly — Covenant will generate the correct selectors and ABI types.
- Always use
@non_reentranton any action that makes external calls. - State updates must precede external calls (CEI). OMEGA will flag violations.