Secure Patterns
Covenant’s type system and compiler catch many vulnerabilities automatically. This page documents patterns that go beyond static analysis — design-level decisions that prevent entire vulnerability classes.
1. Checks-Effects-Interactions (CEI)
Always perform state changes before external calls. Covenant’s LSP warns on violations; the compiler enforces it for @nonreentrant actions.
// CORRECT — effect before interaction
@nonreentrant
action withdraw(amount: u256) {
require(self.balances[msg.sender] >= amount, InsufficientBalance);
self.balances[msg.sender] -= amount; // effect
transfer_eth(msg.sender, amount); // interaction
}
2. Pull payments over push payments
Instead of pushing ETH to recipients (reentrancy risk), let them pull:
field pending_withdrawals: Map<Address, u256>
// Internally: schedule payment
internal action _schedule_payment(to: Address, amount: u256) {
self.pending_withdrawals[to] += amount;
}
// Publicly: recipient pulls their own funds
@nonreentrant
action withdraw_payment() {
let amount = self.pending_withdrawals[msg.sender];
require(amount > 0, NothingToWithdraw);
self.pending_withdrawals[msg.sender] = 0;
transfer_eth(msg.sender, amount);
}
3. Use only() guards, not if/revert manually
// PREFERRED
action admin_action() { only(owner); ... }
// AVOID — easy to forget, no static verification
action admin_action() {
if msg.sender != owner() { revert Unauthorised; }
...
}
4. Validate oracle staleness
Always check updatedAt when consuming price feeds:
let (_, answer, _, updatedAt, _) = feed.latestRoundData();
require(block.timestamp - updatedAt <= MAX_STALENESS, StalePrice);
require(answer > 0, NegativePrice);
5. Two-step ownership transfer
Single-step transfer_ownership can permanently lose a contract if you transfer to a wrong address. Use a two-step pattern:
field pending_owner: Address
action propose_ownership(new_owner: Address) {
only(owner);
self.pending_owner = new_owner;
}
action accept_ownership() {
require(msg.sender == self.pending_owner, NotPendingOwner);
self.pending_owner = Address(0);
_transfer_ownership(msg.sender);
}
6. Emit events for all state changes
Every mutation that downstream systems or UIs need to observe should emit an event. Covenant’s LSP detector missing-event flags state mutations in @view contexts.
7. Timelocks for governance actions
Protect high-impact actions with a timelock:
field upgrade_delay: u256 = 48 * 3600 // 48 hours
field pending_upgrade: Address
field upgrade_eta: u256
action propose_upgrade(new_impl: Address) {
only(owner);
self.pending_upgrade = new_impl;
self.upgrade_eta = block.timestamp + self.upgrade_delay;
emit UpgradeProposed(new_impl, self.upgrade_eta);
}
action execute_upgrade() {
only(owner);
require(block.timestamp >= self.upgrade_eta, TooEarly);
_upgrade(self.pending_upgrade);
}
8. Integer overflow
Covenant uses checked arithmetic by default. All +, -, * on integer types revert on overflow. Use unchecked { } only when you have mathematically proven safety:
// Safe by default
self.total += amount;
// Unchecked (document why it is safe)
unchecked {
// amount is always < (2^256 - total) because of the cap check above
self.total += amount;
}