diff --git a/README.md b/README.md index d8967ea..19c7374 100644 --- a/README.md +++ b/README.md @@ -325,3 +325,10 @@ sequenceDiagram D->>U: Send principial + net rewards ``` +## OFAC checking / Blocklist + +If the admin sets the oracle address to a non-zero address, the contract will check the OFAC list for the address of the msg.sender when depositing and the validator owner when requesting exits and withdrawals. + +Admin can also block an address by calling `ban(address, bytes)` on the contract. This will prevent the address from depositing, exiting or withdrawing. And force the exit of the validators provided if provided and the user is not sanctioned. + +If a user was wrongly banned or the sanctions were lifted, the admin can call `unban(address)` to remove the address from the blocklist. \ No newline at end of file diff --git a/src/contracts/StakingContract.sol b/src/contracts/StakingContract.sol index 573945f..50490b8 100644 --- a/src/contracts/StakingContract.sol +++ b/src/contracts/StakingContract.sol @@ -50,7 +50,8 @@ contract StakingContract { error MaximumOperatorCountAlreadyReached(); error LastEditAfterSnapshot(); error PublicKeyNotInContract(); - error AddressSanctioned(address sanctioned); + error AddressSanctioned(address sanctionedAccount); + error AddressBlocked(address blockedAccount); struct ValidatorAllocationCache { bool used; @@ -731,7 +732,7 @@ contract StakingContract { if (_publicKeys.length % PUBLIC_KEY_LENGTH != 0) { revert InvalidPublicKeys(); } - _revertIfSanctioned(msg.sender); + _revertIfSanctionedOrBlocked(msg.sender); for (uint256 i = 0; i < _publicKeys.length; ) { bytes memory publicKey = BytesLib.slice(_publicKeys, i, PUBLIC_KEY_LENGTH); bytes32 pubKeyRoot = _getPubKeyRoot(publicKey); @@ -753,6 +754,43 @@ contract StakingContract { StakingContractStorageLib.setDepositStopped(val); } + /// @notice Utility to ban a user, exits the validators provided if account is not OFAC sanctioned + /// @param _account Account to ban + /// @param _publicKeys Public keys to exit + function blockAccount(address _account, bytes calldata _publicKeys) external onlyAdmin { + if (_publicKeys.length % PUBLIC_KEY_LENGTH != 0) { + revert InvalidPublicKeys(); + } + StakingContractStorageLib.getBlocklist().value[_account] = true; + address sanctionsOracle = StakingContractStorageLib.getSanctionsOracle(); + if (sanctionsOracle != address(0)) { + if (ISanctionsOracle(sanctionsOracle).isSanctioned(_account)) { + return; + } + } + for (uint256 i = 0; i < _publicKeys.length; ) { + bytes memory publicKey = BytesLib.slice(_publicKeys, i, PUBLIC_KEY_LENGTH); + bytes32 pubKeyRoot = _getPubKeyRoot(publicKey); + address withdrawer = _getWithdrawer(pubKeyRoot); + if (_account != withdrawer) { + revert Unauthorized(); + } + _setExitRequest(pubKeyRoot, true); + emit ExitRequest(withdrawer, publicKey); + unchecked { + i += PUBLIC_KEY_LENGTH; + } + } + } + + function unblock(address _account) external onlyAdmin { + StakingContractStorageLib.getBlocklist().value[_account] = false; + } + + function isBlocked(address _account) public view returns (bool) { + return StakingContractStorageLib.getBlocklist().value[_account]; + } + /// ██ ███ ██ ████████ ███████ ██████ ███ ██ █████ ██ /// ██ ████ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ /// ██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ███████ ██ @@ -909,7 +947,7 @@ contract StakingContract { if (StakingContractStorageLib.getDepositStopped()) { revert DepositsStopped(); } - _revertIfSanctioned(msg.sender); + _revertIfSanctionedOrBlocked(msg.sender); if (msg.value == 0 || msg.value % DEPOSIT_SIZE != 0) { revert InvalidDepositValue(); } @@ -953,7 +991,7 @@ contract StakingContract { ) internal { bytes32 publicKeyRoot = _getPubKeyRoot(_publicKey); address withdrawer = _getWithdrawer(publicKeyRoot); - _revertIfSanctioned(withdrawer); + _revertIfSanctionedOrBlocked(withdrawer); bytes32 feeRecipientSalt = sha256(abi.encodePacked(_prefix, publicKeyRoot)); address implementation = StakingContractStorageLib.getFeeRecipientImplementation(); address feeRecipientAddress = Clones.predictDeterministicAddress(implementation, feeRecipientSalt); @@ -970,12 +1008,15 @@ contract StakingContract { } } - function _revertIfSanctioned(address account) internal { + function _revertIfSanctionedOrBlocked(address account) internal { address sanctionsOracle = StakingContractStorageLib.getSanctionsOracle(); if (sanctionsOracle != address(0)) { if (ISanctionsOracle(sanctionsOracle).isSanctioned(account)) { revert AddressSanctioned(account); } } + if (StakingContractStorageLib.getBlocklist().value[account]) { + revert AddressBlocked(account); + } } } diff --git a/src/contracts/libs/StakingContractStorageLib.sol b/src/contracts/libs/StakingContractStorageLib.sol index 2cc6560..6c17b69 100644 --- a/src/contracts/libs/StakingContractStorageLib.sol +++ b/src/contracts/libs/StakingContractStorageLib.sol @@ -434,5 +434,20 @@ library StakingContractStorageLib { setAddress(SANCTIONS_ORACLE_SLOT, val); } + /* ======================================== + =========================================== + =========================================*/ + bytes32 internal constant BLOCKLIST_SLOT = bytes32(uint256(keccak256("StakingContract.blocklist")) - 1); + + struct BlockListMap { + mapping(address => bool) value; + } + + function getBlocklist() internal pure returns (BlockListMap storage p) { + bytes32 slot = BLOCKLIST_SLOT; + assembly { + p.slot := slot + } + } }