Skip to content

Commit

Permalink
refactor and test: making bond calculator inherit the generic one, co…
Browse files Browse the repository at this point in the history
…rrecting tests
  • Loading branch information
kupermind committed Jul 12, 2024
1 parent 0c254b1 commit 21bf1d9
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 126 deletions.
97 changes: 50 additions & 47 deletions contracts/BondCalculator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.25;

Check warning on line 2 in contracts/BondCalculator.sol

View workflow job for this annotation

GitHub Actions / build

Found more than One contract per file. 2 contracts found!

import {mulDiv} from "@prb/math/src/Common.sol";
import {GenericBondCalculator} from "./GenericBondCalculator.sol";
import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol";

interface ITokenomics {
Expand All @@ -23,6 +24,9 @@ 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 {
Expand Down Expand Up @@ -56,43 +60,45 @@ struct Product {
uint32 vesting;
}

/// @title GenericBondSwap - Smart contract for generic bond calculation mechanisms in exchange for OLAS tokens.
/// @dev The bond calculation mechanism is based on the UniswapV2Pair contract.
/// @author AL
/// @title BondCalculator - Smart contract for bond calculation payout in exchange for OLAS tokens based on dynamic IDF.
/// @author Aleksandr Kuperman - <[email protected]>
contract GenericBondCalculator {
/// @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;
// OLAS contract address
address public immutable olas;
// veOLAS contract address
address public immutable ve;

Check warning on line 74 in contracts/BondCalculator.sol

View workflow job for this annotation

GitHub Actions / build

Immutable variables name are set to be in capitalized SNAKE_CASE
// Tokenomics contract address
address public immutable tokenomics;

// Contract owner
address public owner;
// Discount params
DiscountParams public discountParams;


/// @dev Generic Bond Calcolator constructor
/// @dev Bond Calculator constructor.
/// @param _olas OLAS contract address.
/// @param _tokenomics Tokenomics contract address.
constructor(address _olas, address _ve, address _tokenomics, DiscountParams memory _discountParams) {
// Check for at least one zero contract address
if (_olas == address(0) || _ve == address(0) || _tokenomics == address(0)) {
/// @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();
}

olas = _olas;
ve = _ve;
tokenomics = _tokenomics;
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) {
Expand Down Expand Up @@ -129,6 +135,10 @@ contract GenericBondCalculator {
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) {
Expand All @@ -143,26 +153,16 @@ contract GenericBondCalculator {
emit DiscountParamsUpdated(newDiscountParams);
}

/// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism.
/// @notice Currently there is only one implementation of a bond calculation mechanism based on the UniswapV2 LP.
/// @notice IDF has a 10^18 multiplier and priceLP has the same as well, so the result must be divided by 10^36.
/// @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 bondVestingTime Bond vesting time.
/// @param productMaxVestingTime Product max vesting time.
/// @param productSupply Current product supply.
/// @param productPayout Current product payout.
/// @param data Custom data that is used to calculate the IDF.
/// @return amountOLAS Resulting amount of OLAS tokens.
/// #if_succeeds {:msg "LP price limit"} priceLP * tokenAmount <= type(uint192).max;
function calculatePayoutOLAS(
address account,
uint256 tokenAmount,
uint256 priceLP,
uint256 bondVestingTime,
uint256 productMaxVestingTime,
uint256 productSupply,
uint256 productPayout
) external view returns (uint256 amountOLAS) {
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)
Expand All @@ -178,34 +178,33 @@ contract GenericBondCalculator {
}

// Calculate the dynamic inverse discount factor
uint256 idf = calculateIDF(account, bondVestingTime, productMaxVestingTime, productSupply, productPayout);
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 account Bonding account address.
/// @param bondVestingTime Bonding desired vesting time.
/// @param productMaxVestingTime Product max vesting time.
/// @param productSupply Current product supply.
/// @param productPayout Current product payout.
/// @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(
address account,
uint256 bondVestingTime,
uint256 productMaxVestingTime,
uint256 productSupply,
uint256 productPayout
) public view returns (uint256 idf) {
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.targetNewUnits > 0) {
if (localParams.weightFactors[0] > 0) {
uint256 numNewUnits = ITokenomics(tokenomics).getLastEpochNumNewUnits();

// If the number of new units exceeds the target, bound by the target number
Expand All @@ -217,16 +216,20 @@ contract GenericBondCalculator {

// Second discount booster: booster += k2 * bondVestingTime / productMaxVestingTime
// Add vesting time discount booster
discountBooster += (localParams.weightFactors[1] * bondVestingTime * 1e18) / productMaxVestingTime;
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
productSupply = productSupply + productPayout;
discountBooster += localParams.weightFactors[2] * (1e18 - ((productPayout * 1e18) / productSupply));
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.targetVotingPower > 0) {
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
Expand Down
34 changes: 19 additions & 15 deletions contracts/Depository.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,21 @@ import {ITokenomics} from "./interfaces/ITokenomics.sol";
import {ITreasury} from "./interfaces/ITreasury.sol";

interface IBondCalculator {
/// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism.
/// @notice Currently there is only one implementation of a bond calculation mechanism based on the UniswapV2 LP.
/// @notice IDF has a 10^18 multiplier and priceLP has the same as well, so the result must be divided by 10^36.
/// @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 bondVestingTime Bond vesting time.
/// @param productMaxVestingTime Product max vesting time.
/// @param productSupply Current product supply.
/// @param productPayout Current product payout.
/// @param data Custom data that is used to calculate the IDF.
/// @return amountOLAS Resulting amount of OLAS tokens.
function calculatePayoutOLAS(
address account,
uint256 tokenAmount,
uint256 priceLP,
uint256 bondVestingTime,
uint256 productMaxVestingTime,
uint256 productSupply,
uint256 productPayout
bytes memory data
) external view returns (uint256 amountOLAS);

/// @dev Gets current reserves of OLAS / totalSupply of Uniswap V2-like LP tokens.
/// @param token Token address.
/// @return priceLP Resulting reserveX / totalSupply ratio with 18 decimals.
function getCurrentPriceLP(address token) external view returns (uint256 priceLP);
}

/// @dev Wrong amount received / provided.
Expand Down Expand Up @@ -372,10 +368,11 @@ contract Depository is ERC721, IErrorsTokenomics {
// Get the LP token address
address token = product.token;

// Calculate the payout in OLAS tokens based on the LP pair with the discount factor (DF) calculation
// Calculate the payout in OLAS tokens based on the LP pair with the inverse discount factor (IDF) calculation
// Note that payout cannot be zero since the price LP is non-zero, otherwise the product would not be created
payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(msg.sender, tokenAmount, product.priceLP,
bondVestingTime, productMaxVestingTime, supply, product.payout);
payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP,
// Encode parameters required for the IDF calculation
abi.encode(msg.sender, bondVestingTime, productMaxVestingTime, supply, product.payout));

// Check for the sufficient supply
if (payout > supply) {
Expand Down Expand Up @@ -565,6 +562,13 @@ contract Depository is ERC721, IErrorsTokenomics {
}
}

/// @dev Gets current reserves of OLAS / totalSupply of Uniswap L2-like LP tokens.
/// @param token Token address.
/// @return priceLP Resulting reserveX / totalSupply ratio with 18 decimals.
function getCurrentPriceLP(address token) external view returns (uint256 priceLP) {
return IBondCalculator(bondCalculator).getCurrentPriceLP(token);
}

/// @dev Gets the valid bond Id from the provided index.
/// @param id Bond counter.
/// @return Bond Id.
Expand Down
23 changes: 11 additions & 12 deletions contracts/GenericBondCalculator.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
pragma solidity ^0.8.25;

import {mulDiv} from "@prb/math/src/Common.sol";
import "./interfaces/ITokenomics.sol";
import "./interfaces/IUniswapV2Pair.sol";

/// @dev Value overflow.
Expand All @@ -13,17 +12,17 @@ error Overflow(uint256 provided, uint256 max);
/// @dev Provided zero address.
error ZeroAddress();

/// @title GenericBondSwap - Smart contract for generic bond calculation mechanisms in exchange for OLAS tokens.
/// @dev The bond calculation mechanism is based on the UniswapV2Pair contract.
/// @author AL
/// @title GenericBondCalculator - Smart contract for generic bond calculation mechanisms in exchange for OLAS tokens.
/// @author Aleksandr Kuperman - <[email protected]>
/// @author Andrey Lebedev - <[email protected]>
/// @author Mariapia Moscatiello - <[email protected]>
contract GenericBondCalculator {
// OLAS contract address
address public immutable olas;
// Tokenomics contract address
address public immutable tokenomics;

/// @dev Generic Bond Calcolator constructor
/// @dev Generic Bond Calculator constructor
/// @param _olas OLAS contract address.
/// @param _tokenomics Tokenomics contract address.
constructor(address _olas, address _tokenomics) {
Expand All @@ -43,7 +42,7 @@ contract GenericBondCalculator {
/// @param priceLP LP token price.
/// @return amountOLAS Resulting amount of OLAS tokens.
/// #if_succeeds {:msg "LP price limit"} priceLP * tokenAmount <= type(uint192).max;
function calculatePayoutOLAS(uint256 tokenAmount, uint256 priceLP) external view
function calculatePayoutOLAS(uint256 tokenAmount, uint256 priceLP, bytes memory) external view virtual
returns (uint256 amountOLAS)
{
// The result is divided by additional 1e18, since it was multiplied by in the current LP price calculation
Expand All @@ -60,15 +59,15 @@ contract GenericBondCalculator {
revert Overflow(totalTokenValue, type(uint192).max);
}
// 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 = ITokenomics(tokenomics).getLastIDF() * totalTokenValue / 1e36;
// Note IDF in Tokenomics is deprecated, and can be assumed as equal to 1e18 by default
amountOLAS = totalTokenValue / 1e18;
}

/// @dev Gets current reserves of OLAS / totalSupply of LP tokens.
/// @dev Gets current reserves of OLAS / totalSupply of Uniswap V2-like LP tokens.
/// @notice The price LP calculation is based on the UniswapV2Pair contract.
/// @param token Token address.
/// @return priceLP Resulting reserveX / totalSupply ratio with 18 decimals.
function getCurrentPriceLP(address token) external view returns (uint256 priceLP)
{
function getCurrentPriceLP(address token) external view virtual returns (uint256 priceLP) {
IUniswapV2Pair pair = IUniswapV2Pair(token);
uint256 totalSupply = pair.totalSupply();
if (totalSupply > 0) {
Expand Down
11 changes: 6 additions & 5 deletions contracts/test/DepositAttacker.sol
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
pragma solidity ^0.8.25;

import {ERC721TokenReceiver} from "../../lib/solmate/src/tokens/ERC721.sol";
import "../interfaces/IToken.sol";
import "../interfaces/IUniswapV2Pair.sol";

interface IDepository {
function deposit(uint256 productId, uint256 tokenAmount) external
function deposit(uint256 productId, uint256 tokenAmount, uint256 vestingTime) external
returns (uint256 payout, uint256 expiry, uint256 bondId);
}

Expand All @@ -29,7 +30,7 @@ interface IZRouter {
}

/// @title DepositAttacker - Smart contract to prove that the deposit attack via price manipulation is not possible
contract DepositAttacker {
contract DepositAttacker is ERC721TokenReceiver {
uint256 public constant LARGE_APPROVAL = 1_000_000 * 1e18;

constructor() {}
Expand Down Expand Up @@ -80,7 +81,7 @@ contract DepositAttacker {
// console.log("AttackDeposit ## OLAS reserved after swap", balanceOLA);
// console.log("AttackDeposit ## DAI reserved before swap", balanceDAI);

(payout, , ) = IDepository(depository).deposit(bid, amountTo);
(payout, , ) = IDepository(depository).deposit(bid, amountTo, 1 weeks);

// DAI approve
IToken(path[1]).approve(swapRouter, LARGE_APPROVAL);
Expand Down Expand Up @@ -145,7 +146,7 @@ contract DepositAttacker {
// console.log("AttackDeposit ## OLAS reserved after swap", balanceOLA);
// console.log("AttackDeposit ## DAI reserved before swap", balanceDAI);

(payout, , ) = IDepository(depository).deposit(bid, amountTo);
(payout, , ) = IDepository(depository).deposit(bid, amountTo, 1 weeks);

// DAI approve
IToken(path[1]).approve(swapRouter, LARGE_APPROVAL);
Expand Down
Loading

0 comments on commit 21bf1d9

Please sign in to comment.