Skip to content

Commit

Permalink
feat: Add preview deposit withdraw tests (SC-499) (#26)
Browse files Browse the repository at this point in the history
* feat: add first tests

* fix: rm duplicate file

* feat: update preview deposit coverage

* feat: start on withdraw tests

* feat: unit tests complete

* feat: add fuzz test

* fix: update file names
  • Loading branch information
lucas-manuel authored Jul 29, 2024
1 parent a0c1153 commit b02926d
Show file tree
Hide file tree
Showing 4 changed files with 315 additions and 0 deletions.
1 change: 1 addition & 0 deletions test/invariant/handlers/RateSetterHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ contract RateSetterHandler is StdUtils {

setRateCount++;
}

}
131 changes: 131 additions & 0 deletions test/unit/PreviewDeposit.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.13;

import "forge-std/Test.sol";

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

contract PSMPreviewDeposit_FailureTests is PSMTestBase {

function test_previewDeposit_invalidAsset() public {
vm.expectRevert("PSM3/invalid-asset");
psm.previewDeposit(makeAddr("other-token"), 1);
}

}

contract PSMPreviewDeposit_SuccessTests is PSMTestBase {

address depositor = makeAddr("depositor");

function test_previewDeposit_dai_firstDeposit() public view {
assertEq(psm.previewDeposit(address(dai), 1), 1);
assertEq(psm.previewDeposit(address(dai), 2), 2);
assertEq(psm.previewDeposit(address(dai), 3), 3);

assertEq(psm.previewDeposit(address(dai), 1e18), 1e18);
assertEq(psm.previewDeposit(address(dai), 2e18), 2e18);
assertEq(psm.previewDeposit(address(dai), 3e18), 3e18);
}

function testFuzz_previewDeposit_dai_firstDeposit(uint256 amount) public view {
amount = _bound(amount, 0, DAI_TOKEN_MAX);
assertEq(psm.previewDeposit(address(dai), amount), amount);
}

function test_previewDeposit_usdc_firstDeposit() public view {
assertEq(psm.previewDeposit(address(usdc), 1), 1e12);
assertEq(psm.previewDeposit(address(usdc), 2), 2e12);
assertEq(psm.previewDeposit(address(usdc), 3), 3e12);

assertEq(psm.previewDeposit(address(usdc), 1e6), 1e18);
assertEq(psm.previewDeposit(address(usdc), 2e6), 2e18);
assertEq(psm.previewDeposit(address(usdc), 3e6), 3e18);
}

function testFuzz_previewDeposit_usdc_firstDeposit(uint256 amount) public view {
amount = _bound(amount, 0, USDC_TOKEN_MAX);
assertEq(psm.previewDeposit(address(usdc), amount), amount * 1e12);
}

function test_previewDeposit_sDai_firstDeposit() public view {
assertEq(psm.previewDeposit(address(sDai), 1), 1);
assertEq(psm.previewDeposit(address(sDai), 2), 2);
assertEq(psm.previewDeposit(address(sDai), 3), 3);
assertEq(psm.previewDeposit(address(sDai), 4), 5);

assertEq(psm.previewDeposit(address(sDai), 1e18), 1.25e18);
assertEq(psm.previewDeposit(address(sDai), 2e18), 2.50e18);
assertEq(psm.previewDeposit(address(sDai), 3e18), 3.75e18);
assertEq(psm.previewDeposit(address(sDai), 4e18), 5.00e18);
}

function testFuzz_previewDeposit_sDai_firstDeposit(uint256 amount) public view {
amount = _bound(amount, 0, SDAI_TOKEN_MAX);
assertEq(psm.previewDeposit(address(sDai), amount), amount * 1.25e27 / 1e27);
}

function test_previewDeposit_afterDepositsAndExchangeRateIncrease() public {
_assertOneToOne();

_deposit(address(dai), depositor, 1e18);
_assertOneToOne();

_deposit(address(usdc), depositor, 1e6);
_assertOneToOne();

_deposit(address(sDai), depositor, 0.8e18);
_assertOneToOne();

mockRateProvider.__setConversionRate(2e27);

// $300 dollars of value deposited, 300 shares minted.
// sDAI portion becomes worth $160, full pool worth $360, each share worth $1.20
// 1 USDC = 1/1.20 = 0.833...
assertEq(psm.previewDeposit(address(dai), 1e18), 0.833333333333333333e18);
assertEq(psm.previewDeposit(address(usdc), 1e6), 0.833333333333333333e18);
assertEq(psm.previewDeposit(address(sDai), 1e18), 1.666666666666666666e18); // 1 sDAI = $2
}

function testFuzz_previewDeposit_afterDepositsAndExchangeRateIncrease(
uint256 amount1,
uint256 amount2,
uint256 amount3,
uint256 conversionRate,
uint256 previewAmount
) public {
amount1 = _bound(amount1, 1, DAI_TOKEN_MAX);
amount2 = _bound(amount2, 1, USDC_TOKEN_MAX);
amount3 = _bound(amount3, 1, SDAI_TOKEN_MAX);
conversionRate = _bound(conversionRate, 1.00e27, 1000e27);
previewAmount = _bound(previewAmount, 0, DAI_TOKEN_MAX);

_assertOneToOne();

_deposit(address(dai), depositor, amount1);
_assertOneToOne();

_deposit(address(usdc), depositor, amount2);
_assertOneToOne();

_deposit(address(sDai), depositor, amount3);
_assertOneToOne();

mockRateProvider.__setConversionRate(conversionRate);

uint256 totalSharesMinted = amount1 + amount2 * 1e12 + amount3 * 1.25e27 / 1e27;
uint256 totalValue = amount1 + amount2 * 1e12 + amount3 * conversionRate / 1e27;
uint256 usdcPreviewAmount = previewAmount / 1e12;

assertEq(psm.previewDeposit(address(dai), previewAmount), previewAmount * totalSharesMinted / totalValue);
assertEq(psm.previewDeposit(address(usdc), usdcPreviewAmount), usdcPreviewAmount * 1e12 * totalSharesMinted / totalValue); // Divide then multiply to replicate rounding
assertEq(psm.previewDeposit(address(sDai), previewAmount), (previewAmount * conversionRate / 1e27) * totalSharesMinted / totalValue);
}

function _assertOneToOne() internal view {
assertEq(psm.previewDeposit(address(dai), 1e18), 1e18);
assertEq(psm.previewDeposit(address(usdc), 1e6), 1e18);
assertEq(psm.previewDeposit(address(sDai), 1e18), 1.25e18);
}

}
183 changes: 183 additions & 0 deletions test/unit/PreviewWIthdraw.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.13;

import "forge-std/Test.sol";

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

contract PSMPreviewWithdraw_FailureTests is PSMTestBase {

function test_previewWithdraw_invalidAsset() public {
vm.expectRevert("PSM3/invalid-asset");
psm.previewWithdraw(makeAddr("other-token"), 1);
}

}

contract PSMPreviewWithdraw_ZeroAssetsTests is PSMTestBase {

// Always returns zero because there is no balance of assets in the PSM in this case
function test_previewWithdraw_zeroTotalAssets() public {
( uint256 shares1, uint256 assets1 ) = psm.previewWithdraw(address(dai), 1e18);
( uint256 shares2, uint256 assets2 ) = psm.previewWithdraw(address(usdc), 1e6);
( uint256 shares3, uint256 assets3 ) = psm.previewWithdraw(address(sDai), 1e18);

assertEq(shares1, 0);
assertEq(assets1, 0);
assertEq(shares2, 0);
assertEq(assets2, 0);
assertEq(shares3, 0);
assertEq(assets3, 0);

mockRateProvider.__setConversionRate(2e27);

( shares1, assets1 ) = psm.previewWithdraw(address(dai), 1e18);
( shares2, assets2 ) = psm.previewWithdraw(address(usdc), 1e6);
( shares3, assets3 ) = psm.previewWithdraw(address(sDai), 1e18);

assertEq(shares1, 0);
assertEq(assets1, 0);
assertEq(shares2, 0);
assertEq(assets2, 0);
assertEq(shares3, 0);
assertEq(assets3, 0);
}

}

contract PSMPreviewWithdraw_SuccessTests is PSMTestBase {

function setUp() public override {
super.setUp();
// Setup so that address(this) has the most shares, higher underlying balance than PSM
// balance of sDAI and USDC
_deposit(address(dai), address(this), 100e18);
_deposit(address(usdc), makeAddr("usdc-user"), 10e6);
_deposit(address(sDai), makeAddr("sDai-user"), 1e18);
}

function test_previewWithdraw_dai_amountLtUnderlyingBalance() public view {
( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(dai), 100e18 - 1);
assertEq(shares, 100e18 - 1);
assertEq(assets, 100e18 - 1);
}

function test_previewWithdraw_dai_amountEqUnderlyingBalance() public view {
( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(dai), 100e18);
assertEq(shares, 100e18);
assertEq(assets, 100e18);
}

function test_previewWithdraw_dai_amountGtUnderlyingBalance() public view {
( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(dai), 100e18 + 1);
assertEq(shares, 100e18);
assertEq(assets, 100e18);
}

function test_previewWithdraw_usdc_amountLtUnderlyingBalanceAndLtPsmBalance() public view {
( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(usdc), 10e6 - 1);
assertEq(shares, 10e18 - 1e12);
assertEq(assets, 10e6 - 1);
}

function test_previewWithdraw_usdc_amountLtUnderlyingBalanceAndEqPsmBalance() public view {
( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(usdc), 10e6);
assertEq(shares, 10e18);
assertEq(assets, 10e6);
}

function test_previewWithdraw_usdc_amountLtUnderlyingBalanceAndGtPsmBalance() public view {
( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(usdc), 10e6 + 1);
assertEq(shares, 10e18);
assertEq(assets, 10e6);
}

function test_previewWithdraw_sdai_amountLtUnderlyingBalanceAndLtPsmBalance() public view {
( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(sDai), 1e18 - 1);
assertEq(shares, 1.25e18 - 2);
assertEq(assets, 1e18 - 1);
}

function test_previewWithdraw_sdai_amountLtUnderlyingBalanceAndEqPsmBalance() public view {
( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(sDai), 1e18);
assertEq(shares, 1.25e18);
assertEq(assets, 1e18);
}

function test_previewWithdraw_sdai_amountLtUnderlyingBalanceAndGtPsmBalance() public view {
( uint256 shares, uint256 assets ) = psm.previewWithdraw(address(sDai), 1e18 + 1);
assertEq(shares, 1.25e18);
assertEq(assets, 1e18);
}

}

contract PSMPreviewWithdraw_SuccessFuzzTests is PSMTestBase {

struct TestParams {
uint256 amount1;
uint256 amount2;
uint256 amount3;
uint256 previewAmount1;
uint256 previewAmount2;
uint256 previewAmount3;
uint256 conversionRate;
}

function testFuzz_previewWithdraw(TestParams memory params) public {
params.amount1 = _bound(params.amount1, 1, DAI_TOKEN_MAX);
params.amount2 = _bound(params.amount2, 1, USDC_TOKEN_MAX);
params.amount3 = _bound(params.amount3, 1, SDAI_TOKEN_MAX);

// Only covering case of amount being below underlying to focus on value conversion
// and avoid reimplementation of contract logic for dealing with capping amounts
params.previewAmount1 = _bound(params.previewAmount1, 0, params.amount1);
params.previewAmount2 = _bound(params.previewAmount2, 0, params.amount2);
params.previewAmount3 = _bound(params.previewAmount3, 0, params.amount3);

_deposit(address(dai), address(this), params.amount1);
_deposit(address(usdc), address(this), params.amount2);
_deposit(address(sDai), address(this), params.amount3);

( uint256 shares1, uint256 assets1 ) = psm.previewWithdraw(address(dai), params.previewAmount1);
( uint256 shares2, uint256 assets2 ) = psm.previewWithdraw(address(usdc), params.previewAmount2);
( uint256 shares3, uint256 assets3 ) = psm.previewWithdraw(address(sDai), params.previewAmount3);

uint256 totalSharesMinted = params.amount1 + params.amount2 * 1e12 + params.amount3 * 1.25e27 / 1e27;
uint256 totalValue = totalSharesMinted;

assertEq(shares1, params.previewAmount1 * totalSharesMinted / totalValue);
assertEq(shares2, params.previewAmount2 * 1e12 * totalSharesMinted / totalValue);
assertEq(shares3, params.previewAmount3 * 1.25e27 / 1e27 * totalSharesMinted / totalValue);

assertEq(assets1, params.previewAmount1);
assertEq(assets2, params.previewAmount2);
assertEq(assets3, params.previewAmount3);

params.conversionRate = _bound(params.conversionRate, 0.001e27, 1000e27);
mockRateProvider.__setConversionRate(params.conversionRate);

// sDai value accrual changes the value of shares in the PSM
totalValue = params.amount1 + params.amount2 * 1e12 + params.amount3 * params.conversionRate / 1e27;

( shares1, assets1 ) = psm.previewWithdraw(address(dai), params.previewAmount1);
( shares2, assets2 ) = psm.previewWithdraw(address(usdc), params.previewAmount2);
( shares3, assets3 ) = psm.previewWithdraw(address(sDai), params.previewAmount3);

uint256 sDaiConvertedAmount = params.previewAmount3 * params.conversionRate / 1e27;

assertApproxEqAbs(shares1, params.previewAmount1 * totalSharesMinted / totalValue, 1);
assertApproxEqAbs(shares2, params.previewAmount2 * 1e12 * totalSharesMinted / totalValue, 1);
assertApproxEqAbs(shares3, sDaiConvertedAmount * totalSharesMinted / totalValue, 1);

// Assert shares are always rounded up
assertGe(shares1, params.previewAmount1 * totalSharesMinted / totalValue);
assertGe(shares2, params.previewAmount2 * 1e12 * totalSharesMinted / totalValue);
assertGe(shares3, sDaiConvertedAmount * totalSharesMinted / totalValue);

assertApproxEqAbs(assets1, params.previewAmount1, 1);
assertApproxEqAbs(assets2, params.previewAmount2, 1);
assertApproxEqAbs(assets3, params.previewAmount3, 1);
}

}
File renamed without changes.

0 comments on commit b02926d

Please sign in to comment.