-
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
Conversation
kupermind
commented
Jul 14, 2024
- Initial implementation and testing of dynamic IDF and bonding refactor.
// Current OLAS payout | ||
// This value is bound by the initial total supply | ||
uint96 payout; |
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.
Adding as part of dynamic IDF requirement
// Account address | ||
address account; |
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.
Account is not needed as bonds become ERC-721 tokens
/// #if_succeeds {:msg "payout"} old(mapBondProducts[productId].supply) == mapBondProducts[productId].supply + payout; | ||
/// #if_succeeds {:msg "OLAS balances"} IToken(mapBondProducts[productId].token).balanceOf(treasury) == old(IToken(mapBondProducts[productId].token).balanceOf(treasury)) + tokenAmount; | ||
function deposit(uint256 productId, uint256 tokenAmount) external | ||
function deposit(uint256 productId, uint256 tokenAmount, uint256 bondVestingTime) external |
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.
As part of the AIP-1 for dynamic IDF, the bonder can choose the bond vesting time, just not to be below the minimum allowed one.
// 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
weights can all be zero, leaving as is.
contracts/GenericBondCalculator.sol
Outdated
function calculatePayoutOLAS( | ||
uint256 tokenAmount, | ||
uint256 priceLP, | ||
bytes memory data |
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.
I'm not sure that we should use implicit pack of arguments as a raw sequence of bytes (memory data) and then unpack it (abi.decode) instead of explicitly enumerating the arguments.
contracts/Depository.sol
Outdated
payout = IGenericBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP); | ||
payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP, | ||
// Encode parameters required for the IDF calculation | ||
abi.encode(msg.sender, bondVestingTime, productMaxVestingTime, supply, product.payout)); |
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.
abi.encode(msg.sender, bondVestingTime, productMaxVestingTime, supply, product.payout)
This design seems like overengineering to me.
better just
IBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP,
// Encode parameters required for the IDF calculation
msg.sender, bondVestingTime, productMaxVestingTime, supply, product.payout);
In case of any errors, this will make debugging much more difficult.
// Create and mint a new bond | ||
bondId = totalSupply; | ||
// Safe mint is needed since contracts can create bonds as well | ||
_safeMint(msg.sender, bondId); |
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.
audit notes: re-check reentrancy in internal audit
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.
re-check, please, in https://github.com/valory-xyz/autonolas-registries/blob/main/contracts/ServiceRegistryL2.sol:
logic:
serviceId = totalSupply;
serviceId++;
totalSupply = serviceId;
// Mint the service instance to the service owner and record the service structure
_safeMint(serviceOwner, serviceId);
so, id++ then _safeMint(serviceOwner, serviceId);
here:
bondId = totalSupply;
// Safe mint is needed since contracts can create bonds as well
_safeMint(msg.sender, bondId);
totalSupply = bondId + 1;
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.
service Ids indexing must start from zero due to possible mapping conflicts. There is no such thing for bonds.
// Maximum sum of discount factor weights | ||
uint256 public constant MAX_SUM_WEIGHTS = 10_000; |
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.
Sum of weights for k
fractions - using the Vote Weighting terminology let them be from 0 to 100% with a 0.01% step.
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 | ||
// 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; | ||
} |
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.
Corresponds to 2 variables and 4 weights set by the DAO.
/// @param productSupply Current product supply. | ||
/// @param productPayout Current product payout. | ||
/// @return amountOLAS Resulting amount of OLAS tokens. | ||
function calculatePayoutOLAS( |
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.
This is left as is from the GenericBondCalculator
, which is now obsolete.
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.
ok
|
||
// 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; |
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.
This is the only new difference - tokenomics-calculated IDF is changed with the local calculated dynamic IDF according to the proposed approach.
uint256 productMaxVestingTime = product.vesting; | ||
// Calculate vesting limits | ||
if (bondVestingTime < MIN_VESTING) { | ||
revert LowerThan(bondVestingTime, MIN_VESTING); | ||
} | ||
if (bondVestingTime > productMaxVestingTime) { | ||
revert Overflow(bondVestingTime, productMaxVestingTime); | ||
} |
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.
Bond vesting time is now chosen by the user from MIN_VESTING
to max product defined vesting time.
// Note that payout cannot be zero since the price LP is non-zero, otherwise the product would not be created | ||
payout = IGenericBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP); | ||
payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(msg.sender, tokenAmount, product.priceLP, |
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.
Calls new BondCalculator
contract based on proposed dynamic IDF.
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.
What's the dynamic IDF again - how does it differ from previous one?
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.
Dynamic IDF based on 4 parameters described in the doc: number of new units per epoch, vesting time (bound by the product vesting time), bond liquidity coefficient (the more one buys from the beginning of the product, the bigger IDF they get), veOLAS amount of the bonder.
In the previous approach the IDF was a pure tokenomics value based on the number of new units added in the epoch, and new owners providing units during the epoch as well.
// Adjust payout and set supply to zero if supply drops below the min defined value | ||
if (supply < minOLASLeftoverAmount) { | ||
payout += supply; | ||
supply = 0; | ||
} |
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.
If the product supply drops below a specified value, it's remainder is given to the bonder, and the product is instantly closed.
} | ||
|
||
/// @title Bond Depository - Smart contract for OLAS Bond Depository | ||
/// @author AL | ||
/// @author Aleksandr Kuperman - <[email protected]> | ||
contract Depository is IErrorsTokenomics { | ||
contract Depository is ERC721, IErrorsTokenomics { |
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.
Bonds become NFT-s, yay!
// DAO set voting power limit for the bonding account | ||
// This value is bound by the veOLAS total voting power |
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
// 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Added a description in #179
@@ -1,93 +0,0 @@ | |||
// SPDX-License-Identifier: MIT |
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.
Why is all this logic no longer needed?
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.
Generic bond calculator used the old IDF approach, that is no longer the case. The new approach requires a new set of functions.
// Check the epsilonRate value for idf to fit in its size | ||
// 2^64 - 1 < 18.5e18, idf is equal at most 1 + epsilonRate < 18e18, which fits in the variable size | ||
// epsilonRate is the part of the IDF calculation and thus its change will be accounted for in the next epoch | ||
if (_epsilonRate > 0 && _epsilonRate <= 17e18) { | ||
epsilonRate = uint64(_epsilonRate); | ||
} else { | ||
_epsilonRate = epsilonRate; | ||
} |
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.
why was this dropped?
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.
epsilonRate
was used to set the default addition to the discount factor set by the DAO. In a new IDF approach, its value varies from 1 to 2 only (whereas in the old approach it could go up to 17), and thus no defaults are needed.
// Check if the owner has introduced component / agent for the first time | ||
// This is done together with the new unit check, otherwise it could be just a new unit owner | ||
address unitOwner = IToken(registries[unitType]).ownerOf(serviceUnitIds[j]); | ||
if (!mapNewOwners[unitOwner]) { | ||
mapNewOwners[unitOwner] = true; | ||
mapEpochTokenomics[curEpoch].epochPoint.numNewOwners++; | ||
} |
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.
why removed?
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.
This is no longer used anywhere in the calculations.
@@ -1,7 +1,6 @@ | |||
// SPDX-License-Identifier: MIT | |||
pragma solidity ^0.8.25; |
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.
Not sure we should change Tokenomics at all. Feels like we could get away with keeping the existing implementation ...
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.
Dev incentives revision force us to change tokenomics anyway.
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.
The idea of calculating dev incentives in a new way is to calculate top-ups based on donator veOLAS amount. Right now top-ups are calculated based on the overall ETH amount donated throughout the epoch, and used as a factor in calculating top-ups (i.e., 10 ETH donated, 20 OLAS dev inflation - we split 20 OLAS the same way as 10ETH are split, considering the donator had enough veOLAS).
In a proposed approach veOLAS amount is a major variable for top-ups distribution - amount of veOLAS is split between components / agents donated. ETH is no longer in the equation of top-ups.
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.
I see. However, ETH is still being used as part of the donation and distributed to all components/agents. Is it distributed according to the same proportion as the boosts?
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.
The ETH distribution is going to be left as is. Meaning that proportions will be different.
olas = _olas; | ||
tokenomics = _tokenomics; | ||
treasury = _treasury; | ||
bondCalculator = _bondCalculator; | ||
baseURI = _baseURI; |
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.
Doesn't make sense here imo. We can have a purely on-chain NFT. There are these SVG libraries that are pretty cool. We don't want to have to serve custom images here, just unnecessary.
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.
Actually we don't need any link at all. All we need to comply with the standard is that the function tokenURI()
does not revert. So it can be as simple as just returning an NFT id as a string.
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.
It's a bit messy to not offer an image. Will come back
// Note that payout cannot be zero since the price LP is non-zero, otherwise the product would not be created | ||
payout = IGenericBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP); | ||
payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(msg.sender, tokenAmount, product.priceLP, |
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.
What's the dynamic IDF again - how does it differ from previous one?
|
||
/// @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 comment
The 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 comment
The 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 comment
The 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 comment
The 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.
/// @param productSupply Current product supply. | ||
/// @param productPayout Current product payout. | ||
/// @return amountOLAS Resulting amount of OLAS tokens. | ||
function calculatePayoutOLAS( |
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.
ok