-
Notifications
You must be signed in to change notification settings - Fork 9
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
AIP-1 bonding #170
base: main
Are you sure you want to change the base?
AIP-1 bonding #170
Changes from 7 commits
a0737ee
e092d4e
8280452
dc5a551
2ef7e3a
0c254b1
21bf1d9
002218c
721703f
1493900
f921c5c
543d825
0868302
8311624
dc18a3d
bb35f9a
7035d04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.25; | ||
|
||
import {mulDiv} from "@prb/math/src/Common.sol"; | ||
import {GenericBondCalculator} from "./GenericBondCalculator.sol"; | ||
import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol"; | ||
|
||
interface ITokenomics { | ||
/// @dev Gets number of new units that were donated in the last epoch. | ||
/// @return Number of new units. | ||
function getLastEpochNumNewUnits() external view returns (uint256); | ||
} | ||
|
||
/// @dev Only `owner` has a privilege, but the `sender` was provided. | ||
/// @param sender Sender address. | ||
/// @param owner Required sender address as an owner. | ||
error OwnerOnly(address sender, address owner); | ||
|
||
/// @dev Value overflow. | ||
/// @param provided Overflow value. | ||
/// @param max Maximum possible value. | ||
error Overflow(uint256 provided, uint256 max); | ||
|
||
/// @dev Provided zero address. | ||
error ZeroAddress(); | ||
|
||
/// @dev Provided zero value. | ||
error ZeroValue(); | ||
|
||
// Struct for discount factor params | ||
// The size of the struct is 96 + 64 + 64 = 224 (1 slot) | ||
struct DiscountParams { | ||
// DAO set voting power limit for the bonding account | ||
// This value is bound by the veOLAS total voting power | ||
uint96 targetVotingPower; | ||
// DAO set number of new units per epoch limit | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New units for what? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a description in #179 |
||
// This number is bound by the total number of possible components and agents | ||
uint64 targetNewUnits; | ||
// DAO set weight factors | ||
// The sum of factors cannot exceed the value of 10_000 (100% with a 0.01% step) | ||
uint16[4] weightFactors; | ||
} | ||
Comment on lines
+32
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Corresponds to 2 variables and 4 weights set by the DAO. |
||
|
||
// The size of the struct is 160 + 32 + 160 + 96 = 256 + 192 (2 slots) | ||
struct Product { | ||
// priceLP (reserve0 / totalSupply or reserve1 / totalSupply) with 18 additional decimals | ||
// priceLP = 2 * r0/L * 10^18 = 2*r0*10^18/sqrt(r0*r1) ~= 61 + 96 - sqrt(96 * 112) ~= 53 bits (if LP is balanced) | ||
// or 2* r0/sqrt(r0) * 10^18 => 87 bits + 60 bits = 147 bits (if LP is unbalanced) | ||
uint160 priceLP; | ||
// Supply of remaining OLAS tokens | ||
// After 10 years, the OLAS inflation rate is 2% per year. It would take 220+ years to reach 2^96 - 1 | ||
uint96 supply; | ||
// Token to accept as a payment | ||
address token; | ||
// Current OLAS payout | ||
// This value is bound by the initial total supply | ||
uint96 payout; | ||
// Max bond vesting time | ||
// 2^32 - 1 is enough to count 136 years starting from the year of 1970. This counter is safe until the year of 2106 | ||
uint32 vesting; | ||
} | ||
|
||
/// @title BondCalculator - Smart contract for bond calculation payout in exchange for OLAS tokens based on dynamic IDF. | ||
/// @author Aleksandr Kuperman - <[email protected]> | ||
/// @author Andrey Lebedev - <[email protected]> | ||
/// @author Mariapia Moscatiello - <[email protected]> | ||
contract BondCalculator is GenericBondCalculator { | ||
event OwnerUpdated(address indexed owner); | ||
event DiscountParamsUpdated(DiscountParams newDiscountParams); | ||
|
||
// Maximum sum of discount factor weights | ||
uint256 public constant MAX_SUM_WEIGHTS = 10_000; | ||
Comment on lines
+71
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sum of weights for |
||
// veOLAS contract address | ||
address public immutable ve; | ||
|
||
// Contract owner | ||
address public owner; | ||
// Discount params | ||
DiscountParams public discountParams; | ||
|
||
|
||
/// @dev Bond Calculator constructor. | ||
/// @param _olas OLAS contract address. | ||
/// @param _tokenomics Tokenomics contract address. | ||
/// @param _ve veOLAS contract address. | ||
/// @param _discountParams Discount factor parameters. | ||
constructor(address _olas, address _tokenomics, address _ve, DiscountParams memory _discountParams) | ||
GenericBondCalculator(_olas, _tokenomics) | ||
{ | ||
// Check for zero address | ||
if (_ve == address(0)) { | ||
revert ZeroAddress(); | ||
} | ||
|
||
ve = _ve; | ||
owner = msg.sender; | ||
|
||
// Check for zero values | ||
if (_discountParams.targetNewUnits == 0 || _discountParams.targetVotingPower == 0) { | ||
revert ZeroValue(); | ||
} | ||
// Check the sum of factors that cannot exceed the value of 10_000 (100% with a 0.01% step) | ||
uint256 sumWeights; | ||
for (uint256 i = 0; i < _discountParams.weightFactors.length; ++i) { | ||
sumWeights += _discountParams.weightFactors[i]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. _discountParams.weightFactors[i] >0 and sumWeights > 0 ? or some ki can be zero? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. weights can all be zero, leaving as is. |
||
} | ||
if (sumWeights > MAX_SUM_WEIGHTS) { | ||
revert Overflow(sumWeights, MAX_SUM_WEIGHTS); | ||
} | ||
discountParams = _discountParams; | ||
} | ||
|
||
/// @dev Changes contract owner address. | ||
/// @param newOwner Address of a new owner. | ||
function changeOwner(address newOwner) external { | ||
// Check for the contract ownership | ||
if (msg.sender != owner) { | ||
revert OwnerOnly(msg.sender, owner); | ||
} | ||
|
||
// Check for the zero address | ||
if (newOwner == address(0)) { | ||
revert ZeroAddress(); | ||
} | ||
|
||
owner = newOwner; | ||
emit OwnerUpdated(newOwner); | ||
} | ||
|
||
/// @dev Changed inverse discount factor parameters. | ||
/// @param newDiscountParams Struct of new discount parameters. | ||
function changeDiscountParams(DiscountParams memory newDiscountParams) external { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this is global, not product specific? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, as otherwise this would need to be voted on for every product. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically each product is voted on. So wouldn't it be more useful to be able to set this as part of the product? Just thinking out loud. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Considering how products are launched, those would be pretty much the same. Also, if something happens on a protocol level, my understanding is that we want all the bonds to follow new parameters even for unfinished products, since otherwise the old params could become wrong / unjustified. |
||
// Check for the contract ownership | ||
if (msg.sender != owner) { | ||
revert OwnerOnly(msg.sender, owner); | ||
} | ||
|
||
// Check for zero values | ||
if (newDiscountParams.targetNewUnits == 0 || newDiscountParams.targetVotingPower == 0) { | ||
revert ZeroValue(); | ||
} | ||
// Check the sum of factors that cannot exceed the value of 10_000 (100% with a 0.01% step) | ||
uint256 sumWeights; | ||
for (uint256 i = 0; i < newDiscountParams.weightFactors.length; ++i) { | ||
sumWeights += newDiscountParams.weightFactors[i]; | ||
} | ||
if (sumWeights > MAX_SUM_WEIGHTS) { | ||
revert Overflow(sumWeights, MAX_SUM_WEIGHTS); | ||
} | ||
|
||
discountParams = newDiscountParams; | ||
|
||
emit DiscountParamsUpdated(newDiscountParams); | ||
} | ||
|
||
/// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism accounting for dynamic IDF. | ||
/// @param tokenAmount LP token amount. | ||
/// @param priceLP LP token price. | ||
/// @param data Custom data that is used to calculate the IDF. | ||
/// @return amountOLAS Resulting amount of OLAS tokens. | ||
function calculatePayoutOLAS( | ||
uint256 tokenAmount, | ||
uint256 priceLP, | ||
bytes memory data | ||
) external view override returns (uint256 amountOLAS) { | ||
// The result is divided by additional 1e18, since it was multiplied by in the current LP price calculation | ||
// The resulting amountDF can not overflow by the following calculations: idf = 64 bits; | ||
// priceLP = 2 * r0/L * 10^18 = 2*r0*10^18/sqrt(r0*r1) ~= 61 + 96 - sqrt(96 * 112) ~= 53 bits (if LP is balanced) | ||
// or 2* r0/sqrt(r0) * 10^18 => 87 bits + 60 bits = 147 bits (if LP is unbalanced); | ||
// tokenAmount is of the order of sqrt(r0*r1) ~ 104 bits (if balanced) or sqrt(96) ~ 10 bits (if max unbalanced); | ||
// overall: 64 + 53 + 104 = 221 < 256 - regular case if LP is balanced, and 64 + 147 + 10 = 221 < 256 if unbalanced | ||
// mulDiv will correctly fit the total amount up to the value of max uint256, i.e., max of priceLP and max of tokenAmount, | ||
// however their multiplication can not be bigger than the max of uint192 | ||
uint256 totalTokenValue = mulDiv(priceLP, tokenAmount, 1); | ||
// Check for the cumulative LP tokens value limit | ||
if (totalTokenValue > type(uint192).max) { | ||
revert Overflow(totalTokenValue, type(uint192).max); | ||
} | ||
|
||
// Calculate the dynamic inverse discount factor | ||
uint256 idf = calculateIDF(data); | ||
|
||
// Amount with the discount factor is IDF * priceLP * tokenAmount / 1e36 | ||
// At this point of time IDF is bound by the max of uint64, and totalTokenValue is no bigger than the max of uint192 | ||
amountOLAS = (idf * totalTokenValue) / 1e36; | ||
} | ||
|
||
/// @dev Calculated inverse discount factor based on bonding and account parameters. | ||
/// @param data Custom data that is used to calculate the IDF: | ||
/// - account Account address. | ||
/// - bondVestingTime Bond vesting time. | ||
/// - productMaxVestingTime Product max vesting time. | ||
/// - productSupply Current product supply. | ||
/// - productPayout Current product payout. | ||
/// @return idf Inverse discount factor in 18 decimals format. | ||
function calculateIDF(bytes memory data) public view virtual returns (uint256 idf) { | ||
// Decode the required data | ||
(address account, uint256 bondVestingTime, uint256 productMaxVestingTime, uint256 productSupply, | ||
uint256 productPayout) = abi.decode(data, (address, uint256, uint256, uint256, uint256)); | ||
|
||
// Get the copy of the discount params | ||
DiscountParams memory localParams = discountParams; | ||
uint256 discountBooster; | ||
|
||
// First discount booster: booster = k1 * NumNewUnits(previous epoch) / TargetNewUnits(previous epoch) | ||
// Check the number of new units coming from tokenomics vs the target number of new units | ||
if (localParams.weightFactors[0] > 0) { | ||
uint256 numNewUnits = ITokenomics(tokenomics).getLastEpochNumNewUnits(); | ||
|
||
// If the number of new units exceeds the target, bound by the target number | ||
if (numNewUnits >= localParams.targetNewUnits) { | ||
numNewUnits = localParams.targetNewUnits; | ||
} | ||
discountBooster = (localParams.weightFactors[0] * numNewUnits * 1e18) / localParams.targetNewUnits; | ||
} | ||
|
||
// Second discount booster: booster += k2 * bondVestingTime / productMaxVestingTime | ||
// Add vesting time discount booster | ||
if (localParams.weightFactors[1] > 0) { | ||
discountBooster += (localParams.weightFactors[1] * bondVestingTime * 1e18) / productMaxVestingTime; | ||
} | ||
|
||
// Third discount booster: booster += k3 * (1 - productPayout(at bonding time) / productSupply) | ||
// Add product supply discount booster | ||
if (localParams.weightFactors[2] > 0) { | ||
productSupply = productSupply + productPayout; | ||
discountBooster += localParams.weightFactors[2] * (1e18 - ((productPayout * 1e18) / productSupply)); | ||
} | ||
|
||
// Fourth discount booster: booster += k4 * getVotes(bonding account) / targetVotingPower | ||
// Check the veOLAS balance of a bonding account | ||
if (localParams.weightFactors[3] > 0) { | ||
uint256 vPower = IVotingEscrow(ve).getVotes(account); | ||
|
||
// If the number of new units exceeds the target, bound by the target number | ||
if (vPower >= localParams.targetVotingPower) { | ||
vPower = localParams.targetVotingPower; | ||
} | ||
discountBooster += (localParams.weightFactors[3] * vPower * 1e18) / localParams.targetVotingPower; | ||
} | ||
|
||
// Normalize discount booster by the max sum of weights | ||
discountBooster /= MAX_SUM_WEIGHTS; | ||
|
||
// IDF = 1 + normalized booster | ||
idf = 1e18 + discountBooster; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Description is not very clear. what does the value affect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added description in #179