Skip to content

Commit

Permalink
feat : implementation of CL fee dipsatching
Browse files Browse the repository at this point in the history
  • Loading branch information
0xvv committed Mar 27, 2023
1 parent 2fcb03a commit 8a2a7e0
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 157 deletions.
46 changes: 21 additions & 25 deletions src/contracts/ConsensusLayerFeeDispatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ contract ConsensusLayerFeeDispatcher is IFeeDispatcher {
bytes32 internal constant STAKING_CONTRACT_ADDRESS_SLOT =
keccak256("ConsensusLayerFeeRecipient.stakingContractAddress");
uint256 internal constant BASIS_POINTS = 10_000;
uint256 internal constant SLOT_DURATION_SEC = 12;
bytes32 internal constant VERSION_SLOT = keccak256("ConsensusLayerFeeRecipient.version");

/// @notice Ensures an initialisation call has been called only once per _version value
Expand All @@ -57,49 +58,44 @@ contract ConsensusLayerFeeDispatcher is IFeeDispatcher {
}

/// @notice Performs a withdrawal on this contract's balance
function dispatch(bytes32) external payable {
revert NotImplemented();
/*
uint256 balance = address(this).balance; // this has taken into account msg.value
if (balance == 0) {
revert ZeroBalanceWithdrawal();
}
function dispatch(bytes32 _publicKeyRoot) external payable {
IStakingContractFeeDetails stakingContract = IStakingContractFeeDetails(
STAKING_CONTRACT_ADDRESS_SLOT.getAddress()
);
address withdrawer = stakingContract.getWithdrawerFromPublicKeyRoot(_publicKeyRoot);
address operator = stakingContract.getOperatorFeeRecipient(_publicKeyRoot);
address treasury = stakingContract.getTreasury();
uint256 globalFee;
if (balance >= 32 ether) {
// withdrawing a healthy & exited validator
globalFee = ((balance - 32 ether) * stakingContract.getGlobalFee()) / BASIS_POINTS;
} else if (balance <= 16 ether) {
// withdrawing from what looks like skimming
globalFee = (balance * stakingContract.getGlobalFee()) / BASIS_POINTS;

uint256 balance = address(this).balance; // this has taken into account msg.value
if (balance == 0) {
revert ZeroBalanceWithdrawal();
}

uint256 lastWithdrawal = stakingContract.getLastWithdrawFromPublicKeyRoot(_publicKeyRoot);
uint256 maxClSinceWithdrawal = ((block.timestamp - lastWithdrawal) / SLOT_DURATION_SEC) * stakingContract.getMaxClPerBlock();

uint256 operatorFee = (globalFee * stakingContract.getOperatorFee()) / BASIS_POINTS;
uint256 nonExemptBalance = maxClSinceWithdrawal < balance ? maxClSinceWithdrawal : balance;

uint256 globalFee = (nonExemptBalance * stakingContract.getGlobalFee()) / BASIS_POINTS;

uint256 operatorFee = (globalFee * stakingContract.getOperatorFee()) / BASIS_POINTS;

address withdrawer = stakingContract.getWithdrawerFromPublicKeyRoot(_publicKeyRoot);
(bool status, bytes memory data) = withdrawer.call{value: balance - globalFee}("");
if (status == false) {
revert WithdrawerReceiveError(data);
}
address operator = stakingContract.getOperatorFeeRecipient(_publicKeyRoot);
if (globalFee > 0) {
address treasury = stakingContract.getTreasury();
(status, data) = treasury.call{value: globalFee - operatorFee}("");
if (status == false) {
revert FeeRecipientReceiveError(data);
revert TreasuryReceiveError(data);
}
}
if (operatorFee > 0) {

(status, data) = operator.call{value: operatorFee}("");
if (status == false) {
revert TreasuryReceiveError(data);
revert FeeRecipientReceiveError(data);
}
}
emit Withdrawal(withdrawer, operator, balance - globalFee, operatorFee, globalFee - operatorFee);
*/
emit Withdrawal(withdrawer, operator, _publicKeyRoot, balance - globalFee, operatorFee, globalFee - operatorFee);
}

/// @notice Retrieve the staking contract address
Expand Down
38 changes: 38 additions & 0 deletions src/contracts/StakingContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,23 @@ contract StakingContract {
return _getWithdrawer(_publicKeyRoot);
}

/// @notice Retrieve last withdrawal timestamp of public key
/// @param _publicKey Public Key to check
function getLastWithdraw(bytes calldata _publicKey) external view returns (uint256) {
return _getLastWithdrawal(_getPubKeyRoot(_publicKey));
}

/// @notice Retrieve last withdrawal timestamp of public key root
/// @param _publicKeyRoot Hash of the public key
function getLastWithdrawFromPublicKeyRoot(bytes32 _publicKeyRoot) external view returns (uint256) {
return _getLastWithdrawal(_publicKeyRoot);
}

/// @notice Retrieve the max CL rewards per block for fee computing
function getMaxClPerBlock() external view returns (uint256) {
return StakingContractStorageLib.getMaxClPerBlock();
}

/// @notice Retrieve operator details
/// @param _operatorIndex Operator index
function getOperator(uint256 _operatorIndex)
Expand Down Expand Up @@ -370,6 +387,13 @@ contract StakingContract {
withdrawers.value[pubkeyRoot] = _newWithdrawer;
}

/// @notice Set max CL reward per block for fee computing
/// @dev Only callable by the admin
/// @param _newMaxClPerBlock New max CL reward per block address
function setMaxCLPerBlock(uint256 _newMaxClPerBlock) external onlyAdmin {
StakingContractStorageLib.setMaxClPerBlock(_newMaxClPerBlock);
}

/// @notice Set operator staking limits
/// @dev Only callable by admin
/// @dev Limit should not exceed the validator key count of the operator
Expand Down Expand Up @@ -558,6 +582,7 @@ contract StakingContract {
for (uint256 i = 0; i < _publicKeys.length; ) {
bytes memory publicKey = BytesLib.slice(_publicKeys, i, PUBLIC_KEY_LENGTH);
_deployAndWithdraw(publicKey, EXECUTION_LAYER_SALT_PREFIX, StakingContractStorageLib.getELDispatcher());
_setLastWithdrawal(_getPubKeyRoot(publicKey), block.timestamp);
unchecked {
i += PUBLIC_KEY_LENGTH;
}
Expand All @@ -575,6 +600,7 @@ contract StakingContract {
for (uint256 i = 0; i < _publicKeys.length; ) {
bytes memory publicKey = BytesLib.slice(_publicKeys, i, PUBLIC_KEY_LENGTH);
_deployAndWithdraw(publicKey, CONSENSUS_LAYER_SALT_PREFIX, StakingContractStorageLib.getCLDispatcher());
_setLastWithdrawal(_getPubKeyRoot(publicKey), block.timestamp);
unchecked {
i += PUBLIC_KEY_LENGTH;
}
Expand All @@ -593,6 +619,7 @@ contract StakingContract {
bytes memory publicKey = BytesLib.slice(_publicKeys, i, PUBLIC_KEY_LENGTH);
_deployAndWithdraw(publicKey, EXECUTION_LAYER_SALT_PREFIX, StakingContractStorageLib.getELDispatcher());
_deployAndWithdraw(publicKey, CONSENSUS_LAYER_SALT_PREFIX, StakingContractStorageLib.getCLDispatcher());
_setLastWithdrawal(_getPubKeyRoot(publicKey), block.timestamp);
unchecked {
i += PUBLIC_KEY_LENGTH;
}
Expand All @@ -613,6 +640,7 @@ contract StakingContract {
/// @param _publicKey Validator to withdraw Consensus Layer Fees from
function withdrawCLFee(bytes calldata _publicKey) external {
_deployAndWithdraw(_publicKey, CONSENSUS_LAYER_SALT_PREFIX, StakingContractStorageLib.getCLDispatcher());
_setLastWithdrawal(_getPubKeyRoot(_publicKey), block.timestamp);
}

/// @notice Withdraw both Consensus and Execution Layer Fee for a given validator public key
Expand All @@ -621,6 +649,7 @@ contract StakingContract {
function withdraw(bytes calldata _publicKey) external {
_deployAndWithdraw(_publicKey, EXECUTION_LAYER_SALT_PREFIX, StakingContractStorageLib.getELDispatcher());
_deployAndWithdraw(_publicKey, CONSENSUS_LAYER_SALT_PREFIX, StakingContractStorageLib.getCLDispatcher());
_setLastWithdrawal(_getPubKeyRoot(_publicKey), block.timestamp);
}

function requestValidatorsExit(bytes calldata _publicKeys) external {
Expand Down Expand Up @@ -669,6 +698,14 @@ contract StakingContract {
return StakingContractStorageLib.getWithdrawers().value[_publicKeyRoot];
}

function _getLastWithdrawal(bytes32 _publicKeyRoot) internal view returns (uint256) {
return StakingContractStorageLib.getLastWithdraw().value[_publicKeyRoot];
}

function _setLastWithdrawal(bytes32 _publicKeyRoot, uint256 _timestamp) internal {
StakingContractStorageLib.getLastWithdraw().value[_publicKeyRoot] = _timestamp;
}

function _updateAvailableValidatorCount(uint256 _operatorIndex) internal {
StakingContractStorageLib.ValidatorsFundingInfo memory validatorFundingInfo = StakingContractStorageLib
.getValidatorsFundingInfo(_operatorIndex);
Expand Down Expand Up @@ -720,6 +757,7 @@ contract StakingContract {
_depositValidator(publicKey, signature, withdrawalCredentials);
bytes32 pubkeyRoot = _getPubKeyRoot(publicKey);
StakingContractStorageLib.getWithdrawers().value[pubkeyRoot] = _withdrawer;
_setLastWithdrawal(pubkeyRoot, block.timestamp);
emit Deposit(msg.sender, _withdrawer, publicKey, signature);
unchecked {
++i;
Expand Down
4 changes: 4 additions & 0 deletions src/contracts/interfaces/IStakingContractFeeDetails.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ pragma solidity >=0.8.10;
interface IStakingContractFeeDetails {
function getWithdrawerFromPublicKeyRoot(bytes32 _publicKeyRoot) external view returns (address);

function getLastWithdrawFromPublicKeyRoot(bytes32 _publicKeyRoot) external view returns (uint256);

function getTreasury() external view returns (address);

function getOperatorFeeRecipient(bytes32 pubKeyRoot) external view returns (address);

function getGlobalFee() external view returns (uint256);

function getOperatorFee() external view returns (uint256);

function getMaxClPerBlock() external view returns (uint256);
}
31 changes: 31 additions & 0 deletions src/contracts/libs/StakingContractStorageLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -317,4 +317,35 @@ library StakingContractStorageLib {
function setWithdrawerCustomizationEnabled(bool _enabled) internal {
setBool(WITHDRAWER_CUSTOMIZATION_ENABLED_SLOT, _enabled);
}

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

bytes32 internal constant LAST_WITHDRAW_MAPPING_SLOT = keccak256("StakingContract.lastWithdraw");

struct LastWithdrawSlot {
mapping(bytes32 => uint256) value;
}

function getLastWithdraw() internal pure returns (LastWithdrawSlot storage p) {
bytes32 slot = LAST_WITHDRAW_MAPPING_SLOT;
assembly {
p.slot := slot
}
}

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

bytes32 internal constant MAX_CL_PER_BLOCK_SLOT = keccak256("StakingContract.maxClPerBlock");

function getMaxClPerBlock() internal view returns (uint256) {
return getUint256(MAX_CL_PER_BLOCK_SLOT);
}

function setMaxClPerBlock(uint256 _newMaxClPerBlock) internal {
setUint256(MAX_CL_PER_BLOCK_SLOT, _newMaxClPerBlock);
}
}
92 changes: 47 additions & 45 deletions src/test/ConsensusLayerFeeDispatcher.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
pragma solidity >=0.8.10;

import "forge-std/Vm.sol";
import "forge-std/Test.sol";

import "../contracts/ConsensusLayerFeeDispatcher.sol";
import "../contracts/libs/BytesLib.sol";
import "../contracts/ConsensusLayerFeeDispatcher.sol";
Expand Down Expand Up @@ -34,9 +36,17 @@ contract StakingContractMock {
function getOperatorFeeRecipient(bytes32) external pure returns (address) {
return operator;
}

function getMaxClPerBlock() external pure returns (uint256) {
return 608411286029; //Based on a 5 % APY and 12 second slot duration
}

function getLastWithdrawFromPublicKeyRoot(bytes32) external pure returns (uint256) {
return 1; // initial timestamp in unit test
}
}

contract ConsensusLayerFeeDispatcherTest {
contract ConsensusLayerFeeDispatcherTest is Test {
event Withdrawal(
address indexed withdrawer,
address indexed feeRecipient,
Expand All @@ -46,7 +56,8 @@ contract ConsensusLayerFeeDispatcherTest {
uint256 treasuryFee
);

Vm internal vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
uint256 internal immutable ONE_ETH_REWARD_TIME = (10 * 2629800 / 16) * 12;

IStakingContractFeeDetails internal stakingContract;
ConsensusLayerFeeDispatcher internal cld;
address internal constant bob = address(1);
Expand Down Expand Up @@ -104,95 +115,86 @@ contract ConsensusLayerFeeDispatcherTest {
require(status == false);
vm.stopPrank();
}

/*

function testWithdrawCLFeesExitedValidator() external {
vm.warp(block.timestamp + ONE_ETH_REWARD_TIME );
vm.deal(address(this), 33 ether);
assert(bob.balance == 0);
assert(treasury.balance == 0);
assert(operator.balance == 0);
vm.expectEmit(true, true, true, true);
emit Withdrawal(bob, operator, 32.9 ether, 0.02 ether, 0.08 ether);
vm.expectEmit(true, true, true, false); // Exact ammounts not checked in the event
emit Withdrawal(bob, operator, bytes32(0), 32.9 ether, 0.02 ether, 0.08 ether);
cld.dispatch{value: 33 ether}(bytes32(0));
assert(bob.balance == 32.9 ether);
assert(treasury.balance == 0.08 ether);
assert(operator.balance == 0.02 ether);
}
*/

assertApproxEqAbs(bob.balance, 32.9 ether, 10 ** 6);
assertApproxEqAbs(treasury.balance, 0.08 ether, 10 ** 5);
assertApproxEqAbs(operator.balance, 0.02 ether, 10 ** 5);

/*
}

function testWithdrawCLFeesSkimmedValidator() external {
vm.warp(block.timestamp + ONE_ETH_REWARD_TIME * 2 ); //to avoid rounding errors in the event
vm.deal(address(this), 1 ether);
assert(bob.balance == 0);
assert(treasury.balance == 0);
assert(operator.balance == 0);
vm.expectEmit(true, true, true, true);
emit Withdrawal(bob, operator, 0.9 ether, 0.02 ether, 0.08 ether);
emit Withdrawal(bob, operator, bytes32(0), 0.9 ether, 0.02 ether, 0.08 ether);
cld.dispatch{value: 1 ether}(bytes32(0));
assert(bob.balance == 0.9 ether);
assert(treasury.balance == 0.08 ether);
assert(operator.balance == 0.02 ether);
}
*/

/*

function testWithdrawCLFeesSlashedValidator() external {
vm.warp(block.timestamp + ONE_ETH_REWARD_TIME );
vm.deal(address(this), 31.95 ether);
assert(bob.balance == 0);
assert(operator.balance == 0);
vm.expectEmit(true, true, true, true);
emit Withdrawal(bob, operator, 31.95 ether, 0 ether, 0);
vm.expectEmit(true, true, true, false); // ETH values in the event aren't checked
emit Withdrawal(bob, operator, bytes32(0), 0 ether, 0 ether, 0 ether);
cld.dispatch{value: 31.95 ether}(bytes32(0));
assert(bob.balance == 31.95 ether);
assert(operator.balance == 0 ether);
assert(treasury.balance == 0 ether);

assertApproxEqAbs(bob.balance, 31.85 ether, 10 ** 6);
assertApproxEqAbs(treasury.balance, 0.08 ether, 10 ** 5);
assertApproxEqAbs(operator.balance, 0.02 ether, 10 ** 5);
}
*/
/*

function testWithdrawCLFeesTwice() external {
vm.warp(block.timestamp + ONE_ETH_REWARD_TIME * 2 ); //to avoid rounding errors in the event
vm.deal(address(this), 1 ether);
assert(bob.balance == 0);
assert(treasury.balance == 0);
assert(operator.balance == 0);
vm.expectEmit(true, true, true, true);
emit Withdrawal(bob, operator, 0.9 ether, 0.02 ether, 0.08 ether);
emit Withdrawal(bob, operator, bytes32(0), 0.9 ether, 0.02 ether, 0.08 ether);
cld.dispatch{value: 1 ether}(bytes32(0));
assert(bob.balance == 0.9 ether);
assert(treasury.balance == 0.08 ether);
assert(operator.balance == 0.02 ether);
assertApproxEqAbs(bob.balance, 0.9 ether, 10 ** 6);
assertApproxEqAbs(treasury.balance, 0.08 ether, 10 ** 6);
assertApproxEqAbs(operator.balance, 0.02 ether, 10 ** 6);
vm.warp(block.timestamp + ONE_ETH_REWARD_TIME);
vm.deal(address(this), 1 ether);
vm.expectEmit(true, true, true, true);
emit Withdrawal(bob, operator, 0.9 ether, 0.02 ether, 0.08 ether);
emit Withdrawal(bob, operator, bytes32(0), 0.9 ether, 0.02 ether, 0.08 ether);
cld.dispatch{value: 1 ether}(bytes32(0));
assert(bob.balance == 1.8 ether);
assert(treasury.balance == 0.16 ether);
assert(operator.balance == 0.04 ether);
assertApproxEqAbs(bob.balance, 1.80 ether, 10 ** 6);
assertApproxEqAbs(treasury.balance, 0.16 ether, 10 ** 6);
assertApproxEqAbs(operator.balance, 0.04 ether, 10 ** 6);
}
*/

/*

function testWithdrawCLFeesAnotherPublicKey() external {
vm.warp(block.timestamp + ONE_ETH_REWARD_TIME * 2 ); //to avoid rounding errors in the event
vm.deal(address(this), 1 ether);
assert(bob.balance == 0);
assert(operator.balance == 0);
assert(treasury.balance == 0);
assert(address(0).balance == 0);
vm.expectEmit(true, true, true, true);
emit Withdrawal(address(0), operator, 0.9 ether, 0.02 ether, 0.08 ether);
emit Withdrawal(address(0), operator, keccak256(bytes("another public key")), 0.9 ether, 0.02 ether, 0.08 ether);
cld.dispatch{value: 1 ether}(keccak256(bytes("another public key")));
assert(bob.balance == 0);
assert(operator.balance == 0.02 ether);
assert(treasury.balance == 0.08 ether);
assert(address(0).balance == 0.9 ether);
}
*/

function testRevertNotImplemented() external {
vm.deal(address(this), 1 ether);
assert(bob.balance == 0);
assert(treasury.balance == 0);
assert(operator.balance == 0);
vm.expectRevert(abi.encodeWithSignature("NotImplemented()"));
cld.dispatch{value: 1 ether}(bytes32(0));
}
}
Loading

0 comments on commit 8a2a7e0

Please sign in to comment.