Skip to content

Commit

Permalink
test: add BancorExchangeProvider pricing tests (#532)
Browse files Browse the repository at this point in the history
### Description

This Pr adds tests for the pricing logic of the bancor exchange
provider.

During the testing, we realized that the BancorFormula.sol contract is
missing one function we need.
This function is getAmountIn for the tokenIn being the BancorToken. The
new function is called `saleCost()`

The math formula for this function is derived from the
`saleTargetAmount()` function already implemented. It's important that
the base of power operation is larger one otherwise the power function
in that contract reverts.

These are the current formulas used for the different pricing functions:

![image](https://github.com/user-attachments/assets/f1bfd968-1c5a-4ccf-b4eb-830010ed5979)

This is how we derived the formula for `saleCost()`


![image](https://github.com/user-attachments/assets/5f2af737-7f54-477a-b6bc-9bc42eb5a4d5)


### Related issues

- Fixes mento-protocol/mento-general#453

### How to review 

- verify formula is derived correctly 
- exit contribution is applied correctly 
- tests cover everything

---------

Co-authored-by: Ryan Noble <ryanjnoble@gmail.com>
Co-authored-by: baroooo <baranseltekin@gmail.com>
  • Loading branch information
3 people authored Oct 23, 2024
1 parent fcd3d02 commit 578747c
Show file tree
Hide file tree
Showing 3 changed files with 1,095 additions and 76 deletions.
14 changes: 5 additions & 9 deletions contracts/goodDollar/BancorExchangeProvider.sol
Original file line number Diff line number Diff line change
Expand Up @@ -306,14 +306,9 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B
if (tokenIn == exchange.reserveAsset) {
scaledAmountIn = fundCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut);
} else {
scaledAmountIn = fundSupplyAmount(
exchange.tokenSupply,
exchange.reserveBalance,
exchange.reserveRatio,
scaledAmountOut
);

scaledAmountIn = (scaledAmountIn * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution);
// apply exit contribution
scaledAmountOut = (scaledAmountOut * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution);
scaledAmountIn = saleCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut);
}
}

Expand All @@ -339,13 +334,14 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B
scaledAmountIn
);
} else {
scaledAmountIn = (scaledAmountIn * (MAX_WEIGHT - exchange.exitContribution)) / MAX_WEIGHT;
scaledAmountOut = saleTargetAmount(
exchange.tokenSupply,
exchange.reserveBalance,
exchange.reserveRatio,
scaledAmountIn
);
// apply exit contribution
scaledAmountOut = (scaledAmountOut * (MAX_WEIGHT - exchange.exitContribution)) / MAX_WEIGHT;
}
}

Expand Down
58 changes: 40 additions & 18 deletions contracts/goodDollar/BancorFormula.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pragma solidity 0.8.18;
* - bumped solidity version to 0.8.18 and removed SafeMath
* - removed unused functions and variables
* - scaled max weight from 1e6 to 1e8 reran all const python scripts for increased precision
* - added the saleCost() function that returns the amounIn of tokens required to receive a given amountOut of reserve tokens
*
*/

Expand Down Expand Up @@ -218,7 +219,15 @@ contract BancorFormula {
* calculates the target amount for a given conversion (in the reserve token)
*
* Formula:
* return = _reserveBalance * (1 - (1 - _amount / _supply) ^ (1000000 / _reserveWeight))
* return = _reserveBalance * (1 - (1 - _amount / _supply) ^ (MAX_WEIGHT / _reserveWeight))
*
* @dev by MentoLabs: This function actually calculates a different formula that is equivalent to the one above.
* But ensures the base of the power function is larger than 1, which is required by the power function.
* The formula is:
* = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(MAX_WEIGHT/reserveRatio))
* formula: amountOut = ----------------------------------------------------------------------------------
* = (tokenSupply/(tokenSupply - amountIn ))^(MAX_WEIGHT/reserveRatio)
*
*
* @param _supply liquid token supply
* @param _reserveBalance reserve balance
Expand Down Expand Up @@ -297,42 +306,55 @@ contract BancorFormula {
}

/**
* @dev given a pool token supply, reserve balance, reserve ratio and an amount of reserve tokens to fund with,
* calculates the amount of pool tokens received for purchasing with the given amount of reserve tokens
* Added by MentoLabs:
* @notice This function calculates the amount of tokens required to purchase a given amount of reserve tokens.
* @dev this formula was derived from the actual saleTargetAmount() function, and also ensures that the base of the power function is larger than 1.
*
*
* = tokenSupply * (-1 + (reserveBalance / (reserveBalance - amountOut) )^(reserveRatio/MAX_WEIGHT) )
* Formula: amountIn = ------------------------------------------------------------------------------------------------
* = (reserveBalance / (reserveBalance - amountOut) )^(reserveRatio/MAX_WEIGHT)
*
* Formula:
* return = _supply * ((_amount / _reserveBalance + 1) ^ (_reserveRatio / MAX_WEIGHT) - 1)
*
* @param _supply pool token supply
* @param _reserveBalance reserve balance
* @param _reserveRatio reserve ratio, represented in ppm (2-2000000)
* @param _amount amount of reserve tokens to fund with
* @param _reserveWeight reserve weight, represented in ppm
* @param _amount amount of reserve tokens to get the target amount for
*
* @return pool token amount
* @return reserve token amount
*/
function fundSupplyAmount(
function saleCost(
uint256 _supply,
uint256 _reserveBalance,
uint32 _reserveRatio,
uint32 _reserveWeight,
uint256 _amount
) internal view returns (uint256) {
// validate input
require(_supply > 0, "ERR_INVALID_SUPPLY");
require(_reserveBalance > 0, "ERR_INVALID_RESERVE_BALANCE");
require(_reserveRatio > 1 && _reserveRatio <= MAX_WEIGHT * 2, "ERR_INVALID_RESERVE_RATIO");
require(_reserveWeight > 0 && _reserveWeight <= MAX_WEIGHT, "ERR_INVALID_RESERVE_WEIGHT");

// special case for 0 amount
require(_amount <= _reserveBalance, "ERR_INVALID_AMOUNT");

// special case for 0 sell amount
if (_amount == 0) return 0;

// special case if the reserve ratio = 100%
if (_reserveRatio == MAX_WEIGHT) return (_amount * _supply) / _reserveBalance;
// special case for selling the entire supply
if (_amount == _reserveBalance) return _supply;

// special case if the weight = 100%
// base formula can be simplified to:
// Formula: amountIn = amountOut * supply / reserveBalance
// the +1 and -1 are to ensure that this function rounds up which is required to prevent protocol loss.
if (_reserveWeight == MAX_WEIGHT) return (_supply * _amount - 1) / _reserveBalance + 1;

uint256 result;
uint8 precision;
uint256 baseN = _reserveBalance + _amount;
(result, precision) = power(baseN, _reserveBalance, _reserveRatio, MAX_WEIGHT);
uint256 temp = (_supply * result) >> precision;
return temp - _supply;
uint256 baseD = _reserveBalance - _amount;
(result, precision) = power(_reserveBalance, baseD, _reserveWeight, MAX_WEIGHT);
uint256 temp1 = _supply * result;
uint256 temp2 = _supply << precision;
return (temp1 - temp2 - 1) / result + 1;
}

/**
Expand Down
Loading

0 comments on commit 578747c

Please sign in to comment.