Skip to content

Commit

Permalink
feat: optional OFAC oracle check
Browse files Browse the repository at this point in the history
  • Loading branch information
0xvv committed Nov 9, 2024
1 parent af56cf2 commit 4513c1e
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 0 deletions.
22 changes: 22 additions & 0 deletions src/contracts/StakingContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "./interfaces/IFeeRecipient.sol";
import "./interfaces/IDepositContract.sol";
import "./libs/StakingContractStorageLib.sol";
import "@openzeppelin/contracts/proxy/Clones.sol";
import "./interfaces/ISanctionsOracle.sol";

/// @title Ethereum Staking Contract
/// @author Kiln
Expand Down Expand Up @@ -49,6 +50,7 @@ contract StakingContract {
error MaximumOperatorCountAlreadyReached();
error LastEditAfterSnapshot();
error PublicKeyNotInContract();
error AddressSanctioned(address sanctioned);

struct ValidatorAllocationCache {
bool used;
Expand Down Expand Up @@ -203,6 +205,13 @@ contract StakingContract {
emit SetWithdrawerCustomizationStatus(_enabled);
}

/// @notice Changes the sanctions oracle address
/// @param _sanctionsOracle New sanctions oracle address
/// @dev If the address is address(0), the sanctions oracle checks are skipped
function setSanctionsOracle(address _sanctionsOracle) external onlyAdmin {
StakingContractStorageLib.setSanctionsOracle(_sanctionsOracle);
}

/// @notice Retrieve system admin
function getAdmin() external view returns (address) {
return StakingContractStorageLib.getAdmin();
Expand Down Expand Up @@ -722,6 +731,7 @@ contract StakingContract {
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);
Expand Down Expand Up @@ -899,6 +909,7 @@ contract StakingContract {
if (StakingContractStorageLib.getDepositStopped()) {
revert DepositsStopped();
}
_revertIfSanctioned(msg.sender);
if (msg.value == 0 || msg.value % DEPOSIT_SIZE != 0) {
revert InvalidDepositValue();
}
Expand Down Expand Up @@ -941,6 +952,8 @@ contract StakingContract {
address _dispatcher
) internal {
bytes32 publicKeyRoot = _getPubKeyRoot(_publicKey);
address withdrawer = _getWithdrawer(publicKeyRoot);
_revertIfSanctioned(withdrawer);
bytes32 feeRecipientSalt = sha256(abi.encodePacked(_prefix, publicKeyRoot));
address implementation = StakingContractStorageLib.getFeeRecipientImplementation();
address feeRecipientAddress = Clones.predictDeterministicAddress(implementation, feeRecipientSalt);
Expand All @@ -956,4 +969,13 @@ contract StakingContract {
revert InvalidZeroAddress();
}
}

function _revertIfSanctioned(address account) internal {
address sanctionsOracle = StakingContractStorageLib.getSanctionsOracle();
if (sanctionsOracle != address(0)) {
if (ISanctionsOracle(sanctionsOracle).isSanctioned(account)) {
revert AddressSanctioned(account);
}
}
}
}
5 changes: 5 additions & 0 deletions src/contracts/interfaces/ISanctionsOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pragma solidity >=0.8.10;

interface ISanctionsOracle {
function isSanctioned(address account) external returns (bool);
}
16 changes: 16 additions & 0 deletions src/contracts/libs/StakingContractStorageLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -419,4 +419,20 @@ library StakingContractStorageLib {
function setLastValidatorEdit(uint256 value) internal {
setUint256(LAST_VALIDATOR_EDIT_SLOT, value);
}

/* ========================================
===========================================
=========================================*/

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

Check failure on line 427 in src/contracts/libs/StakingContractStorageLib.sol

View workflow job for this annotation

GitHub Actions / run-lint

Line length must be no more than 120 but current length is 121

function getSanctionsOracle() internal view returns (address) {
return getAddress(SANCTIONS_ORACLE_SLOT);
}

function setSanctionsOracle(address val) internal {
setAddress(SANCTIONS_ORACLE_SLOT, val);
}


}
72 changes: 72 additions & 0 deletions src/test/StakingContract.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2022,6 +2022,18 @@ contract StakingContractOneValidatorTest is Test {
}
}

contract SanctionsOracle {
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 {
address internal treasury;
StakingContract internal stakingContract;
Expand All @@ -2037,6 +2049,8 @@ contract StakingContractBehindProxyTest is Test {
ConsensusLayerFeeDispatcher internal cld;
FeeRecipient internal feeRecipientImpl;

SanctionsOracle oracle;

event ExitRequest(address caller, bytes pubkey);

function setUp() public {
Expand Down Expand Up @@ -2099,6 +2113,8 @@ contract StakingContractBehindProxyTest is Test {
stakingContract.setOperatorLimit(0, 10, block.number);
vm.stopPrank();
}

oracle = new SanctionsOracle();
}

event Deposit(address indexed caller, address indexed withdrawer, bytes publicKey, bytes signature);
Expand Down Expand Up @@ -2161,6 +2177,31 @@ contract StakingContractBehindProxyTest is Test {
assert(deactivated == false);
}

function test_deposit_withsanctions_senderSanctioned(address user) public {
oracle.setSanction(user, true);

vm.prank(admin);
stakingContract.setSanctionsOracle(address(oracle));

vm.deal(user, 32 ether);

vm.startPrank(user);
vm.expectRevert(abi.encodeWithSignature("AddressSanctioned(address)", user));
stakingContract.deposit{value: 32 ether}();
vm.stopPrank();
}

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

vm.deal(user, 32 ether);

vm.startPrank(user);
stakingContract.deposit{value: 32 ether}();
vm.stopPrank();
}

function testExplicitDepositTwoValidators(uint256 _userSalt) public {
address user = uf._new(_userSalt);
vm.deal(user, 32 * 2 ether);
Expand Down Expand Up @@ -3011,6 +3052,20 @@ contract StakingContractBehindProxyTest is Test {
assertApproxEqAbs(feeRecipientOne.balance, 0.04 ether, 10**5);
}

function test_withdraw_withSanctions_RecipientSanctioned() public {
oracle.setSanction(bob, true);
vm.prank(admin);
stakingContract.setSanctionsOracle(address(oracle));

bytes
memory publicKey = hex"21d2e725aef3a8f9e09d8f4034948bb7f79505fc7c40e7a7ca15734bad4220a594bf0c6257cef7db88d9fc3fd4360759";
vm.deal(bob, 32 ether);
vm.startPrank(bob);
vm.expectRevert(abi.encodeWithSignature("AddressSanctioned(address)", bob));
stakingContract.deposit{value: 32 ether}();
vm.stopPrank();
}

function testWithdrawAllFees_asAdmin() public {
bytes
memory publicKey = hex"21d2e725aef3a8f9e09d8f4034948bb7f79505fc7c40e7a7ca15734bad4220a594bf0c6257cef7db88d9fc3fd4360759";
Expand Down Expand Up @@ -3268,6 +3323,23 @@ contract StakingContractBehindProxyTest is Test {
stakingContract.requestValidatorsExit(publicKey);
}

function test_requestValidatorExits_OracleActive_OwnerSanctioned() public {
vm.prank(admin);
stakingContract.setSanctionsOracle(address(oracle));
bytes
memory publicKey = hex"21d2e725aef3a8f9e09d8f4034948bb7f79505fc7c40e7a7ca15734bad4220a594bf0c6257cef7db88d9fc3fd4360759";
vm.deal(bob, 32 ether);
vm.startPrank(bob);
stakingContract.deposit{value: 32 ether}();
vm.stopPrank();

oracle.setSanction(bob, true);
vm.expectRevert(abi.encodeWithSignature("AddressSanctioned(address)", bob));

vm.prank(bob);
stakingContract.requestValidatorsExit(publicKey);
}

function testRequestValidatorsExits_TwoValidators() public {
bytes
memory publicKey = hex"21d2e725aef3a8f9e09d8f4034948bb7f79505fc7c40e7a7ca15734bad4220a594bf0c6257cef7db88d9fc3fd4360759";
Expand Down

0 comments on commit 4513c1e

Please sign in to comment.