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

AIP-1 bonding #170

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open

AIP-1 bonding #170

wants to merge 17 commits into from

Conversation

kupermind
Copy link
Collaborator

  • Initial implementation and testing of dynamic IDF and bonding refactor.

@kupermind kupermind requested review from DavidMinarsch and 77ph July 14, 2024 13:06
Comment on lines +75 to +77
// Current OLAS payout
// This value is bound by the initial total supply
uint96 payout;
Copy link
Collaborator Author

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

Comment on lines -30 to -31
// Account address
address account;
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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];
Copy link
Collaborator

@77ph 77ph Jul 24, 2024

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?

Copy link
Collaborator Author

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.

function calculatePayoutOLAS(
uint256 tokenAmount,
uint256 priceLP,
bytes memory data
Copy link
Collaborator

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.

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));
Copy link
Collaborator

@77ph 77ph Jul 24, 2024

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);
Copy link
Collaborator

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

Copy link
Collaborator

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;

Copy link
Collaborator Author

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.

77ph
77ph previously approved these changes Jul 24, 2024
Comment on lines +71 to +72
// Maximum sum of discount factor weights
uint256 public constant MAX_SUM_WEIGHTS = 10_000;
Copy link
Collaborator Author

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.

Comment on lines +32 to +42
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;
}
Copy link
Collaborator Author

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(
Copy link
Collaborator Author

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.

Copy link
Contributor

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;
Copy link
Collaborator Author

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.

Comment on lines +361 to +368
uint256 productMaxVestingTime = product.vesting;
// Calculate vesting limits
if (bondVestingTime < MIN_VESTING) {
revert LowerThan(bondVestingTime, MIN_VESTING);
}
if (bondVestingTime > productMaxVestingTime) {
revert Overflow(bondVestingTime, productMaxVestingTime);
}
Copy link
Collaborator Author

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,
Copy link
Collaborator Author

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.

Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Comment on lines +391 to +395
// Adjust payout and set supply to zero if supply drops below the min defined value
if (supply < minOLASLeftoverAmount) {
payout += supply;
supply = 0;
}
Copy link
Collaborator Author

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 {
Copy link
Collaborator Author

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!

Comment on lines +33 to +34
// DAO set voting power limit for the bonding account
// This value is bound by the veOLAS total voting power
Copy link
Contributor

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?

Copy link
Collaborator Author

@kupermind kupermind Aug 5, 2024

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New units for what?

Copy link
Collaborator Author

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
Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Comment on lines -667 to -674
// 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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this dropped?

Copy link
Collaborator Author

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.

Comment on lines -965 to -971
// 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++;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why removed?

Copy link
Collaborator Author

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;
Copy link
Contributor

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 ...

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

Copy link
Contributor

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?

Copy link
Collaborator Author

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;
Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Contributor

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,
Copy link
Contributor

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 {
Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants