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: Add fuzz testing for swap functionality, add rounding up (SC-482) #22

Merged
merged 37 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8e20594
feat: update swap to swapExactAmountIn
lucas-manuel Jul 2, 2024
b3a2d67
feat: add preview test coverage
lucas-manuel Jul 2, 2024
1c42d99
feat: add new function
lucas-manuel Jul 3, 2024
278d99e
feat: create new file, failure tests passing
lucas-manuel Jul 4, 2024
54ca402
ci: add coverage backgp
lucas-manuel Jul 4, 2024
95c9654
Merge branch 'master' into sc-490-add-exact-amount-out-swap
lucas-manuel Jul 4, 2024
1a426da
Merge branch 'add-coverage-back' into sc-490-add-exact-amount-out-swap
lucas-manuel Jul 4, 2024
02074d0
feat: get new swap tests passing
lucas-manuel Jul 4, 2024
93220d5
feat: set up initial test
lucas-manuel Jul 4, 2024
7a9c76e
feat: refactor code to add returns
lucas-manuel Jul 4, 2024
8279856
feat: add return value assertions
lucas-manuel Jul 4, 2024
389c859
Merge branch 'sc-490-add-exact-amount-out-swap' into sc-482-swap-fuzz…
lucas-manuel Jul 4, 2024
2aabbf8
feat: working fuzz test
lucas-manuel Jul 4, 2024
e810447
feat: add swap exact out, show that rounding is against user
lucas-manuel Jul 4, 2024
3484433
fix: update natspec
lucas-manuel Jul 4, 2024
86cf19d
Merge branch 'sc-490-add-exact-amount-out-swap' into sc-482-swap-fuzz…
lucas-manuel Jul 4, 2024
d736ad1
feat: refactor to round up on swap exact out
lucas-manuel Jul 4, 2024
4bffb0e
feat: all tests passing
lucas-manuel Jul 4, 2024
2ec107e
fix: merge conflicts
lucas-manuel Jul 4, 2024
747eb61
Merge branch 'master' into add-coverage-back
lucas-manuel Jul 4, 2024
869ccdc
Merge branch 'add-coverage-back' into sc-490-add-exact-amount-out-swap
lucas-manuel Jul 4, 2024
113cd97
Merge branch 'sc-490-add-exact-amount-out-swap' into sc-482-swap-fuzz…
lucas-manuel Jul 4, 2024
132d11b
fix: merge conflicts
lucas-manuel Jul 4, 2024
c76f76c
Merge branch 'sc-490-add-exact-amount-out-swap' into sc-482-swap-fuzz…
lucas-manuel Jul 4, 2024
6b5b94b
fix: cleanup
lucas-manuel Jul 4, 2024
b3db93e
Merge branch 'sc-490-add-exact-amount-out-swap' into sc-482-swap-fuzz…
lucas-manuel Jul 4, 2024
226c8db
feat: update tolerances
lucas-manuel Jul 4, 2024
c30dc9e
Merge branch 'sc-490-add-exact-amount-out-swap' into sc-482-swap-fuzz…
lucas-manuel Jul 4, 2024
da5ce84
fix: update tolerance
lucas-manuel Jul 4, 2024
777b8e0
Merge branch 'sc-490-add-exact-amount-out-swap' into sc-482-swap-fuzz…
lucas-manuel Jul 4, 2024
057b057
fix: increase fuzz tolerance
lucas-manuel Jul 4, 2024
82aa91b
Merge branch 'sc-490-add-exact-amount-out-swap' into sc-482-swap-fuzz…
lucas-manuel Jul 4, 2024
5f7742a
fix: conflicts
lucas-manuel Jul 5, 2024
30edbaf
Merge branch 'master' into sc-482-swap-fuzz-testing
lucas-manuel Jul 5, 2024
00aea79
fix: updat to totalAssets
lucas-manuel Jul 5, 2024
52571e9
fix: review changes
lucas-manuel Jul 8, 2024
68d8a40
Merge branch 'master' into sc-482-swap-fuzz-testing
lucas-manuel Jul 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading