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

Feat: slippage protection for reserve ratio #554

Merged
merged 9 commits into from
Nov 26, 2024
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);
}
}
Loading