Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compliance features #101

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,63 @@
# Staking Contracts changelog


## [latest](https://github.com/kilnfi/staking-contracts)

- [feat: add optional OFAC sanctions check](https://github.com/kilnfi/staking-contracts/pull/101/commits/4513c1e406c8dffe126bc450dfc02af510187933)
- [feat: blockList](https://github.com/kilnfi/staking-contracts/pull/101/commits/b680e4e017dfb57a4c97425f1ee85c118fd95e53)
- [refacto: refresh tests]()

## [1.2.0](https://github.com/kilnfi/staking-contracts/releases/tag/1.2.0)

This entry was created retroactively and is not exhaustive, the git history is fairly detailed and can be used to track minor changes not logged here (tests changes, gas opti, minor fixes). The history is best viewed on github to see the matching issues.

- [feat: implementation of batch withdrawal functions](https://github.com/kilnfi/staking-contracts/commit/eaaff6975dccb641b93e049f072c957a99854754)
- [feat: implementation of CL fee dispatching](https://github.com/kilnfi/staking-contracts/commit/8a2a7e0b61874b71e7d036e16425ec4e9bcf3835)
- [feat: requestValidatorExit()](https://github.com/kilnfi/staking-contracts/commit/757f17d8e187031332a2357427cfdc0a6de7717e)
- [feat: new CL dispatch logic](https://github.com/kilnfi/staking-contracts/commit/60387680768fd0c9da24ab097dd7953a2b8df19d)
- [feat: slashing logic removed](https://github.com/kilnfi/staking-contracts/commit/196a1bbb1b720b1134253890e3f7010c3f3143ee)
- [feat: immutable commission limits](https://github.com/kilnfi/staking-contracts/commit/ea9f10d58b131ee40560364137a046272fe6a62a)
- [fix: split initialization](https://github.com/kilnfi/staking-contracts/commit/3aa65764f2d868d41b52aceac092cdb43a59d7a9)
- [feat: stop deposits flag](https://github.com/kilnfi/staking-contracts/commit/aad23d5b1bec1ff6c8229f0b197a4575a800614c)
- [feat: restrict withdrawal function](https://github.com/kilnfi/staking-contracts/commit/ed1b36be629b13ac4b1f417eb0da084067ef803a)
- [feat: optional AuthorizedFeeRecipient](https://github.com/kilnfi/staking-contracts/commit/35acce30b033314906ec98395c53f4fb2844b61e)

### Audit fixes
- [remove multi operator logic](https://github.com/kilnfi/staking-contracts/commit/e5c91d8a08a5fd64bddb6b5a9e09f467e0b3bbc0)
- [remove Treasury contract](https://github.com/kilnfi/staking-contracts/commit/8306951add826c11f5decc427cb0ea6d6cd889ba)
- [reset operator index when removing validators](https://github.com/kilnfi/staking-contracts/commit/8def7d680a95f66137f16ddb53ca669bf099ab04)
- [implement snapshot mechanism for stored keys](https://github.com/kilnfi/staking-contracts/commit/dc6f050b3bf1f234e89d321037a9e353127dae8a)

### Deployments

- [Ledger Komainu mainnet deployment (now Kiln dApp)](https://github.com/kilnfi/staking-contracts/commit/a74d0810a2c97b2eaaa7763bd347ed30eed2b7e2)

- [Goerli deployment](https://github.com/kilnfi/staking-contracts/commit/fb1be197899b28b3ba72a2f3af752666b5125e81)

- [Updated implementations](https://github.com/kilnfi/staking-contracts/commit/f33eb8dc37fab40217dbe1e69853ca3fcd884a2d)

- [Holesky devenet & testnet](https://github.com/kilnfi/staking-contracts/commit/6df02b9c7d003504f1b57b7ef6d639ce963943dc)

- [Consensys immutable deployment](https://github.com/kilnfi/staking-contracts/commit/53f2d9b0d0662d1f5d44fab7f04684cca56df2fb)

- [Safe promotional deployment + testnet](https://github.com/kilnfi/staking-contracts/commit/af56cf295664d61ab0e23e45d5eabf780e4e59ab)

- [Safe second deployment](https://github.com/kilnfi/staking-contracts/commit/bb8e64d583ce31b03d7f5ff613931c7819621ddb)

## v0.2.2 (September 13th 2022)

### :dizzy: Features

- [feat: add missing events](https://github.com/kilnfi/staking-contracts/pull/61)

### Deployments

- [Enzyme mainnet deployment](https://github.com/kilnfi/staking-contracts/commit/42761e7837498c27798bd15e7d0886f3dea7180b)

- [Ledger Live mainnet deployment](https://github.com/kilnfi/staking-contracts/commit/cd680d350bfe4edacadccf01b6dd1484cd8a49b0)

- [Ledger Vault mainnet deployment](https://github.com/kilnfi/staking-contracts/commit/dd41162155a5e944731d544229f2763d1a99eb9e)

## v0.2.1 (August 26th 2022)

### :dizzy: Features
Expand Down
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. 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. Blocked users can still request exit and withdraw their funds unless they are also sanctioned.

If a user was wrongly banned or the ban is 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.
2 changes: 1 addition & 1 deletion lib/forge-std
107 changes: 91 additions & 16 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,8 @@ contract StakingContract {
error MaximumOperatorCountAlreadyReached();
error LastEditAfterSnapshot();
error PublicKeyNotInContract();
error AddressSanctioned(address sanctionedAccount);
error AddressBlocked(address blockedAccount);

struct ValidatorAllocationCache {
bool used;
Expand Down Expand Up @@ -203,6 +206,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 @@ -719,22 +729,8 @@ contract StakingContract {
}

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

/// @notice Utility to stop or allow deposits
Expand All @@ -743,6 +739,39 @@ contract StakingContract {
StakingContractStorageLib.setDepositStopped(val);
}

/// @notice Utility to ban a user, exits the validators provided if account is not OFAC sanctioned
/// @notice Blocks the account from depositing, the account is still alowed to exit & withdraw if not 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);
}

/// @notice Utility to unban a user
/// @param _account Account to unban
function unblock(address _account) external onlyAdmin {
StakingContractStorageLib.getBlocklist().value[_account] = false;
}

/// @notice Utility to check if an account is blocked or sanctioned
/// @param _account Account to check
/// @return isBlocked True if the account is blocked
/// @return isSanctioned True if the account is sanctioned, always false if not sanctions oracle is set
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 @@ -788,6 +817,29 @@ contract StakingContract {
StakingContractStorageLib.getExitRequestMap().value[_publicKeyRoot] = _value;
}

/// @notice Function to emit the ExitRequest event for each public key
/// @param publicKeys Concatenated public keys
/// @param owner Address of the expected owner of the public keys
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 @@ -899,6 +951,7 @@ contract StakingContract {
if (StakingContractStorageLib.getDepositStopped()) {
revert DepositsStopped();
}
_revertIfSanctionedOrBlocked(msg.sender);
if (msg.value == 0 || msg.value % DEPOSIT_SIZE != 0) {
revert InvalidDepositValue();
}
Expand Down Expand Up @@ -941,6 +994,7 @@ contract StakingContract {
address _dispatcher
) internal {
bytes32 publicKeyRoot = _getPubKeyRoot(_publicKey);
_revertIfSanctioned(msg.sender);
bytes32 feeRecipientSalt = sha256(abi.encodePacked(_prefix, publicKeyRoot));
address implementation = StakingContractStorageLib.getFeeRecipientImplementation();
address feeRecipientAddress = Clones.predictDeterministicAddress(implementation, feeRecipientSalt);
Expand All @@ -956,4 +1010,25 @@ contract StakingContract {
revert InvalidZeroAddress();
}
}

function _revertIfSanctionedOrBlocked(address account) internal view {
address sanctionsOracle = StakingContractStorageLib.getSanctionsOracle();
if (sanctionsOracle != address(0)) {
if (ISanctionsOracle(sanctionsOracle).isSanctioned(account)) {
revert AddressSanctioned(account);
}
}
if (StakingContractStorageLib.getBlocklist().value[account]) {
revert AddressBlocked(account);
}
}

function _revertIfSanctioned(address account) internal view {
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 view returns (bool);
}
32 changes: 32 additions & 0 deletions src/contracts/libs/StakingContractStorageLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -419,4 +419,36 @@ 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);
}

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

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
}
}
}
Loading
Loading