Skip to content

Commit

Permalink
fix: cantina 111 & 477
Browse files Browse the repository at this point in the history
  • Loading branch information
kasperpawlowski committed Jul 4, 2024
1 parent f301947 commit 697e846
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 98 deletions.
49 changes: 32 additions & 17 deletions src/Synths/PegStabilityModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,61 +5,70 @@ pragma solidity ^0.8.0;
import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import {Math} from "openzeppelin-contracts/utils/math/Math.sol";
import {ESynth} from "./ESynth.sol";

/// @title PegStabilityModule
/// @custom:security-contact [email protected]
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice The PegStabilityModule is granted minting rights on the ESynth and must allow slippage-free conversion from
/// and to the underlying asset as per configured conversionPrice. On deployment, the fee for swaps to synthetic asset
/// and to the underlying asset as per configured CONVERSION_PRICE. On deployment, the fee for swaps to synthetic asset
/// and to underlying asset are defined. These fees must accrue to the PegStabilityModule contract and can not be
/// withdrawn, serving as a permanent reserve to support the peg. Swapping to the synthetic asset is possible up to the
/// minting cap granted for the PegStabilityModule in the ESynth. Swapping to the underlying asset is possible up to the
/// amount of the underlying asset held by the PegStabilityModule.
contract PegStabilityModule is EVCUtil {
using SafeERC20 for IERC20;
using Math for uint256;

uint256 public constant BPS_SCALE = 100_00;
uint256 public constant PRICE_SCALE = 1e18;

ESynth public immutable synth;
IERC20 public immutable underlying;
uint256 public immutable conversionPrice; // 1e18 = 1 SYNTH == 1 UNDERLYING, 0.01e18 = 1 SYNTH == 0.01 UNDERLYING

uint256 public immutable TO_UNDERLYING_FEE;
uint256 public immutable TO_SYNTH_FEE;
uint256 public immutable CONVERSION_PRICE;

error E_ZeroAddress();
error E_FeeExceedsBPS();
error E_ZeroConversionPrice();

/// @param _evc The address of the EVC.
/// @param _synth The address of the synthetic asset.
/// @param _underlying The address of the underlying asset.
/// @param toUnderlyingFeeBPS The fee for swapping to the underlying asset in basis points. eg: 100 = 1%
/// @param toSynthFeeBPS The fee for swapping to the synthetic asset in basis points. eg: 100 = 1%
/// @param _toUnderlyingFeeBPS The fee for swapping to the underlying asset in basis points. eg: 100 = 1%
/// @param _toSynthFeeBPS The fee for swapping to the synthetic asset in basis points. eg: 100 = 1%
/// @param _conversionPrice The conversion price between the synthetic and underlying asset.
/// eg: 1e18 = 1 SYNTH == 1 UNDERLYING, 0.01e18 = 1 SYNTH == 0.01 UNDERLYING
/// @dev _conversionPrice = 10**underlyingDecimals corresponds to 1:1 peg
/// @dev if underlying is 18 decimals, _conversionPrice = 1e18 corresponds to 1:1 peg
/// @dev if underlying is 6 decimals, _conversionPrice = 1e6 corresponds to 1:1 peg
constructor(
address _evc,
address _synth,
address _underlying,
uint256 toUnderlyingFeeBPS,
uint256 toSynthFeeBPS,
uint256 _toUnderlyingFeeBPS,
uint256 _toSynthFeeBPS,
uint256 _conversionPrice
) EVCUtil(_evc) {
if (toUnderlyingFeeBPS >= BPS_SCALE || toSynthFeeBPS >= BPS_SCALE) {
if (_synth == address(0) || _underlying == address(0)) {
revert E_ZeroAddress();
}

if (_toUnderlyingFeeBPS >= BPS_SCALE || _toSynthFeeBPS >= BPS_SCALE) {
revert E_FeeExceedsBPS();
}

if (_evc == address(0) || _synth == address(0) || _underlying == address(0)) {
revert E_ZeroAddress();
if (_conversionPrice == 0) {
revert E_ZeroConversionPrice();
}

synth = ESynth(_synth);
underlying = IERC20(_underlying);
TO_UNDERLYING_FEE = toUnderlyingFeeBPS;
TO_SYNTH_FEE = toSynthFeeBPS;
conversionPrice = _conversionPrice;
TO_UNDERLYING_FEE = _toUnderlyingFeeBPS;
TO_SYNTH_FEE = _toSynthFeeBPS;
CONVERSION_PRICE = _conversionPrice;
}

/// @notice Swaps the given amount of synth to underlying given an input amount of synth.
Expand Down Expand Up @@ -130,27 +139,33 @@ contract PegStabilityModule is EVCUtil {
/// @param amountIn The amount of synth to swap.
/// @return The amount of underlying received.
function quoteToUnderlyingGivenIn(uint256 amountIn) public view returns (uint256) {
return amountIn * (BPS_SCALE - TO_UNDERLYING_FEE) * conversionPrice / BPS_SCALE / PRICE_SCALE;
return amountIn.mulDiv(
(BPS_SCALE - TO_UNDERLYING_FEE) * CONVERSION_PRICE, BPS_SCALE * PRICE_SCALE, Math.Rounding.Floor
);
}

/// @notice Quotes the amount of underlying given an output amount of synth.
/// @param amountOut The amount of underlying to receive.
/// @return The amount of synth swapped.
function quoteToUnderlyingGivenOut(uint256 amountOut) public view returns (uint256) {
return amountOut * BPS_SCALE * PRICE_SCALE / (BPS_SCALE - TO_UNDERLYING_FEE) / conversionPrice;
return amountOut.mulDiv(
(BPS_SCALE + TO_UNDERLYING_FEE) * PRICE_SCALE, BPS_SCALE * CONVERSION_PRICE, Math.Rounding.Ceil
);
}

/// @notice Quotes the amount of synth given an input amount of underlying.
/// @param amountIn The amount of underlying to swap.
/// @return The amount of synth received.
function quoteToSynthGivenIn(uint256 amountIn) public view returns (uint256) {
return amountIn * (BPS_SCALE - TO_SYNTH_FEE) * PRICE_SCALE / BPS_SCALE / conversionPrice;
return
amountIn.mulDiv((BPS_SCALE - TO_SYNTH_FEE) * PRICE_SCALE, BPS_SCALE * CONVERSION_PRICE, Math.Rounding.Floor);
}

/// @notice Quotes the amount of synth given an output amount of underlying.
/// @param amountOut The amount of synth to receive.
/// @return The amount of underlying swapped.
function quoteToSynthGivenOut(uint256 amountOut) public view returns (uint256) {
return amountOut * BPS_SCALE * conversionPrice / (BPS_SCALE - TO_SYNTH_FEE) / PRICE_SCALE;
return
amountOut.mulDiv((BPS_SCALE + TO_SYNTH_FEE) * CONVERSION_PRICE, BPS_SCALE * PRICE_SCALE, Math.Rounding.Ceil);
}
}
183 changes: 102 additions & 81 deletions test/unit/pegStabilityModules/PSM.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,52 +32,73 @@ contract PSMTest is Test {
// Deploy EVC
evc = new EthereumVaultConnector();

// Deploy underlying
underlying = new TestERC20("TestUnderlying", "TUNDERLYING", 18, false);

// Deploy synth
vm.prank(owner);
synth = new ESynth(evc, "TestSynth", "TSYNTH");

// Deploy underlying
underlying = new TestERC20("TestUnderlying", "TUNDERLYING", 18, false);

// Deploy PSM
vm.prank(owner);
psm = new PegStabilityModule(
address(evc), address(synth), address(underlying), TO_UNDERLYING_FEE, TO_SYNTH_FEE, CONVERSION_PRICE
);
}

function fuzzSetUp(
uint8 underlyingDecimals,
uint256 _toUnderlyingFeeBPS,
uint256 _toSynthFeeBPS,
uint256 _conversionPrice
) internal {
// Redeploy underlying
underlying = new TestERC20("TestUnderlying", "TUNDERLYING", underlyingDecimals, false);

// Redeploy PSM
vm.prank(owner);
psm = new PegStabilityModule(
address(evc), address(synth), address(underlying), _toUnderlyingFeeBPS, _toSynthFeeBPS, _conversionPrice
);

// Give PSM and wallets some underlying
underlying.mint(address(psm), 100e18);
underlying.mint(wallet1, 100e18);
underlying.mint(wallet2, 100e18);
uint128 amount = uint128(100 * 10 ** underlyingDecimals);
underlying.mint(address(psm), amount);
underlying.mint(wallet1, amount);
underlying.mint(wallet2, amount);

// Approve PSM to spend underlying
vm.prank(wallet1);
underlying.approve(address(psm), 100e18);
underlying.approve(address(psm), type(uint256).max);
vm.prank(wallet2);
underlying.approve(address(psm), 100e18);
underlying.approve(address(psm), type(uint256).max);

// Set PSM as minter
amount = 100 * 10 ** 18;
vm.prank(owner);
synth.setCapacity(address(psm), 100e18);
synth.setCapacity(address(psm), amount);

// Mint some synth to wallets
vm.startPrank(owner);
synth.setCapacity(owner, 200e18);
synth.mint(wallet1, 100e18);
synth.mint(wallet2, 100e18);
synth.setCapacity(owner, uint128(2 * amount));
synth.mint(wallet1, amount);
synth.mint(wallet2, amount);
vm.stopPrank();

// Set approvals for PSM
vm.prank(wallet1);
synth.approve(address(psm), 100e18);
synth.approve(address(psm), type(uint256).max);
vm.prank(wallet2);
synth.approve(address(psm), 100e18);
synth.approve(address(psm), type(uint256).max);
}

function testConstructor() public view {
assertEq(address(psm.EVC()), address(evc));
assertEq(address(psm.synth()), address(synth));
assertEq(address(psm.underlying()), address(underlying));
assertEq(psm.TO_UNDERLYING_FEE(), TO_UNDERLYING_FEE);
assertEq(psm.TO_SYNTH_FEE(), TO_SYNTH_FEE);
assertEq(psm.CONVERSION_PRICE(), CONVERSION_PRICE);
}

function testConstructorToUnderlyingFeeExceedsBPS() public {
Expand Down Expand Up @@ -115,9 +136,21 @@ contract PSMTest is Test {
);
}

function testSwapToUnderlyingGivenIn() public {
uint256 amountIn = 10e18;
uint256 expectedAmountOut = amountIn * (BPS_SCALE - TO_UNDERLYING_FEE) / BPS_SCALE;
function testConstructorZeroConversionPrice() public {
vm.expectRevert(PegStabilityModule.E_ZeroConversionPrice.selector);
new PegStabilityModule(address(evc), address(synth), address(underlying), TO_UNDERLYING_FEE, TO_SYNTH_FEE, 0);
}

function testSwapToUnderlyingGivenIn(uint8 underlyingDecimals, uint256 fee, uint256 amountInNoDecimals) public {
underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18));
fee = bound(fee, 0, BPS_SCALE - 1);
amountInNoDecimals = bound(amountInNoDecimals, 1, 100);
fuzzSetUp(underlyingDecimals, fee, 0, 10 ** underlyingDecimals);

uint256 amountIn = amountInNoDecimals * 10 ** 18;
uint256 expectedAmountOut = amountInNoDecimals * 10 ** underlyingDecimals * (BPS_SCALE - fee) / BPS_SCALE;

assertEq(psm.quoteToUnderlyingGivenIn(amountIn), expectedAmountOut);

uint256 swapperSynthBalanceBefore = synth.balanceOf(wallet1);
uint256 receiverBalanceBefore = underlying.balanceOf(wallet2);
Expand All @@ -135,9 +168,16 @@ contract PSMTest is Test {
assertEq(psmUnderlyingBalanceAfter, psmUnderlyingBalanceBefore - expectedAmountOut);
}

function testSwapToUnderlyingGivenOut() public {
uint256 amountOut = 10e18;
uint256 expectedAmountIn = amountOut * BPS_SCALE / (BPS_SCALE - TO_UNDERLYING_FEE);
function testSwapToUnderlyingGivenOut(uint8 underlyingDecimals, uint256 fee, uint256 amountOutNoDecimals) public {
underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18));
fee = bound(fee, 0, BPS_SCALE - 1);
amountOutNoDecimals = bound(amountOutNoDecimals, 1, 50);
fuzzSetUp(underlyingDecimals, fee, 0, 10 ** underlyingDecimals);

uint256 amountOut = amountOutNoDecimals * 10 ** underlyingDecimals;
uint256 expectedAmountIn = amountOutNoDecimals * 10 ** 18 * (BPS_SCALE + fee) / BPS_SCALE;

assertEq(psm.quoteToUnderlyingGivenOut(amountOut), expectedAmountIn);

uint256 swapperSynthBalanceBefore = synth.balanceOf(wallet1);
uint256 receiverBalanceBefore = underlying.balanceOf(wallet2);
Expand All @@ -155,9 +195,16 @@ contract PSMTest is Test {
assertEq(psmUnderlyingBalanceAfter, psmUnderlyingBalanceBefore - amountOut);
}

function testSwapToSynthGivenIn() public {
uint256 amountIn = 10e18;
uint256 expectedAmountOut = amountIn * (BPS_SCALE - TO_SYNTH_FEE) / BPS_SCALE;
function testSwapToSynthGivenIn(uint8 underlyingDecimals, uint256 fee, uint256 amountInNoDecimals) public {
underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18));
fee = bound(fee, 0, BPS_SCALE - 1);
amountInNoDecimals = bound(amountInNoDecimals, 1, 100);
fuzzSetUp(underlyingDecimals, 0, fee, 10 ** underlyingDecimals);

uint256 amountIn = amountInNoDecimals * 10 ** underlyingDecimals;
uint256 expectedAmountOut = amountInNoDecimals * 10 ** 18 * (BPS_SCALE - fee) / BPS_SCALE;

assertEq(psm.quoteToSynthGivenIn(amountIn), expectedAmountOut);

uint256 swapperUnderlyingBalanceBefore = underlying.balanceOf(wallet1);
uint256 receiverSynthBalanceBefore = synth.balanceOf(wallet2);
Expand All @@ -175,9 +222,16 @@ contract PSMTest is Test {
assertEq(psmUnderlyingBalanceAfter, psmUnderlyingBalanceBefore + amountIn);
}

function testSwapToSynthGivenOut() public {
uint256 amountOut = 10e18;
uint256 expectedAmountIn = amountOut * BPS_SCALE / (BPS_SCALE - TO_SYNTH_FEE);
function testSwapToSynthGivenOut(uint8 underlyingDecimals, uint256 fee, uint256 amountOutNoDecimals) public {
underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18));
fee = bound(fee, 0, BPS_SCALE - 1);
amountOutNoDecimals = bound(amountOutNoDecimals, 1, 50);
fuzzSetUp(underlyingDecimals, 0, fee, 10 ** underlyingDecimals);

uint256 amountOut = amountOutNoDecimals * 10 ** 18;
uint256 expectedAmountIn = amountOutNoDecimals * 10 ** underlyingDecimals * (BPS_SCALE + fee) / BPS_SCALE;

assertEq(psm.quoteToSynthGivenOut(amountOut), expectedAmountIn);

uint256 swapperUnderlyingBalanceBefore = underlying.balanceOf(wallet1);
uint256 receiverSynthBalanceBefore = synth.balanceOf(wallet2);
Expand All @@ -195,66 +249,33 @@ contract PSMTest is Test {
assertEq(psmUnderlyingBalanceAfter, psmUnderlyingBalanceBefore + expectedAmountIn);
}

// Test quotes
function testQuoteToUnderlyingGivenIn() public view {
uint256 amountIn = 10e18;
uint256 expectedAmountOut = amountIn * (BPS_SCALE - TO_UNDERLYING_FEE) / BPS_SCALE;

uint256 amountOut = psm.quoteToUnderlyingGivenIn(amountIn);

assertEq(amountOut, expectedAmountOut);
}

function testQuoteToUnderlyingGivenOut() public view {
uint256 amountOut = 10e18;
uint256 expectedAmountIn = amountOut * BPS_SCALE / (BPS_SCALE - TO_UNDERLYING_FEE);

uint256 amountIn = psm.quoteToUnderlyingGivenOut(amountOut);

assertEq(amountIn, expectedAmountIn);
}

function testQuoteToSynthGivenIn() public view {
uint256 amountIn = 10e18;
uint256 expectedAmountOut = amountIn * (BPS_SCALE - TO_SYNTH_FEE) / BPS_SCALE;
function testSanityPriceConversions(uint8 underlyingDecimals, uint256 amount, uint256 multiplier) public {
underlyingDecimals = uint8(bound(underlyingDecimals, 6, 18));
amount = bound(amount, 1, 100);
multiplier = bound(multiplier, 1, 10000);
fuzzSetUp(underlyingDecimals, 0, 0, 10 ** underlyingDecimals * multiplier / 100);

uint256 amountOut = psm.quoteToSynthGivenIn(amountIn);
uint256 synthAmount = amount * 10 ** 18;
uint256 underlyingAmount = amount * 10 ** underlyingDecimals * multiplier / 100;

assertEq(amountOut, expectedAmountOut);
assertEq(psm.quoteToSynthGivenIn(underlyingAmount), synthAmount);
assertEq(psm.quoteToSynthGivenOut(synthAmount), underlyingAmount);
assertEq(psm.quoteToUnderlyingGivenIn(synthAmount), underlyingAmount);
assertEq(psm.quoteToUnderlyingGivenOut(underlyingAmount), synthAmount);
}

function testQuoteToSynthGivenOut() public view {
uint256 amountOut = 10e18;
uint256 expectedAmountIn = amountOut * BPS_SCALE / (BPS_SCALE - TO_SYNTH_FEE);

uint256 amountIn = psm.quoteToSynthGivenOut(amountOut);

assertEq(amountIn, expectedAmountIn);
}

function testSanityPriceConversionToSynth() public {
uint256 price = 0.25e18;

uint256 synthAmount = 1e18;
uint256 underlyingAmount = 0.25e18;

PegStabilityModule psmNoFee =
new PegStabilityModule(address(evc), address(synth), address(underlying), 0, 0, price);

assertEq(psmNoFee.quoteToSynthGivenIn(underlyingAmount), synthAmount);
assertEq(psmNoFee.quoteToSynthGivenOut(synthAmount), underlyingAmount);
function testRoundingPriceConversionsEqualDecimals() public {
assertEq(psm.quoteToSynthGivenIn(1), 0);
assertEq(psm.quoteToSynthGivenOut(1), 2);
assertEq(psm.quoteToUnderlyingGivenIn(1), 0);
assertEq(psm.quoteToUnderlyingGivenOut(1), 2);
}

function testSanityPriceConversionToUnderlying() public {
uint256 price = 0.25e18;

uint256 synthAmount = 1e18;
uint256 underlyingAmount = 0.25e18;

PegStabilityModule psmNoFee =
new PegStabilityModule(address(evc), address(synth), address(underlying), 0, 0, price);

assertEq(psmNoFee.quoteToUnderlyingGivenIn(synthAmount), underlyingAmount);
assertEq(psmNoFee.quoteToUnderlyingGivenOut(underlyingAmount), synthAmount);
function testRoundingPriceConversionsDiffDecimals() public {
fuzzSetUp(8, 0, 0, 1e8);
assertEq(psm.quoteToSynthGivenIn(1), 1e10);
assertEq(psm.quoteToSynthGivenOut(1), 1);
assertEq(psm.quoteToUnderlyingGivenIn(1), 0);
assertEq(psm.quoteToUnderlyingGivenOut(1), 1e10);
}
}

0 comments on commit 697e846

Please sign in to comment.