Smaug is a guard contract for Safe multi-signature wallets that implements budget controls and transaction approval mechanisms. It's designed to protect assets by enforcing spending limits based on various timeframes:
- Daily limits
- Block limits
- Per-transaction limits
- Total (lifetime) limits
- Budget enforcement for multiple assets
- Transaction pre-approval mechanism
- Time-locked policy updates
- Support for ERC20 tokens and native ETH
- End-of-transaction balance tracking (not intra-transaction monitoring)
Smaug only monitors the net balance change at the end of transaction execution, not intermediate transfers during execution. This means:
- Only the final token/ETH outflow is considered when checking budget limits
- Internal transfers within a transaction can temporarily exceed budget limits
- As long as the final balance after transaction execution is within budget limits, the transaction will not be restricted
- This design allows for complex transactions with multiple internal transfers while still enforcing overall budget controls
Smaug uses a Time-To-Live (TTL) mechanism that affects several aspects of the contract's behavior:
- Definition: TTL is a time period (in seconds) that determines how long it takes for various approvals and scheduled changes to become valid
- Default: The TTL is set during contract initialization (typically 86400 seconds or 1 day)
- Scope: The TTL value applies globally to all assets protected by the contract
- Pre-approved transactions: The TTL acts as a maturity period for pre-approved transactions. When a transaction is pre-approved, it is still subject to budget checks until the TTL period has passed. Only after this waiting period does it bypass budget checks. The exact check is:
if (block.timestamp - preApprovalTimestamp < TTL)
, then budget checks apply. - Policy updates: Changes to asset policies (budget limits) are scheduled and only applied after the TTL period has passed from the time of scheduling.
- TTL updates: Changes to the TTL value itself are also time-locked and only take effect after waiting for the current TTL period from when the update was scheduled.
To schedule a TTL update:
// Schedule a change to the TTL value (will take effect after the current TTL period)
smaug.scheduleTTLUpdate(43200); // Change TTL to 12 hours (43200 seconds)
- Pre-approved transactions require maturity: The TTL acts as a maturity period for pre-approved transactions. When a transaction is pre-approved by hash, it is still subject to budget checks until the TTL period has passed. Only after this waiting period does it bypass budget checks.
- Pre-approvals mature after TTL period: After the TTL period passes from when a transaction was pre-approved, that transaction becomes exempt from standard budget checks and limits.
- TTL is a mandatory waiting period: For pre-approved transactions, TTL defines the minimum waiting period before the pre-approval takes effect and can bypass budget checks.
- All updates respect TTL: Policy changes, TTL changes, and pre-approval maturity all follow the same time-based TTL mechanism.
Tests were conducted to verify the core functionality of the Smaug contract. Key findings:
- ✅ Budget Enforcement: Successfully restricts transactions exceeding configured limits
- ✅ Pre-approval Bypass: Correctly allows pre-approved transactions to bypass budget checks
- ✅ Pre-approval Expiration: Pre-approved transactions correctly expire after the TTL period
- ✅ Pre-approval Revocation: Pre-approval can be revoked before activation
- ✅ Asset Protection: Multiple assets can be protected with independent budgets
- ✅ TTL Updates: TTL updates are correctly applied after the TTL period
To protect an asset with budget controls:
// Add protection for an ERC20 token
smaug.addProtectedAsset(
tokenAddress, // Zero address for ETH
{
inDay: 1000 ether, // 1000 tokens per day
inBlock: 100 ether, // 100 tokens per block
inTX: 50 ether, // 50 tokens per transaction
inTotal: 10000 ether // 10000 tokens total lifetime limit
}
);
To bypass budget controls for specific transactions:
// Pre-approve a transaction hash
bytes32 txHash = keccak256("Your transaction hash here");
smaug.preApproveTx(txHash);
If a pre-approved transaction needs to be canceled or revoked:
// Revoke a previously pre-approved transaction
bytes32 txHash = keccak256("Your transaction hash here");
smaug.revokePreApprovedTx(txHash);
This will immediately invalidate the pre-approval, causing any subsequent executions of the transaction to be subject to normal budget checks, even if the TTL period has passed.
To update budget controls with a time-lock:
// Schedule a policy update
smaug.schedulePolicyUpdate(
tokenAddress,
{
inDay: 2000 ether, // Updated limits
inBlock: 200 ether,
inTX: 150 ether,
inTotal: 20000 ether
}
);
The code was reviewed internally for security issues and best practices. No critical issues were found.