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

feat: bond curve mechanics #43

Merged
merged 21 commits into from
Dec 6, 2023
Merged
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
9 changes: 6 additions & 3 deletions script/DeployBase.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,18 @@ abstract contract DeployBase is Script {
moduleType: "community-staking-module",
locator: address(locator)
});
uint256[] memory curve = new uint256[](2);
curve[0] = 2 ether;
curve[1] = 4 ether;
CSAccounting accounting = new CSAccounting({
commonBondSize: 2 ether,
bondCurve: curve,
admin: deployer,
lidoLocator: address(locator),
communityStakingModule: address(csm),
wstETH: address(wstETH),
// todo: arguable. should be discussed
_blockedBondRetentionPeriod: 8 weeks,
_blockedBondManagementPeriod: 1 weeks
bondLockRetentionPeriod: 8 weeks,
bondLockManagementPeriod: 1 weeks
});

CSFeeOracle oracleImpl = new CSFeeOracle({
Expand Down
363 changes: 141 additions & 222 deletions src/CSAccounting.sol

Large diffs are not rendered by default.

201 changes: 201 additions & 0 deletions src/CSBondCurve.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// SPDX-FileCopyrightText: 2023 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.21;

abstract contract CSBondCurveBase {
event BondCurveChanged(uint256[] bondCurve);
event BondMultiplierChanged(
uint256 indexed nodeOperatorId,
uint256 basisPoints
);
}

abstract contract CSBondCurve is CSBondCurveBase {
/// @dev Array of bond amounts for particular keys count.
///
/// For example:
/// Array Index |> 0 1 2 i
/// Bond Amount |> [ 2 ETH ] [ 3.9 ETH ] [ 5.7 ETH ] [ ... ]
/// Keys Count |> 1 2 3 i + 1
///
/// Bond Amount (ETH)
/// ^
/// |
/// 6 -
/// | ------------------- 5.9 ETH -->..
/// 5.5 - . ^
/// | . |
/// 5 - . |
/// | . |
/// 4.5 - . |
/// | . |
/// 4 - .. |
/// | -------- 3.9 ETH -->.. |
/// 3.5 - .^ |
/// | .. | |
/// 3 - .. | |
/// | . | |
/// 2.5 - . | |
/// | .. | |
/// 2 - -------->.. | |
/// | ^ | |
/// |----------|----------|----------|----------|----> Keys Count
/// | 1 2 3 i
///
uint256[] public bondCurve;

/// @dev This mapping contains bond multiplier points (in basis points) for Node Operator's bond.
/// By default, all Node Operators have x1 multiplier (10000 basis points).
///
/// For example:
/// There is a bond curve as above ^
/// Some Node Operator has x0.90 bond multiplier (9000 basis points)
/// Bond Curve with multiplier for this Node Operator will be:
///
/// Bond Amount (ETH)
/// ^
/// |
/// 4 -
/// | ------------------- 3.6 ETH -->.
/// 3.5 - .. ^
/// | .. |
/// 3 - .. |
/// | -------- 2.7 ETH -->... |
/// 2.5 - .. | |
/// | .. | |
/// 2 - .. | |
/// | 1.8 ETH->... | |
/// 1.5 - ^ | |
/// | | | |
/// 1 - | | |
/// |----------|----------|----------|----------|----> Keys Count
/// | 1 2 3 i
///
mapping(uint256 => uint256) public bondMultiplierBP;

// todo: might be redefined in the future
uint256 internal constant MAX_CURVE_LENGTH = 20;
uint256 internal constant MIN_CURVE_LENGTH = 1;

uint256 internal constant BASIS_POINTS = 10000;
uint256 internal constant MAX_BOND_MULTIPLIER = BASIS_POINTS; // x1
uint256 internal constant MIN_BOND_MULTIPLIER = MAX_BOND_MULTIPLIER / 2; // x0.5

uint256 internal _bondCurveTrend;
skhomuti marked this conversation as resolved.
Show resolved Hide resolved

constructor(uint256[] memory _bondCurve) {
_setBondCurve(_bondCurve);
}

function _setBondCurve(uint256[] memory _bondCurve) internal {
skhomuti marked this conversation as resolved.
Show resolved Hide resolved
if (
_bondCurve.length < MIN_CURVE_LENGTH ||
_bondCurve.length > MAX_CURVE_LENGTH
) revert InvalidBondCurveLength();
// todo: check curve values (not worse than previous and makes sense)
if (_bondCurve[0] == 0) revert InvalidBondCurveValues();
for (uint256 i = 1; i < _bondCurve.length; i++) {
if (_bondCurve[i] <= _bondCurve[i - 1])
revert InvalidBondCurveValues();
}
bondCurve = _bondCurve;
_bondCurveTrend =
_bondCurve[_bondCurve.length - 1] -
// if the curve length is 1, then 0 is used as the previous value to calculate the trend
(_bondCurve.length > 1 ? _bondCurve[_bondCurve.length - 2] : 0);
emit BondCurveChanged(_bondCurve);
}

function _setBondMultiplier(
skhomuti marked this conversation as resolved.
Show resolved Hide resolved
uint256 nodeOperatorId,
uint256 basisPoints
) internal {
if (
basisPoints < MIN_BOND_MULTIPLIER ||
basisPoints > MAX_BOND_MULTIPLIER
) revert InvalidMultiplier();
// todo: check curve values (not worse than previous)
bondMultiplierBP[nodeOperatorId] = basisPoints;
emit BondMultiplierChanged(nodeOperatorId, basisPoints);
}

/// @notice Returns basis points of the bond multiplier for the given node operator.
/// if it isn't set, the multiplier is x1 (MAX_BOND_MULTIPLIER)
function getBondMultiplier(
uint256 nodeOperatorId
) public view returns (uint256) {
uint256 basisPoints = bondMultiplierBP[nodeOperatorId];
return basisPoints > 0 ? basisPoints : MAX_BOND_MULTIPLIER;
}

/// @notice Returns keys count for the given bond amount.
function _getKeysCountByBondAmount(
uint256 amount
) internal view returns (uint256) {
return _getKeysCountByBondAmount(amount, MAX_BOND_MULTIPLIER);
}

/// @notice Returns keys count for the given bond amount for particular node operator.
function _getKeysCountByBondAmount(
uint256 amount,
uint256 multiplier
) internal view returns (uint256) {
if (amount < (bondCurve[0] * multiplier) / BASIS_POINTS) return 0;
uint256 maxCurveAmount = (bondCurve[bondCurve.length - 1] *
multiplier) / BASIS_POINTS;
if (amount >= maxCurveAmount) {
return
bondCurve.length +
((amount - maxCurveAmount) /
((_bondCurveTrend * multiplier) / BASIS_POINTS));
}
return _searchKeysCount(amount, multiplier);
}

function _searchKeysCount(
skhomuti marked this conversation as resolved.
Show resolved Hide resolved
uint256 amount,
uint256 multiplier
) internal view returns (uint256) {
uint256 low;
uint256 high = bondCurve.length - 1;
while (low <= high) {
uint256 mid = (low + high) / 2;
uint256 midAmount = (bondCurve[mid] * multiplier) / BASIS_POINTS;
if (amount == midAmount) {
return mid + 1;
}
if (amount < midAmount) {
// zero mid is avoided above
high = mid - 1;
} else if (amount > midAmount) {
low = mid + 1;
}
}
return low;
}

function _getBondAmountByKeysCount(
uint256 keys
) internal view returns (uint256) {
return _getBondAmountByKeysCount(keys, MAX_BOND_MULTIPLIER);
}

function _getBondAmountByKeysCount(
uint256 keys,
uint256 multiplier
) internal view returns (uint256) {
if (keys == 0) return 0;
madlabman marked this conversation as resolved.
Show resolved Hide resolved
skhomuti marked this conversation as resolved.
Show resolved Hide resolved
if (keys <= bondCurve.length) {
return (bondCurve[keys - 1] * multiplier) / BASIS_POINTS;
}
return
((bondCurve[bondCurve.length - 1] * multiplier) / BASIS_POINTS) +
(keys - bondCurve.length) *
((_bondCurveTrend * multiplier) / BASIS_POINTS);
}

error InvalidBondCurveLength();
error InvalidBondCurveValues();
error InvalidMultiplier();
}
172 changes: 172 additions & 0 deletions src/CSBondLock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// SPDX-FileCopyrightText: 2023 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.21;

abstract contract CSBondLockBase {
event BondLockChanged(
uint256 indexed nodeOperatorId,
uint256 newAmount,
uint256 retentionUntil
);
event BondLockPeriodsChanged(
uint256 retentionPeriod,
uint256 managementPeriod
);

error InvalidBondLockPeriods();
error InvalidBondLockAmount();
}

abstract contract CSBondLock is CSBondLockBase {
struct BondLock {
uint256 amount;
uint256 retentionUntil;
}

// todo: should be reconsidered
uint256 public constant MIN_BOND_LOCK_RETENTION_PERIOD = 4 weeks;
uint256 public constant MAX_BOND_LOCK_RETENTION_PERIOD = 365 days;
uint256 public constant MIN_BOND_LOCK_MANAGEMENT_PERIOD = 1 days;
uint256 public constant MAX_BOND_LOCK_MANAGEMENT_PERIOD = 7 days;

uint256 internal _bondLockRetentionPeriod;
uint256 internal _bondLockManagementPeriod;

mapping(uint256 => BondLock) internal _bondLock;

constructor(uint256 retentionPeriod, uint256 managementPeriod) {
_setBondLockPeriods(retentionPeriod, managementPeriod);
}

function _setBondLockPeriods(
uint256 retention,
uint256 management
) internal {
_validateBondLockPeriods(retention, management);
_bondLockRetentionPeriod = retention;
_bondLockManagementPeriod = management;
skhomuti marked this conversation as resolved.
Show resolved Hide resolved
emit BondLockPeriodsChanged(retention, management);
}

function getBondLockPeriods()
external
view
returns (uint256 retention, uint256 management)
{
return (_bondLockRetentionPeriod, _bondLockManagementPeriod);
}

function _validateBondLockPeriods(
uint256 retention,
uint256 management
) internal pure {
if (
retention < MIN_BOND_LOCK_RETENTION_PERIOD ||
retention > MAX_BOND_LOCK_RETENTION_PERIOD ||
management < MIN_BOND_LOCK_MANAGEMENT_PERIOD ||
management > MAX_BOND_LOCK_MANAGEMENT_PERIOD
) {
revert InvalidBondLockPeriods();
}
}

/// @notice Returns the amount and retention time of locked bond by the given node operator.
function _get(
uint256 nodeOperatorId
) internal view returns (BondLock memory) {
return _bondLock[nodeOperatorId];
}

/// @notice Returns the amount of locked bond by the given node operator.
function _getActualAmount(
uint256 nodeOperatorId
) internal view returns (uint256) {
if (_bondLock[nodeOperatorId].retentionUntil >= block.timestamp) {
return _bondLock[nodeOperatorId].amount;
}
return 0;
}

/// @notice Reports EL rewards stealing for the given node operator.
/// @param nodeOperatorId id of the node operator to lock bond for.
/// @param amount amount to lock.
function _lock(uint256 nodeOperatorId, uint256 amount) internal {
if (amount == 0) {
revert InvalidBondLockAmount();
}
_changeBondLock({
nodeOperatorId: nodeOperatorId,
amount: _bondLock[nodeOperatorId].amount + amount,
retentionUntil: block.timestamp + _bondLockRetentionPeriod
});
}

/// @dev Should be called by the committee. Doesn't settle blocked bond if it is in the safe frame (1 day)
/// @notice Settles blocked bond for the given node operators.
/// @param nodeOperatorIds ids of the node operators to settle blocked bond for.
function _settle(uint256[] memory nodeOperatorIds) internal {
for (uint256 i; i < nodeOperatorIds.length; ++i) {
uint256 nodeOperatorId = nodeOperatorIds[i];
BondLock storage bondLock = _bondLock[nodeOperatorId];
if (
block.timestamp +
_bondLockRetentionPeriod -
bondLock.retentionUntil <
_bondLockManagementPeriod
) {
// blocked bond in safe frame to manage it by committee or node operator
continue;
}
uint256 uncovered;
if (
bondLock.amount > 0 &&
bondLock.retentionUntil >= block.timestamp
) {
uncovered = _penalize(nodeOperatorId, bondLock.amount);
}
_changeBondLock({
nodeOperatorId: nodeOperatorId,
amount: uncovered,
retentionUntil: bondLock.retentionUntil
});
}
}

function _reduceAmount(uint256 nodeOperatorId, uint256 amount) internal {
uint256 blocked = _getActualAmount(nodeOperatorId);
if (amount == 0) {
revert InvalidBondLockAmount();
}
if (blocked < amount) {
revert InvalidBondLockAmount();
}
_changeBondLock(
nodeOperatorId,
_bondLock[nodeOperatorId].amount - amount,
_bondLock[nodeOperatorId].retentionUntil
);
}

function _changeBondLock(
uint256 nodeOperatorId,
uint256 amount,
uint256 retentionUntil
) private {
if (amount == 0) {
delete _bondLock[nodeOperatorId];
emit BondLockChanged(nodeOperatorId, 0, 0);
return;
}
_bondLock[nodeOperatorId] = BondLock({
amount: amount,
retentionUntil: retentionUntil
});
emit BondLockChanged(nodeOperatorId, amount, retentionUntil);
}

function _penalize(
uint256 nodeOperatorId,
uint256 amount
) internal virtual returns (uint256);
}
Loading