Skip to content

Commit

Permalink
feat: Add fuzz testing for swap functionality, add rounding up (SC-48…
Browse files Browse the repository at this point in the history
…2) (#22)

* feat: update swap to swapExactAmountIn

* feat: add preview test coverage

* feat: add new function

* feat: create new file, failure tests passing

* ci: add coverage backgp

* feat: get new swap tests passing

* feat: set up initial test

* feat: refactor code to add returns

* feat: add return value assertions

* feat: working fuzz test

* feat: add swap exact out, show that rounding is against user

* fix: update natspec

* feat: refactor to round up on swap exact out

* feat: all tests passing

* fix: merge conflicts

* fix: cleanup

* feat: update tolerances

* fix: update tolerance

* fix: increase fuzz tolerance

* fix: updat to totalAssets

* fix: review changes
  • Loading branch information
lucas-manuel authored Jul 17, 2024
1 parent e62dac2 commit 039ae15
Show file tree
Hide file tree
Showing 4 changed files with 355 additions and 50 deletions.
66 changes: 37 additions & 29 deletions src/PSM3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,15 @@ contract PSM3 is IPSM3 {
function previewSwapExactIn(address assetIn, address assetOut, uint256 amountIn)
public view override returns (uint256 amountOut)
{
amountOut = _getSwapQuote(assetIn, assetOut, amountIn);
// Round down to get amountOut
amountOut = _getSwapQuote(assetIn, assetOut, amountIn, false);
}

function previewSwapExactOut(address assetIn, address assetOut, uint256 amountOut)
public view override returns (uint256 amountIn)
{
amountIn = _getSwapQuote(assetOut, assetIn, amountOut);
// Round up to get amountIn
amountIn = _getSwapQuote(assetOut, assetIn, amountOut, true);
}

/**********************************************************************************************/
Expand Down Expand Up @@ -271,58 +273,64 @@ contract PSM3 is IPSM3 {
/*** Internal preview functions (swaps) ***/
/**********************************************************************************************/

function _getSwapQuote(address asset, address quoteAsset, uint256 amount)
function _getSwapQuote(address asset, address quoteAsset, uint256 amount, bool roundUp)
public view returns (uint256 quoteAmount)
{
if (asset == address(asset0)) {
if (quoteAsset == address(asset1)) return _previewOneToOneSwap(amount, _asset0Precision, _asset1Precision);
else if (quoteAsset == address(asset2)) return _previewSwapToAsset2(amount, _asset0Precision);
if (quoteAsset == address(asset1)) return _convertOneToOne(amount, _asset0Precision, _asset1Precision, roundUp);
else if (quoteAsset == address(asset2)) return _convertToAsset2(amount, _asset0Precision, roundUp);
}

else if (asset == address(asset1)) {
if (quoteAsset == address(asset0)) return _previewOneToOneSwap(amount, _asset1Precision, _asset0Precision);
else if (quoteAsset == address(asset2)) return _previewSwapToAsset2(amount, _asset1Precision);
if (quoteAsset == address(asset0)) return _convertOneToOne(amount, _asset1Precision, _asset0Precision, roundUp);
else if (quoteAsset == address(asset2)) return _convertToAsset2(amount, _asset1Precision, roundUp);
}

else if (asset == address(asset2)) {
if (quoteAsset == address(asset0)) return _previewSwapFromAsset2(amount, _asset0Precision);
else if (quoteAsset == address(asset1)) return _previewSwapFromAsset2(amount, _asset1Precision);
if (quoteAsset == address(asset0)) return _convertFromAsset2(amount, _asset0Precision, roundUp);
else if (quoteAsset == address(asset1)) return _convertFromAsset2(amount, _asset1Precision, roundUp);
}

revert("PSM3/invalid-asset");
}


function _previewSwapToAsset2(uint256 amountIn, uint256 assetInPrecision)
function _convertToAsset2(uint256 amount, uint256 assetPrecision, bool roundUp)
internal view returns (uint256)
{
return amountIn
* 1e27
/ IRateProviderLike(rateProvider).getConversionRate()
* _asset2Precision
/ assetInPrecision;
uint256 rate = IRateProviderLike(rateProvider).getConversionRate();

if (!roundUp) return amount * 1e27 / rate * _asset2Precision / assetPrecision;

return _divUp(
_divUp(amount * 1e27, rate) * _asset2Precision,
assetPrecision
);
}

function _previewSwapFromAsset2(uint256 amountIn, uint256 assetInPrecision)
function _convertFromAsset2(uint256 amount, uint256 assetPrecision, bool roundUp)
internal view returns (uint256)
{
return amountIn
* IRateProviderLike(rateProvider).getConversionRate()
/ 1e27
* assetInPrecision
/ _asset2Precision;
uint256 rate = IRateProviderLike(rateProvider).getConversionRate();

if (!roundUp) return amount * rate / 1e27 * assetPrecision / _asset2Precision;

return _divUp(
_divUp(amount * rate, 1e27) * assetPrecision,
_asset2Precision
);
}

function _previewOneToOneSwap(
uint256 amountIn,
uint256 assetInPrecision,
uint256 assetOutPrecision
function _convertOneToOne(
uint256 amount,
uint256 assetPrecision,
uint256 convertAssetPrecision,
bool roundUp
)
internal pure returns (uint256)
{
return amountIn
* assetOutPrecision
/ assetInPrecision;
if (!roundUp) return amount * convertAssetPrecision / assetPrecision;

return _divUp(amount * convertAssetPrecision, assetPrecision);
}

/**********************************************************************************************/
Expand Down
34 changes: 25 additions & 9 deletions test/unit/Previews.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,12 @@ contract PSMPreviewSwapExactOut_DaiAssetInTests is PSMTestBase {

mockRateProvider.__setConversionRate(conversionRate);

uint256 amountIn = amountOut * conversionRate / 1e27;
uint256 expectedAmountIn = amountOut * conversionRate / 1e27;

assertEq(psm.previewSwapExactOut(address(dai), address(sDai), amountOut), amountIn);
uint256 amountIn = psm.previewSwapExactOut(address(dai), address(sDai), amountOut);

// Allow for rounding error of 1 unit upwards
assertLe(amountIn - expectedAmountIn, 1);
}

}
Expand Down Expand Up @@ -176,7 +179,10 @@ contract PSMPreviewSwapExactOut_USDCAssetInTests is PSMTestBase {
function testFuzz_previewSwapExactOut_usdcToDai(uint256 amountOut) public view {
amountOut = _bound(amountOut, 0, DAI_TOKEN_MAX);

assertEq(psm.previewSwapExactOut(address(usdc), address(dai), amountOut), amountOut / 1e12);
uint256 amountIn = psm.previewSwapExactOut(address(usdc), address(dai), amountOut);

// Allow for rounding error of 1 unit upwards
assertLe(amountIn - amountOut / 1e12, 1);
}

function test_previewSwapExactOut_usdcToSDai() public view {
Expand All @@ -191,9 +197,13 @@ contract PSMPreviewSwapExactOut_USDCAssetInTests is PSMTestBase {

mockRateProvider.__setConversionRate(conversionRate);

uint256 amountIn = amountOut * conversionRate / 1e27 / 1e12;
// Using raw calculation to demo rounding
uint256 expectedAmountIn = amountOut * conversionRate / 1e27 / 1e12;

uint256 amountIn = psm.previewSwapExactOut(address(usdc), address(sDai), amountOut);

assertEq(psm.previewSwapExactOut(address(usdc), address(sDai), amountOut), amountIn);
// Allow for rounding error of 1 unit upwards
assertLe(amountIn - expectedAmountIn, 1);
}

}
Expand Down Expand Up @@ -250,9 +260,12 @@ contract PSMPreviewSwapExactOut_SDaiAssetInTests is PSMTestBase {

mockRateProvider.__setConversionRate(conversionRate);

uint256 amountIn = amountOut * 1e27 / conversionRate;
uint256 expectedAmountIn = amountOut * 1e27 / conversionRate;

assertEq(psm.previewSwapExactOut(address(sDai), address(dai), amountOut), amountIn);
uint256 amountIn = psm.previewSwapExactOut(address(sDai), address(dai), amountOut);

// Allow for rounding error of 1 unit upwards
assertLe(amountIn - expectedAmountIn, 1);
}

function test_previewSwapExactOut_sDaiToUsdc() public view {
Expand All @@ -267,9 +280,12 @@ contract PSMPreviewSwapExactOut_SDaiAssetInTests is PSMTestBase {

mockRateProvider.__setConversionRate(conversionRate);

uint256 amountIn = amountOut * 1e27 / conversionRate * 1e12;
uint256 expectedAmountIn = amountOut * 1e27 / conversionRate * 1e12;

uint256 amountIn = psm.previewSwapExactOut(address(sDai), address(usdc), amountOut);

assertEq(psm.previewSwapExactOut(address(sDai), address(usdc), amountOut), amountIn);
// Allow for rounding error of 1e12 upwards
assertLe(amountIn - expectedAmountIn, 1e12);
}

}
117 changes: 116 additions & 1 deletion test/unit/SwapExactIn.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "forge-std/Test.sol";

import { PSM3 } from "src/PSM3.sol";

import { MockERC20, MockRateProvider, PSMTestBase } from "test/PSMTestBase.sol";
import { MockERC20, PSMTestBase } from "test/PSMTestBase.sol";

contract PSMSwapExactInFailureTests is PSMTestBase {

Expand Down Expand Up @@ -353,3 +353,118 @@ contract PSMSwapExactInSDaiAssetInTests is PSMSwapExactInSuccessTestsBase {
}

}

contract PSMSwapExactInFuzzTests is PSMTestBase {

address lp0 = makeAddr("lp0");
address lp1 = makeAddr("lp1");
address lp2 = makeAddr("lp2");

address swapper = makeAddr("swapper");

struct FuzzVars {
uint256 lp0StartingValue;
uint256 lp1StartingValue;
uint256 lp2StartingValue;
uint256 psmStartingValue;
uint256 lp0CachedValue;
uint256 lp1CachedValue;
uint256 lp2CachedValue;
uint256 psmCachedValue;
}

/// forge-config: default.fuzz.runs = 10
/// forge-config: pr.fuzz.runs = 100
/// forge-config: master.fuzz.runs = 10000
function testFuzz_swapExactIn(
uint256 conversionRate,
uint256 depositSeed
) public {
// 1% to 200% conversion rate
mockRateProvider.__setConversionRate(_bound(conversionRate, 0.01e27, 2e27));

_deposit(address(dai), lp0, _bound(_hash(depositSeed, "lp0-dai"), 1, DAI_TOKEN_MAX));

_deposit(address(usdc), lp1, _bound(_hash(depositSeed, "lp1-usdc"), 1, USDC_TOKEN_MAX));
_deposit(address(sDai), lp1, _bound(_hash(depositSeed, "lp1-sdai"), 1, SDAI_TOKEN_MAX));

_deposit(address(dai), lp2, _bound(_hash(depositSeed, "lp2-dai"), 1, DAI_TOKEN_MAX));
_deposit(address(usdc), lp2, _bound(_hash(depositSeed, "lp2-usdc"), 1, USDC_TOKEN_MAX));
_deposit(address(sDai), lp2, _bound(_hash(depositSeed, "lp2-sdai"), 1, SDAI_TOKEN_MAX));

FuzzVars memory vars;

vars.lp0StartingValue = psm.convertToAssetValue(psm.shares(lp0));
vars.lp1StartingValue = psm.convertToAssetValue(psm.shares(lp1));
vars.lp2StartingValue = psm.convertToAssetValue(psm.shares(lp2));
vars.psmStartingValue = psm.totalAssets();

vm.startPrank(swapper);

for (uint256 i; i < 1000; ++i) {
MockERC20 assetIn = _getAsset(_hash(i, "assetIn"));
MockERC20 assetOut = _getAsset(_hash(i, "assetOut"));

if (assetIn == assetOut) {
assetOut = _getAsset(_hash(i, "assetOut") + 1);
}

// Calculate the maximum amount that can be swapped by using the inverse conversion rate
uint256 maxAmountIn = psm.previewSwapExactOut(
address(assetIn),
address(assetOut),
assetOut.balanceOf(address(psm))
);

uint256 amountIn = _bound(_hash(i, "amountIn"), 0, maxAmountIn - 1); // Rounding

vars.lp0CachedValue = psm.convertToAssetValue(psm.shares(lp0));
vars.lp1CachedValue = psm.convertToAssetValue(psm.shares(lp1));
vars.lp2CachedValue = psm.convertToAssetValue(psm.shares(lp2));
vars.psmCachedValue = psm.totalAssets();

assetIn.mint(swapper, amountIn);
assetIn.approve(address(psm), amountIn);
psm.swapExactIn(address(assetIn), address(assetOut), amountIn, 0, swapper, 0);

// Rounding is always in favour of the LPs
assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue);
assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue);
assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue);
assertGe(psm.totalAssets(), vars.psmCachedValue);

// Up to 2e12 rounding on each swap
assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0CachedValue, 2e12);
assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1CachedValue, 2e12);
assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2CachedValue, 2e12);
assertApproxEqAbs(psm.totalAssets(), vars.psmCachedValue, 2e12);
}

// Rounding is always in favour of the LPs
assertGe(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0StartingValue);
assertGe(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1StartingValue);
assertGe(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue);
assertGe(psm.totalAssets(), vars.psmStartingValue);

// Up to 2e12 rounding on each swap, for 1000 swaps
assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp0)), vars.lp0StartingValue, 2000e12);
assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp1)), vars.lp1StartingValue, 2000e12);
assertApproxEqAbs(psm.convertToAssetValue(psm.shares(lp2)), vars.lp2StartingValue, 2000e12);
assertApproxEqAbs(psm.totalAssets(), vars.psmStartingValue, 2000e12);
}

function _hash(uint256 number_, string memory salt) internal pure returns (uint256 hash_) {
hash_ = uint256(keccak256(abi.encode(number_, salt)));
}

function _getAsset(uint256 indexSeed) internal view returns (MockERC20) {
uint256 index = indexSeed % 3;

if (index == 0) return dai;
if (index == 1) return usdc;
if (index == 2) return sDai;

else revert("Invalid index");
}

}
Loading

0 comments on commit 039ae15

Please sign in to comment.