Skip to content

BORG Conditions

Overview

A BORG can enforce complex rules on what actions its Gnosis Safe is allowed to execute. Conditions are the mechanism by which these rules are implemented on-chain. The BORG’s Condition Manager (conditionManager.sol) is a modular component that allows multiple custom conditions to be defined and combined with logical operators. In practice, this means you can require certain criteria (time delays, token balances, approvals, etc.) to be met before specific Safe transactions or module functions are permitted. Conditions can apply globally to all actions or only to specific functions, giving fine-grained control over the BORG’s behavior.

Key features of BORG conditions include:
  • Multiple Condition Support: You can attach any number of custom condition contracts to a BORG. Each condition is a separate smart contract implementing the ICondition interface (with a checkCondition function). This modular design means new conditions can be added or removed as needed, even after deployment (by authorized owners).
  • AND/OR Logic: Each condition is labeled with a logical operator specifying how it combines with others – either AND (all specified conditions must be true) or OR (at least one must be true). This enables complex logic. For example, you could require both a time delay and a token balance threshold (AND logic), or allow an action if any one of several conditions is satisfied (OR logic).
  • Global vs. Function-Specific: Conditions can be enforced globally (across the entire contract) or tied to a specific function. A global condition might apply to all actions (e.g. “the DAO’s token price must be above $X for any spending action”), whereas a function-level condition targets one particular method (e.g. “removing a member requires 5 signatures”). The Condition Manager supports both by maintaining a list of global conditions and a mapping of function signatures to condition lists.

The Condition Manager Contract

The Condition Manager (ConditionManager.sol) is the component that stores and evaluates conditions for a BORG. Every implant module in a BORG inherits the Condition Manager, meaning each module gains the ability to check conditions on its operations. The Condition Manager itself extends the BORG’s access control (it inherits BorgAuthACL), so only authorized addresses (usually the DAO or an oversight authority) can add or remove conditions. This ensures that BORG members cannot arbitrarily change the rules; condition management is a privileged action (onlyOwner).

Condition storage: Internally, the manager keeps:

  • An array conditions[] for global conditions, each with an address of a condition contract and a logic flag. These are evaluated for any check of the contract as a whole.
  • A mapping conditionsByFunction that maps a function signature (bytes4) to an array of condition contracts (with logic flags) specific to that function. These are evaluated only when that particular function is invoked.

Each condition is expected to implement the standard interface ICondition.checkCondition(address _contract, bytes4 _functionSignature, bytes data) which returns true or false. The Condition Manager verifies that any added condition contract supports this interface via ERC-165; otherwise the addition is rejected.

Adding & removing conditions: The BORG’s owners (as defined by the ACL) can add a condition by calling addCondition(op, conditionAddress) for a global condition or addConditionToFunction(op, conditionAddress, functionSig) for a function-specific condition. Here op is the logic operator (Logic.AND or Logic.OR) to associate with that condition. The manager will ensure you don’t add duplicates or invalid contracts. Similarly, conditions can be removed via removeCondition(index) or removeConditionFromFunction(index, functionSig) by index.

Condition evaluation: The Condition Manager provides two mechanisms to enforce conditions at runtime: a function to check all global conditions, and a modifier to check function-specific conditions.

  • Global check (checkConditions) – This view function iterates through every condition in the global conditions array and calls its checkCondition. The logic is straightforward: for each condition, if it’s labeled AND, a false result will immediately fail the entire check (returning false). If it’s labeled OR, a true result will immediately succeed the check (returning true). After looping, if no condition triggered an early return, the outcome is either true (if all AND conditions passed or if the last OR checked was true) or false (if none of the OR conditions were met). In essence: all AND-type conditions must pass, and at least one OR-type condition must pass. If no global conditions are defined, checkConditions trivially returns true. This function can be called within contract logic to enforce global constraints before executing an action.

  • Function-level check (conditionCheck modifier) – For more granular control, the Condition Manager offers a modifier conditionCheck() that can be applied to individual functions. This modifier looks up any conditions associated with the function being called (conditionsByFunction[msg.sig]) and requires they be satisfied. It uses a similar AND/OR evaluation: each condition for that function is checked via checkCondition. If an AND-type condition returns false, the modifier triggers a revert (ConditionNotMet); if an OR-type condition returns true, it marks the requirement as fulfilled. After checking all, if at least one condition has been met (or if the function had no conditions), execution proceeds. If no condition was met, it reverts. By attaching this modifier to a function, the developer ensures that whenever that function is called, its specific prerequisite conditions are automatically enforced.

Example: Suppose a BORG implant wants to require that either a time delay has passed or a majority of owners have approved before allowing a certain operation. The DAO (as contract owner) could add two conditions – a TimeCondition and a SignatureCondition – both with logic OR for that function. The Condition Manager will then permit the function call if any one of those conditions is true (enough signatures collected or the time lock expired). If the requirement was that both a delay pass and approvals are given, the DAO would assign one condition as AND and the other as AND (or simply make them global AND conditions), forcing both to be true. This flexibility in logic composition is what allows BORG conditions to mirror real-world approval workflows and legal requirements.

Implementing Custom Conditions (ICondition Interface)

Conditions are pluggable because each is a separate smart contract adhering to a simple interface. The ICondition interface defines one function, checkCondition(address _contract, bytes4 _functionSignature, bytes data) external view returns (bool), which should return true if the condition is satisfied in the given context. The parameters give the condition contract context about what’s happening: _contract is typically the address of the contract whose action is being checked (for a BORG implant, this might be the Safe or module address), _functionSignature is the function being invoked, and data is any extra data (often unused or empty, but available for complex conditions).

To create a new condition type, you can inherit the BaseCondition abstract contract and implement the checkCondition logic. BaseCondition already implements the interface and includes an ERC-165 support check for ICondition, so inheriting it simplifies development. For example, a minimal custom condition contract could look like:

import {BaseCondition} from "./BaseCondition.sol";
 
contract MyCustomCondition is BaseCondition {
    // Example state or parameters
    address public immutable target;
    uint256 public immutable threshold;
 
    constructor(address _target, uint256 _threshold) {
        target = _target;
        threshold = _threshold;
    }
 
    function checkCondition(address _contract, bytes4 _func, bytes memory data) 
        public 
        view 
        override 
        returns (bool) 
    {
        // Your custom logic:
        // return true if condition passes, false if not.
        // (For example, ensure _contract holds at least `threshold` of some token, etc.)
        return /* bool expression */;
    }
}

Any state needed for the condition (addresses, numeric limits, etc.) can be stored in the condition contract. The Condition Manager treats this contract as a black box that returns a boolean when asked. This design means you can encode almost any rule as a condition contract – whether it’s checking an external contract’s state, aggregating signatures, or even querying an oracle (as long as the oracle value is fed on-chain). Because conditions are external contracts, they can maintain their own internal state and update over time (for example, a condition contract might include functions to update thresholds or record approvals). The BORG modules don’t need to know the details; they simply call checkCondition on the condition contract when enforcing rules.

When deploying a BORG, MetaLeX provides a suite of pre-built condition contracts (detailed below) covering common use cases. These can be added directly to your BORG’s Condition Manager. If those don’t fit your needs, you can develop a custom condition contract using the pattern above, and as long as it implements ICondition, it can be integrated seamlessly.

Built-in Condition Types

MetaLeX’s BORG core comes with several ready-made condition contracts in the src/libs/conditions library. These serve as examples and common tools that can be immediately applied. Below is a list of the main condition types and what they enforce:

  • TimeCondition: This condition checks the current blockchain timestamp against a target time, either ensuring the time is before a specified moment or after it. It takes an immutable targetTime and a comparison mode (BEFORE or AFTER) on construction. The condition passes (returns true) only if the current time is on the correct side of the target time (e.g. AFTER a deadline, or BEFORE an expiration). This is useful for enforcing timelocks or scheduling restrictions (e.g., “action X cannot be performed until after date Y”).

  • BalanceCondition: Checks an ERC-20 token balance against a threshold. This condition is constructed with a token address, a target account, an amount, and a comparison (GREATER or LESS). At check time, it reads the current token balance of the target address and compares it to the preset amount. It returns true if the balance is >= the amount (for GREATER) or <= the amount (for LESS). This can enforce that a certain account (such as the BORG’s Safe or a beneficiary) has a required amount of tokens. For example, you might require the Safe to maintain a minimum collateral balance before allowing new loans, etc. (Note: GREATER uses >= and LESS uses <= by implementation.)

  • ChainLinkOracleCondition: Integrates an external price feed via Chainlink oracles. This condition is initialized with a reference to a Chainlink Aggregator (price feed), a target price (as an int256), and a comparison type (GREATER, EQUAL, or LESS). When checked, it fetches the latest price data from the oracle and compares it to the target price. The condition passes if the price relation matches the specified condition (e.g., price > target for GREATER). Importantly, this contract also ensures the oracle data is fresh: if the latest price update is older than a configured duration, it will revert with an error (treated as condition not met). This prevents actions from proceeding based on stale price information. A ChainLinkOracleCondition could be used to, say, allow a trade only if an asset’s price is below a certain threshold (perhaps for buyback conditions) or above a threshold (for distribution events), with the reliability of Chainlink feeds.

  • API3OracleCondition: Similar to the Chainlink condition, but designed for API3’s oracle framework (dAPIs and Airnodes). It uses API3’s proxy interface to read a value and timestamp from a data feed. The condition compares the value to a target (GREATER/EQUAL/LESS) and likewise enforces a freshness threshold – if the data is older than a specified duration, it reverts as stale. Functionally, it accomplishes the same goal: gating actions based on an external data feed’s value. You would choose between ChainLinkOracleCondition and API3OracleCondition depending on which oracle network your data source is on.

  • SignatureCondition: Implements a multi-signature approval requirement off-chain from the Safe itself. You deploy it with a set of signer addresses and a threshold number of signatures, plus a logic mode (AND vs OR). Each designated signer can call the condition’s sign() function to register their approval (and revokeSignature() to withdraw it). When checkCondition is called, the contract counts how many signers have signed and compares against the threshold: if the condition’s logic is AND, it requires all specified signers to have signed (signatureCount == total signers). If the logic is OR, it requires at least the threshold number to have signed (signatureCount >= threshold). This condition is satisfied independent of the Safe’s own transaction approvals – it’s an additional off-chain approval layer. For example, if a BORG Safe has 5 owners, a SignatureCondition could require that 3 out of those 5 call sign() before a certain action is allowed. You might use logic=AND for a unanimous consent requirement, or OR with threshold for majority consent. (Note: the sign() method in this condition is typically called as a separate transaction by the signers sometime before the guarded action is attempted.)

  • MultiUseSignatureCondition: This is an extension of the signature condition concept, geared for per-action approvals. A MultiUseSignatureCondition (defined in multiUseSignCondition.sol) automatically pulls the BORG Safe’s owner list as the set of signers and sets a threshold on how many must approve. What makes it “multi-use” is that it tracks approvals per specific contract call. When signers call its sign(address _contract, bytes _data) function, they approve a particular target contract and calldata combination. The condition stores who signed what, and maintains a count of signatures for each unique _contract + _data combination. The check will return true if either the Safe itself signed (there’s a shortcut allowing the Safe to directly approve, treating a Safe execution as an automatic approval) or if the number of owner signatures for that exact call meets the threshold. This condition is useful for scenarios where each transaction or action needs a fresh set of approvals from owners. For instance, you could require that for any specific large transfer, at least N owners have pre-approved that exact transfer (by calling sign with the recipient and amount data). Since it ties signatures to the exact calldata, owners cannot blanket-approve a broad action – they must approve each instance, providing fine control.

  • DeadManSwitchCondition: A special fail-safe condition that triggers after a period of inactivity. The idea is to detect if the BORG’s Safe has become dormant or if the members are incapacitated. The DeadManSwitchCondition is set up with a delay interval (e.g. 30 days) and the address of the BORG’s Safe. Authorized callers (like the DAO or designated guardians) can invoke initiateTimeDelay() to start the countdown and record the Safe’s current nonce. Once initiated, if the specified delay passes without the Safe’s nonce changing (meaning the Safe has not executed any transaction in that period), then checkCondition will return true. If at any time a Safe transaction occurs, the Safe’s nonce will increment, causing the condition to reset (or it can be manually reset via resetTimeDelay() by an authorized caller). This acts as an automatic trigger for emergency actions: for example, the BORG’s FailSafeImplant uses a DeadManSwitchCondition so that if the Safe owners do nothing for the delay period, the DAO is allowed to recover funds from the Safe. In practice, the DeadManSwitchCondition ensures that prolonged inactivity (which might indicate the BORG is defunct or its private keys are lost) can be detected and responded to on-chain.

Each of these built-in conditions inherits from BaseCondition and implements the necessary logic in checkCondition as summarized above. They are meant to be building blocks – you can mix and match them in the Condition Manager to achieve the desired composite rules for your BORG. For example, a Grants BORG might use a TimeCondition (to enforce a vesting cliff), a BalanceCondition (to ensure a grantee has provided collateral), and a SignatureCondition (to require multi-party approval for releasing funds). All those can be added and configured via the Condition Manager.

Using Conditions in BORG Modules

In a deployed BORG, implants (the modular contracts granting specific powers to the Safe) leverage the Condition Manager to guard their critical functions. As noted, every implant contract already includes the Condition Manager logic, so you can directly call addCondition or addConditionToFunction on an implant to configure its rules. The BORG’s admin (DAO or an authority BORG) typically sets up conditions during deployment or as part of governance decisions.

Example – FailSafeImplant: The FailSafe implant, which allows the DAO to claw back funds from the Safe in emergencies, uses conditions to ensure it only triggers under the right circumstances. In its recoverSafeFunds() function (which transfers assets out of the Safe), the implant includes both a function-level and a global condition check: it is declared with the conditionCheck() modifier and internally calls if (!checkConditions(\"\")) revert before proceeding. In a typical setup, the BORG’s admin might attach a DeadManSwitchCondition globally to this implant (requiring inactivity) and perhaps a SignatureCondition to the recoverSafeFunds function (requiring a certain number of DAO signatories to approve the recovery). As a result, the funds recovery will execute only if both the Safe has been inactive for the delay period and the required approvals have been given – otherwise the Condition Manager will block it, throwing a ConditionsNotMet error. This aligns with the idea that a fail-safe should only activate when clearly necessary (time elapsed with no activity) and agreed upon (multi-signature approval), protecting against premature or malicious triggers.

Example – EjectImplant: Another module, the Eject implant, allows removal of a BORG member from the Safe (and other membership management). Its functions like ejectOwner and changeThreshold are likewise protected by conditions. The code shows these functions use the conditionCheck modifier and then explicitly call checkConditions to enforce any global conditions. An admin could, for instance, add a condition requiring a DAO vote (perhaps through a SignatureCondition representing DAO multisig or an oracle reflecting an off-chain vote) before any owner can be ejected. They might also add a TimeCondition to require a notice period before the change. When someone attempts to call ejectOwner, the Condition Manager will automatically verify those prerequisites and prevent the call if they aren’t fulfilled, emitting a ejectImplant_ConditionsNotMet error. Only when the conditions return true will the Safe’s owner removal actually execute, at which point the implant uses Gnosis Safe’s module mechanism to adjust the Safe’s owners list. This shows how conditions act as a safety net or checkpoint around sensitive governance actions.

General usage workflow: To apply conditions in your BORG, follow these steps:

  1. Deploy or identify the needed condition contracts. For each rule, deploy the corresponding condition (or reuse an existing deployed one). For example, deploy TimeCondition(targetTime, AFTER) for a time lock, or SignatureCondition([listOfSigners], threshold, OR) for a multi-sig approval condition. Ensure the constructor parameters reflect your policy.
  2. Add conditions via the Condition Manager. Using the BORG’s admin account (with onlyOwner permissions on implants), call the implant’s addCondition or addConditionToFunction. Specify the logic (AND/OR), the condition’s address, and (if function-specific) the function signature. For convenience, you can usually get a function signature in Solidity by using MyImplant.contractMethod.selector.
  3. Verify condition integration. Once added, the condition is active. Any attempt to execute the guarded actions will now trigger the checks. It’s wise to test scenarios: e.g., try calling the function before the condition is met to ensure it correctly blocks (the transaction should revert with a ConditionNotMet error), then satisfy the condition (e.g., have signers call sign(), or wait until the time passes) and call again to confirm it succeeds.
  4. Monitor and update as needed. Conditions can often be adjusted post-deployment. For instance, the threshold in a SignatureCondition can be increased by redeploying a new condition or, if the contract supports it, calling an update function (the MultiUseSignatureCondition has an updateThreshold method, guarded by its owner). If a condition becomes obsolete or an error is found, an authorized user can remove it via removeCondition functions, potentially replacing it with a corrected version. Always ensure any changes to conditions align with the BORG’s legal agreements, as these on-chain conditions are meant to enforce those off-chain rules.

By combining these condition contracts, a MetaLeX BORG can programmatically ensure that Safe transactions adhere to the agreed governance policies. The on-chain Condition Manager acts as a real-time referee, checking every important action against a list of encoded rules. This structure provides a high degree of assurance that a BORG’s wallet will operate only within the bounds its stakeholders have set. And because the system is modular, it can evolve: new condition types can be introduced as new needs arise (for example, a KYC verification condition or other oracle-based conditions could be added in the future).

In summary, BORG conditions are a powerful feature that turn a basic multi-sig into a governed cybernetic entity. They allow encoding time delays, approvals, external data requirements, and more, directly into the Safe’s decision process. Using the Condition Manager and the suite of provided condition contracts (or your own custom ones), you can make your BORG’s smart contracts reflect complex organizational logic and legal constraints, ensuring that “code honors contract” within MetaLeX’s framework.