05 — External Calls
Calling an interface
Define an interface and call through it:
interface IERC20 {
action transfer(to: Address, amount: u256) -> Bool
action balanceOf(who: Address) -> u256
}
contract Spender {
field token: Address
action spend(amount: u256) {
let tok = IERC20(self.token);
let ok = tok.transfer(msg.sender, amount);
require(ok, TransferFailed);
}
}
Safety: reentrancy guard
Covenant’s built-in @nonreentrant decorator locks the contract for the duration of the action:
@nonreentrant
action withdraw(amount: u256) {
require(self.balances[msg.sender] >= amount, InsufficientBalance);
self.balances[msg.sender] -= amount; // effect before interaction
let ok = IERC20(self.token).transfer(msg.sender, amount);
require(ok, TransferFailed);
}
The compiler also enforces checks-effects-interactions order via a static lint — disable with @allow(cei_violation) only when you have verified the call is safe.
Low-level calls
For untyped calls (e.g. proxies):
let result = call(target, data: calldata_bytes, value: 0);
require(result.success, CallFailed);
Sending ETH
action fund(recipient: Address) {
require(msg.value > 0, ZeroValue);
transfer_eth(recipient, msg.value);
}
transfer_eth is equivalent to Solidity call{value: amount}("") with a 2300 gas stipend check disabled (Covenant does not use the Solidity 2300-gas anti-pattern).
staticcall
Use @view actions on external interfaces — the compiler emits STATICCALL automatically:
@view
action quote(amount: u256) -> u256 {
return IOracle(self.oracle).price() * amount / 1e18;
}