Skip to content

Commit

Permalink
Feat: slippage protection for reserve ratio (#554)
Browse files Browse the repository at this point in the history
### Description

Adds slippage protection to `updateRatioForReward()`

### Other changes

Simplifies the new ratio calculation formula

### Tested

Unit tests

### Related issues

- Fixes
[#594](mento-protocol/mento-general#594)
  • Loading branch information
baroooo authored Nov 26, 2024
1 parent 86053df commit 9653183
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 32 deletions.
26 changes: 17 additions & 9 deletions contracts/goodDollar/GoodDollarExchangeProvider.sol
Original file line number Diff line number Diff line change
Expand Up @@ -195,23 +195,31 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan
/**
* @inheritdoc IGoodDollarExchangeProvider
* @dev Calculates the new reserve ratio needed to mint the G$ reward while keeping the current price the same.
* calculation: newRatio = reserveBalance / (tokenSupply + reward) * currentPrice
* calculation: newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward)
*/
function updateRatioForReward(bytes32 exchangeId, uint256 reward) external onlyExpansionController whenNotPaused {
function updateRatioForReward(
bytes32 exchangeId,
uint256 reward,
uint256 maxSlippagePercentage
) external onlyExpansionController whenNotPaused {
PoolExchange memory exchange = getPoolExchange(exchangeId);

uint256 currentPriceScaled = currentPrice(exchangeId) * tokenPrecisionMultipliers[exchange.reserveAsset];
uint256 rewardScaled = reward * tokenPrecisionMultipliers[exchange.tokenAddress];
uint256 scaledRatio = uint256(exchange.reserveRatio) * 1e10;
uint256 scaledReward = reward * tokenPrecisionMultipliers[exchange.tokenAddress];

UD60x18 numerator = wrap(exchange.tokenSupply).mul(wrap(scaledRatio));
UD60x18 denominator = wrap(exchange.tokenSupply).add(wrap(scaledReward));
uint256 newScaledRatio = unwrap(numerator.div(denominator));

UD60x18 numerator = wrap(exchange.reserveBalance);
UD60x18 denominator = wrap(exchange.tokenSupply + rewardScaled).mul(wrap(currentPriceScaled));
uint256 newRatioScaled = unwrap(numerator.div(denominator));
uint32 newRatioUint = uint32(newScaledRatio / 1e10);

uint32 newRatioUint = uint32(newRatioScaled / 1e10);
require(newRatioUint > 0, "New ratio must be greater than 0");

uint256 allowedSlippage = (exchange.reserveRatio * maxSlippagePercentage) / MAX_WEIGHT;
require(exchange.reserveRatio - newRatioUint <= allowedSlippage, "Slippage exceeded");

exchanges[exchangeId].reserveRatio = newRatioUint;
exchanges[exchangeId].tokenSupply += rewardScaled;
exchanges[exchangeId].tokenSupply += scaledReward;

emit ReserveRatioUpdated(exchangeId, newRatioUint);
}
Expand Down
25 changes: 20 additions & 5 deletions contracts/goodDollar/GoodDollarExpansionController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Ownabl
/* ==================== State Variables ==================== */
/* ========================================================= */

// MAX_WEIGHT is the max rate that can be assigned to an exchange
uint256 public constant MAX_WEIGHT = 1e18;
// EXPANSION_MAX_WEIGHT is the max rate that can be assigned to an exchange
uint256 public constant EXPANSION_MAX_WEIGHT = 1e18;

// BANCOR_MAX_WEIGHT is used for BPS calculations in GoodDollarExchangeProvider
uint32 public constant BANCOR_MAX_WEIGHT = 1e8;

// Address of the distribution helper contract
IDistributionHelper public distributionHelper;
Expand Down Expand Up @@ -122,7 +125,7 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Ownabl

/// @inheritdoc IGoodDollarExpansionController
function setExpansionConfig(bytes32 exchangeId, uint64 expansionRate, uint32 expansionFrequency) external onlyAvatar {
require(expansionRate < MAX_WEIGHT, "Expansion rate must be less than 100%");
require(expansionRate < EXPANSION_MAX_WEIGHT, "Expansion rate must be less than 100%");
require(expansionRate > 0, "Expansion rate must be greater than 0");
require(expansionFrequency > 0, "Expansion frequency must be greater than 0");

Expand Down Expand Up @@ -190,12 +193,24 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Ownabl

/// @inheritdoc IGoodDollarExpansionController
function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external onlyAvatar {
// Defaults to no slippage protection
mintRewardFromReserveRatio(exchangeId, to, amount, BANCOR_MAX_WEIGHT);
}

/// @inheritdoc IGoodDollarExpansionController
function mintRewardFromReserveRatio(
bytes32 exchangeId,
address to,
uint256 amount,
uint256 maxSlippagePercentage
) public onlyAvatar {
require(to != address(0), "Recipient address must be set");
require(amount > 0, "Amount must be greater than 0");
require(maxSlippagePercentage <= BANCOR_MAX_WEIGHT, "Max slippage percentage cannot be greater than 100%");
IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider))
.getPoolExchange(exchangeId);

goodDollarExchangeProvider.updateRatioForReward(exchangeId, amount);
goodDollarExchangeProvider.updateRatioForReward(exchangeId, amount, maxSlippagePercentage);
IGoodDollar(exchange.tokenAddress).mint(to, amount);

// Ignored, because contracts only interacts with trusted contracts and tokens
Expand Down Expand Up @@ -238,7 +253,7 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Ownabl
);
}

uint256 stepReserveRatioScalar = MAX_WEIGHT - config.expansionRate;
uint256 stepReserveRatioScalar = EXPANSION_MAX_WEIGHT - config.expansionRate;
return unwrap(powu(wrap(stepReserveRatioScalar), numberOfExpansions));
}

Expand Down
3 changes: 2 additions & 1 deletion contracts/interfaces/IGoodDollarExchangeProvider.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ interface IGoodDollarExchangeProvider {
* @notice Calculates the reserve ratio needed to mint the given G$ reward.
* @param exchangeId The ID of the pool the G$ reward is minted from.
* @param reward The amount of G$ tokens to be minted as a reward.
* @param maxSlippagePercentage Maximum allowed percentage difference between new and old reserve ratio (0-1e8).
*/
function updateRatioForReward(bytes32 exchangeId, uint256 reward) external;
function updateRatioForReward(bytes32 exchangeId, uint256 reward, uint256 maxSlippagePercentage) external;

/**
* @notice Pauses the Exchange, disabling minting.
Expand Down
16 changes: 15 additions & 1 deletion contracts/interfaces/IGoodDollarExpansionController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,24 @@ interface IGoodDollarExpansionController {
function mintUBIFromExpansion(bytes32 exchangeId) external returns (uint256 amountMinted);

/**
* @notice Mints a reward of G$ tokens for a given pool.
* @notice Mints a reward of G$ tokens for a given pool. Defaults to no slippage protection.
* @param exchangeId The ID of the pool to mint a G$ reward for.
* @param to The address of the recipient.
* @param amount The amount of G$ tokens to mint.
*/
function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external;

/**
* @notice Mints a reward of G$ tokens for a given pool.
* @param exchangeId The ID of the pool to mint a G$ reward for.
* @param to The address of the recipient.
* @param amount The amount of G$ tokens to mint.
* @param maxSlippagePercentage Maximum allowed percentage difference between new and old reserve ratio (0-100).
*/
function mintRewardFromReserveRatio(
bytes32 exchangeId,
address to,
uint256 amount,
uint256 maxSlippagePercentage
) external;
}
51 changes: 35 additions & 16 deletions test/unit/goodDollar/GoodDollarExchangeProvider.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -686,31 +686,31 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan

vm.expectRevert("New ratio must be greater than 0");
vm.prank(expansionControllerAddress);
exchangeProvider.updateRatioForReward(exchangeId, veryLargeReward);
exchangeProvider.updateRatioForReward(exchangeId, veryLargeReward, 1e8);
}

function test_updateRatioForReward_whenCallerIsNotExpansionController_shouldRevert() public {
vm.prank(makeAddr("NotExpansionController"));
vm.expectRevert("Only ExpansionController can call this function");
exchangeProvider.updateRatioForReward(exchangeId, reward);
exchangeProvider.updateRatioForReward(exchangeId, reward, 1e8);
}

function test_updateRatioForReward_whenExchangeIdIsInvalid_shouldRevert() public {
vm.prank(expansionControllerAddress);
vm.expectRevert("Exchange does not exist");
exchangeProvider.updateRatioForReward(bytes32(0), reward);
exchangeProvider.updateRatioForReward(bytes32(0), reward, 1e8);
}

function test_updateRatioForReward_whenRewardLarger0_shouldReturnCorrectRatioAndEmit() public {
// formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice)
// reserveRatio = 200_000 / ((7_000_000_000 + 1_000) * 0.000100000002) ≈ 0.28571423...
// formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward)
// formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1_000) = 0.28571423
uint32 expectedReserveRatio = 28571423;
uint256 priceBefore = exchangeProvider.currentPrice(exchangeId);

vm.expectEmit(true, true, true, true);
emit ReserveRatioUpdated(exchangeId, expectedReserveRatio);
vm.prank(expansionControllerAddress);
exchangeProvider.updateRatioForReward(exchangeId, reward);
exchangeProvider.updateRatioForReward(exchangeId, reward, 1e8);

IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId);
uint256 priceAfter = exchangeProvider.currentPrice(exchangeId);
Expand All @@ -727,15 +727,16 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan

function test_updateRatioForReward_whenRewardIsSmall_shouldReturnCorrectRatioAndEmit() public {
uint256 _reward = 1e18; // 1 token
// formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice)
// reserveRatio = 200_000 / ((7_000_000_000 + 1) * 0.000100000002) ≈ 0.2857142799
// formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward)
// formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1) = 0.28571427

uint32 expectedReserveRatio = 28571427;
uint256 priceBefore = exchangeProvider.currentPrice(exchangeId);

vm.expectEmit(true, true, true, true);
emit ReserveRatioUpdated(exchangeId, expectedReserveRatio);
vm.prank(expansionControllerAddress);
exchangeProvider.updateRatioForReward(exchangeId, _reward);
exchangeProvider.updateRatioForReward(exchangeId, _reward, 1e8);

IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId);
uint256 priceAfter = exchangeProvider.currentPrice(exchangeId);
Expand All @@ -751,16 +752,16 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan

function test_updateRatioForReward_whenRewardIsLarge_shouldReturnCorrectRatioAndEmit() public {
uint256 _reward = 1_000_000_000 * 1e18; // 1 billion tokens
// formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice)
// reserveRatio = 200_000 / ((7_000_000_000 + 1_000_000_000) * 0.000100000002) ≈ 0.2499999950000...
// formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward)
// formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1_000_000_000) = 0.249999995

uint32 expectedReserveRatio = 24999999;
uint256 priceBefore = exchangeProvider.currentPrice(exchangeId);

vm.expectEmit(true, true, true, true);
emit ReserveRatioUpdated(exchangeId, expectedReserveRatio);
vm.prank(expansionControllerAddress);
exchangeProvider.updateRatioForReward(exchangeId, _reward);
exchangeProvider.updateRatioForReward(exchangeId, _reward, 1e8);

IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId);
uint256 priceAfter = exchangeProvider.currentPrice(exchangeId);
Expand All @@ -774,6 +775,24 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan
assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price");
}

function test_updateRatioForReward_whenSlippageIsHigherThanAccepted_shouldRevert() public {
uint256 _reward = 1_000_000_000 * 1e18; // 1 billion tokens
// formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward)
// formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1_000_000_000) = 0.249999995
// slippage = (newRatio - reserveRatio) / reserveRatio = (0.249999995 - 0.28571428) / 0.28571428 ~= -0.125

uint32 expectedReserveRatio = 24999999;

vm.prank(expansionControllerAddress);
vm.expectRevert("Slippage exceeded");
exchangeProvider.updateRatioForReward(exchangeId, _reward, 12 * 1e6);

vm.expectEmit(true, true, true, true);
emit ReserveRatioUpdated(exchangeId, expectedReserveRatio);
vm.prank(expansionControllerAddress);
exchangeProvider.updateRatioForReward(exchangeId, _reward, 13 * 1e6);
}

function test_updateRatioForReward_withMultipleConsecutiveRewards() public {
uint256 totalReward = 0;
uint256 initialTokenSupply = poolExchange.tokenSupply;
Expand All @@ -783,7 +802,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan

vm.startPrank(expansionControllerAddress);
for (uint256 i = 0; i < 5; i++) {
exchangeProvider.updateRatioForReward(exchangeId, reward);
exchangeProvider.updateRatioForReward(exchangeId, reward, 1e8);
totalReward += reward;
}
vm.stopPrank();
Expand Down Expand Up @@ -811,7 +830,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan
uint256 priceBefore = exchangeProvider.currentPrice(exchangeId);

vm.prank(expansionControllerAddress);
exchangeProvider.updateRatioForReward(exchangeId, fuzzedReward);
exchangeProvider.updateRatioForReward(exchangeId, fuzzedReward, 1e8);

IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId);
uint256 priceAfter = exchangeProvider.currentPrice(exchangeId);
Expand Down Expand Up @@ -876,7 +895,7 @@ contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTe
exchangeProvider.mintFromInterest(exchangeId, 1e18);

vm.expectRevert("Pausable: paused");
exchangeProvider.updateRatioForReward(exchangeId, 1e18);
exchangeProvider.updateRatioForReward(exchangeId, 1e18, 100);
}

function test_unpause_whenCallerIsAvatar_shouldUnpauseAndEnableExchange() public {
Expand All @@ -897,6 +916,6 @@ contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTe

exchangeProvider.mintFromExpansion(exchangeId, 1e18);
exchangeProvider.mintFromInterest(exchangeId, 1e18);
exchangeProvider.updateRatioForReward(exchangeId, 1e18);
exchangeProvider.updateRatioForReward(exchangeId, 1e18, 1e8);
}
}
20 changes: 20 additions & 0 deletions test/unit/goodDollar/GoodDollarExpansionController.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,12 @@ contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDol
expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 0);
}

function test_mintRewardFromReserveRatio_whenSlippageIsGreaterThan100_shouldRevert() public {
vm.prank(avatarAddress);
vm.expectRevert("Max slippage percentage cannot be greater than 100%");
expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 1000e18, 1e8 + 1);
}

function test_mintRewardFromReserveRatio_whenCallerIsAvatar_shouldMintAndEmit() public {
uint256 amountToMint = 1000e18;
address to = makeAddr("To");
Expand All @@ -658,4 +664,18 @@ contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDol

assertEq(token.balanceOf(to), toBalanceBefore + amountToMint);
}

function test_mintRewardFromReserveRatio_whenCustomSlippage_shouldMintAndEmit() public {
uint256 amountToMint = 1000e18;
address to = makeAddr("To");
uint256 toBalanceBefore = token.balanceOf(to);

vm.expectEmit(true, true, true, true);
emit RewardMinted(exchangeId, to, amountToMint);

vm.prank(avatarAddress);
expansionController.mintRewardFromReserveRatio(exchangeId, to, amountToMint, 1);

assertEq(token.balanceOf(to), toBalanceBefore + amountToMint);
}
}

0 comments on commit 9653183

Please sign in to comment.