From 4513c1e406c8dffe126bc450dfc02af510187933 Mon Sep 17 00:00:00 2001 From: 0xvv Date: Sat, 9 Nov 2024 11:19:50 +0700 Subject: [PATCH] feat: optional OFAC oracle check --- src/contracts/StakingContract.sol | 22 ++++++ src/contracts/interfaces/ISanctionsOracle.sol | 5 ++ .../libs/StakingContractStorageLib.sol | 16 +++++ src/test/StakingContract.t.sol | 72 +++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 src/contracts/interfaces/ISanctionsOracle.sol diff --git a/src/contracts/StakingContract.sol b/src/contracts/StakingContract.sol index 31b5adc..573945f 100644 --- a/src/contracts/StakingContract.sol +++ b/src/contracts/StakingContract.sol @@ -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 @@ -49,6 +50,7 @@ contract StakingContract { error MaximumOperatorCountAlreadyReached(); error LastEditAfterSnapshot(); error PublicKeyNotInContract(); + error AddressSanctioned(address sanctioned); struct ValidatorAllocationCache { bool used; @@ -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(); @@ -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); @@ -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(); } @@ -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); @@ -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); + } + } + } } diff --git a/src/contracts/interfaces/ISanctionsOracle.sol b/src/contracts/interfaces/ISanctionsOracle.sol new file mode 100644 index 0000000..d28efd4 --- /dev/null +++ b/src/contracts/interfaces/ISanctionsOracle.sol @@ -0,0 +1,5 @@ +pragma solidity >=0.8.10; + +interface ISanctionsOracle { + function isSanctioned(address account) external returns (bool); +} diff --git a/src/contracts/libs/StakingContractStorageLib.sol b/src/contracts/libs/StakingContractStorageLib.sol index 9b7c347..2cc6560 100644 --- a/src/contracts/libs/StakingContractStorageLib.sol +++ b/src/contracts/libs/StakingContractStorageLib.sol @@ -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); + + function getSanctionsOracle() internal view returns (address) { + return getAddress(SANCTIONS_ORACLE_SLOT); + } + + function setSanctionsOracle(address val) internal { + setAddress(SANCTIONS_ORACLE_SLOT, val); + } + + } diff --git a/src/test/StakingContract.t.sol b/src/test/StakingContract.t.sol index 65fa8c0..97b4d74 100644 --- a/src/test/StakingContract.t.sol +++ b/src/test/StakingContract.t.sol @@ -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; @@ -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 { @@ -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); @@ -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); @@ -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"; @@ -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";