Skip to content

Commit

Permalink
feat: blocklist
Browse files Browse the repository at this point in the history
  • Loading branch information
0xvv committed Dec 3, 2024
1 parent 4513c1e commit b680e4e
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 26 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,12 @@ 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 when requesting exits and withdrawals.

Admin can also block an address by calling `blockAccount(address, bytes)` on the contract. This will prevent the address from depositing, exiting or withdrawing. If the user is not sanctioned the admin can provide validator public keys that will be exited if they are indeed owned by the blocked user.

If a user was wrongly banned or the sanctions were lifted, the admin can call `unblock(address)` to remove the address from the blocklist.

The view function `isBlockedOrSanctioned(address) returns (bool isBlocked, bool isSanctioned)` can be used to check if an address is blocked or sanctioned, if no sanction oracle is set the isSanctioned bool will always return false.
77 changes: 56 additions & 21 deletions src/contracts/StakingContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -728,23 +729,8 @@ contract StakingContract {
}

function requestValidatorsExit(bytes calldata _publicKeys) external {
if (_publicKeys.length % PUBLIC_KEY_LENGTH != 0) {
revert InvalidPublicKeys();
}
_revertIfSanctioned(msg.sender);
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 (msg.sender != withdrawer) {
revert Unauthorized();
}
_setExitRequest(pubKeyRoot, true);
emit ExitRequest(withdrawer, publicKey);
unchecked {
i += PUBLIC_KEY_LENGTH;
}
}
_revertIfSanctionedOrBlocked(msg.sender);
_requestExits(_publicKeys, msg.sender);
}

/// @notice Utility to stop or allow deposits
Expand All @@ -753,6 +739,32 @@ 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 {
StakingContractStorageLib.getBlocklist().value[_account] = true;
address sanctionsOracle = StakingContractStorageLib.getSanctionsOracle();
if (sanctionsOracle != address(0)) {
if (ISanctionsOracle(sanctionsOracle).isSanctioned(_account)) {
return;
}
}
_requestExits(_publicKeys, _account);
}

function unblock(address _account) external onlyAdmin {
StakingContractStorageLib.getBlocklist().value[_account] = false;
}

function isBlockedOrSanctioned(address _account) public view returns (bool isBlocked, bool isSanctioned) {
address sanctionsOracle = StakingContractStorageLib.getSanctionsOracle();
if (sanctionsOracle != address(0)) {
isSanctioned = ISanctionsOracle(sanctionsOracle).isSanctioned(_account);
}
isBlocked = StakingContractStorageLib.getBlocklist().value[_account];
}

/// ██ ███ ██ ████████ ███████ ██████ ███ ██ █████ ██
/// ██ ████ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██
/// ██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ███████ ██
Expand Down Expand Up @@ -798,6 +810,26 @@ contract StakingContract {
StakingContractStorageLib.getExitRequestMap().value[_publicKeyRoot] = _value;
}

function _requestExits(bytes calldata publicKeys, address owner) internal {
if (publicKeys.length % PUBLIC_KEY_LENGTH != 0) {
revert InvalidPublicKeys();
}

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 (owner != withdrawer) {
revert Unauthorized();
}
_setExitRequest(pubKeyRoot, true);
emit ExitRequest(withdrawer, publicKey);
unchecked {
i += PUBLIC_KEY_LENGTH;
}
}
}

function _updateAvailableValidatorCount(uint256 _operatorIndex) internal {
StakingContractStorageLib.ValidatorsFundingInfo memory validatorFundingInfo = StakingContractStorageLib
.getValidatorsFundingInfo(_operatorIndex);
Expand Down Expand Up @@ -909,7 +941,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();
}
Expand Down Expand Up @@ -953,7 +985,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);
Expand All @@ -970,12 +1002,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);
}
}
}
2 changes: 1 addition & 1 deletion src/contracts/interfaces/ISanctionsOracle.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pragma solidity >=0.8.10;

interface ISanctionsOracle {
function isSanctioned(address account) external returns (bool);
function isSanctioned(address account) external view returns (bool);
}
20 changes: 18 additions & 2 deletions src/contracts/libs/StakingContractStorageLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,8 @@ library StakingContractStorageLib {
===========================================
=========================================*/

bytes32 internal constant SANCTIONS_ORACLE_SLOT = bytes32(uint256(keccak256("StakingContract.sanctionsOracle")) - 1);
bytes32 internal constant SANCTIONS_ORACLE_SLOT =
bytes32(uint256(keccak256("StakingContract.sanctionsOracle")) - 1);

function getSanctionsOracle() internal view returns (address) {
return getAddress(SANCTIONS_ORACLE_SLOT);
Expand All @@ -434,5 +435,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
}
}
}
104 changes: 102 additions & 2 deletions src/test/StakingContract.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2023,15 +2023,15 @@ contract StakingContractOneValidatorTest is Test {
}

contract SanctionsOracle {
mapping(address => bool) sanctionsMap;
mapping(address => bool) sanctionsMap;

function isSanctioned(address user) public returns (bool) {
return sanctionsMap[user];
}

function setSanction(address user, bool status) public {
sanctionsMap[user] = status;
}
}
}

contract StakingContractBehindProxyTest is Test {
Expand Down Expand Up @@ -3416,4 +3416,104 @@ contract StakingContractBehindProxyTest is Test {
vm.prank(bob);
stakingContract.requestValidatorsExit(publicKeys);
}

function test_block__NoDeposit_UserNotSanctioned() public {
vm.prank(admin);
stakingContract.blockAccount(bob, "");

vm.deal(bob, 32 ether);

(bool isBlocked, bool isSanctioned) = stakingContract.isBlockedOrSanctioned(bob);

assertTrue(isBlocked);

vm.expectRevert(abi.encodeWithSignature("AddressBlocked(address)", bob));
vm.prank(bob);
stakingContract.deposit{value: 32 ether}();
}

function test_unblock__NoDeposit_UserNotSanctioned() public {
vm.prank(admin);
stakingContract.blockAccount(bob, "");

(bool isBlocked, bool isSanctioned) = stakingContract.isBlockedOrSanctioned(bob);

assertTrue(isBlocked);

vm.prank(admin);
stakingContract.unblock(bob);

vm.deal(bob, 32 ether);
vm.prank(bob);
stakingContract.deposit{value: 32 ether}();
}

function getPubkeyRoot(bytes memory pubkey) public pure returns (bytes32) {
return sha256(abi.encodePacked(pubkey, bytes16(0)));
}

function test_block_UserDepositOneValidator_NotSanctioned() public {
vm.deal(bob, 32 ether);

vm.prank(bob);
stakingContract.deposit{value: 32 ether}();

bytes
memory publicKey = hex"21d2e725aef3a8f9e09d8f4034948bb7f79505fc7c40e7a7ca15734bad4220a594bf0c6257cef7db88d9fc3fd4360759";

vm.expectEmit(true, true, true, true);
emit ExitRequest(bob, publicKey);
vm.prank(admin);
stakingContract.blockAccount(bob, publicKey);

(bool isBlocked, bool isSanctioned) = stakingContract.isBlockedOrSanctioned(bob);

assertTrue(isBlocked);

assertTrue(stakingContract.getExitRequestedFromRoot(getPubkeyRoot(publicKey)));
}

function test_block_UserDepositOneValidator_Sanctioned() public {
vm.prank(admin);
stakingContract.setSanctionsOracle(address(oracle));

vm.deal(bob, 32 ether);

vm.prank(bob);
stakingContract.deposit{value: 32 ether}();

bytes
memory publicKey = hex"21d2e725aef3a8f9e09d8f4034948bb7f79505fc7c40e7a7ca15734bad4220a594bf0c6257cef7db88d9fc3fd4360759";
oracle.setSanction(bob, true);

vm.prank(admin);
stakingContract.blockAccount(bob, publicKey);

(bool isBlocked, bool isSanctioned) = stakingContract.isBlockedOrSanctioned(bob);

assertTrue(isBlocked);

assertFalse(stakingContract.getExitRequestedFromRoot(getPubkeyRoot(publicKey)));
}

function test_block_UserDepositOneValidator_NotSanctioned_WrongPublicKey() public {
vm.prank(admin);
stakingContract.setSanctionsOracle(address(oracle));

vm.deal(bob, 32 ether);

vm.prank(bob);
stakingContract.deposit{value: 32 ether}();

bytes
memory publicKey = hex"21d2e725aef3a8f9e09d8f4034948bb7f79505fc7c40e7a7ca15734bad4220a594bf0c6257cef7db88d9fc3fd4360759";
bytes
memory wrongPublicKey = hex"ffffe725aef3a8f9e09d8f4034948bb7f79505fc7c40e7a7ca15734bad4220a594bf0c6257cef7db88d9fc3fd4360759";

vm.expectRevert(abi.encodeWithSignature("Unauthorized()"));

vm.prank(admin);
stakingContract.blockAccount(bob, wrongPublicKey);
}
}
// TODO test block does block exits and withdrawals

0 comments on commit b680e4e

Please sign in to comment.