// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {Assertion} from "../../lib/credible-std/src/Assertion.sol";
import {PhEvm} from "../../lib/credible-std/src/PhEvm.sol";
import {ISafe} from "@safe-global/safe-contracts/contracts/interfaces/ISafe.sol";
contract UxLinkMultisigProtectionAssertion is Assertion {
uint256 constant COOLING_PERIOD = 24 hours;
mapping(address => uint256) public lastThresholdChange;
mapping(address => uint256) public lastOwnerChange;
mapping(address => mapping(address => bool)) public isOwnerWhitelisted;
function triggers() external view override {
triggerRecorder.registerCallTrigger(
this.assertThresholdProtection.selector,
ISafe.changeThreshold.selector
);
triggerRecorder.registerCallTrigger(
this.assertOwnerAddition.selector,
ISafe.addOwnerWithThreshold.selector
);
triggerRecorder.registerCallTrigger(
this.assertOwnerRemoval.selector,
ISafe.removeOwner.selector
);
}
/// @notice Prevents threshold reduction and ensures minimum threshold of 2
function assertThresholdProtection() external view {
ISafe safe = ISafe(ph.getAssertionAdopter());
PhEvm.CallInputs[] memory calls = ph.getCallInputs(
address(safe),
ISafe.changeThreshold.selector
);
uint256 newThreshold = abi.decode(calls[0].input, (uint256));
uint256 currentThreshold = safe.getThreshold();
// Never allow threshold reduction
require(newThreshold >= currentThreshold, "Threshold cannot be lowered");
// Prevent rapid threshold changes
uint256 lastChange = lastThresholdChange[address(safe)];
require(
block.timestamp >= lastChange + COOLING_PERIOD,
"Threshold change too soon after last change"
);
// Update timestamp if all checks pass
lastThresholdChange[address(safe)] = block.timestamp;
}
/// @notice Validates new owner additions and prevents threshold reduction
function assertOwnerAddition() external view {
ISafe safe = ISafe(ph.getAssertionAdopter());
PhEvm.CallInputs[] memory calls = ph.getCallInputs(
address(safe),
ISafe.addOwnerWithThreshold.selector
);
(address newOwner, uint256 threshold) = abi.decode(calls[0].input, (address, uint256));
uint256 currentThreshold = safe.getThreshold();
// Basic validation
require(newOwner != address(0), "Invalid owner address");
// Check whitelist - assuming whitelist with future allowed owners
require(isOwnerWhitelisted[address(safe)][newOwner], "New owner not whitelisted");
// Never allow threshold reduction
require(threshold >= currentThreshold, "Threshold cannot be lowered");
// Prevent rapid owner changes
uint256 lastChange = lastOwnerChange[address(safe)];
require(
block.timestamp >= lastChange + COOLING_PERIOD,
"Owner addition too soon after last change"
);
// Update timestamp if all checks pass
lastOwnerChange[address(safe)] = block.timestamp;
}
/// @notice Validates owner removals and prevents threshold reduction
function assertOwnerRemoval() external view {
ISafe safe = ISafe(ph.getAssertionAdopter());
PhEvm.CallInputs[] memory calls = ph.getCallInputs(
address(safe),
ISafe.removeOwner.selector
);
(address prevOwner, address ownerToRemove, uint256 threshold) =
abi.decode(calls[0].input, (address, address, uint256));
uint256 currentThreshold = safe.getThreshold();
// Never allow threshold reduction
require(threshold >= currentThreshold, "Threshold cannot be lowered");
// Prevent rapid owner changes
uint256 lastChange = lastOwnerChange[address(safe)];
require(
block.timestamp >= lastChange + COOLING_PERIOD,
"Owner removal too soon after last change"
);
// Update timestamp if all checks pass
lastOwnerChange[address(safe)] = block.timestamp;
}
}