From 822694a26bea3d490f687492c86e2591a71990c3 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Sat, 18 May 2024 11:02:33 -0400 Subject: [PATCH 01/96] feat: first test working --- src/PSM.sol | 55 ++++++++++++++++++++++++++++++---- test/Constructor.t.sol | 8 ++--- test/Getters.t.sol | 2 +- test/InflationAttack.t.sol | 56 +++++++++++++++++++++++++++++++++++ test/PSMTestBase.sol | 4 +-- test/harnesses/PSMHarness.sol | 4 +-- 6 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 test/InflationAttack.t.sol diff --git a/src/PSM.sol b/src/PSM.sol index be49033..e43e52f 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; +import { console2 } from "forge-std/console2.sol"; + import { IERC20 } from "erc20-helpers/interfaces/IERC20.sol"; import { SafeERC20 } from "erc20-helpers/SafeERC20.sol"; @@ -30,12 +32,18 @@ contract PSM { uint256 public immutable asset0Precision; uint256 public immutable asset1Precision; + uint256 public immutable initialBurnAmount; uint256 public totalShares; mapping(address user => uint256 shares) public shares; - constructor(address asset0_, address asset1_, address rateProvider_) { + constructor( + address asset0_, + address asset1_, + address rateProvider_, + uint256 initialBurnAmount_ + ) { require(asset0_ != address(0), "PSM/invalid-asset0"); require(asset1_ != address(0), "PSM/invalid-asset1"); require(rateProvider_ != address(0), "PSM/invalid-rateProvider"); @@ -44,8 +52,9 @@ contract PSM { asset1 = IERC20(asset1_); rateProvider = rateProvider_; - asset0Precision = 10 ** IERC20(asset0_).decimals(); - asset1Precision = 10 ** IERC20(asset1_).decimals(); + asset0Precision = 10 ** IERC20(asset0_).decimals(); + asset1Precision = 10 ** IERC20(asset1_).decimals(); + initialBurnAmount = initialBurnAmount_; } /**********************************************************************************************/ @@ -86,6 +95,13 @@ contract PSM { // Convert amount to 1e18 precision denominated in value of asset0 then convert to shares. uint256 newShares = convertToShares(_getAssetValue(asset, assetsToDeposit)); + if (totalShares == 0 && initialBurnAmount != 0) { + shares[address(0)] += initialBurnAmount; + totalShares += initialBurnAmount; + + newShares -= initialBurnAmount; + } + shares[msg.sender] += newShares; totalShares += newShares; @@ -103,13 +119,21 @@ contract PSM { ? assetBalance : maxAssetsToWithdraw; - uint256 sharesToBurn = convertToShares(asset, assetsWithdrawn); + console2.log("assetsWithdrawn", assetsWithdrawn); + console2.log("assetBalance ", assetBalance); + + uint256 sharesToBurn = convertToSharesRoundUp(asset, assetsWithdrawn); + + console2.log("sharesToBurn ", sharesToBurn); if (sharesToBurn > shares[msg.sender]) { assetsWithdrawn = convertToAssets(asset, shares[msg.sender]); - sharesToBurn = convertToShares(asset, assetsWithdrawn); + sharesToBurn = convertToSharesRoundUp(asset, assetsWithdrawn); } + console2.log("assetsWithdrawn", assetsWithdrawn); + console2.log("sharesToBurn ", sharesToBurn); + unchecked { shares[msg.sender] -= sharesToBurn; totalShares -= sharesToBurn; @@ -122,19 +146,36 @@ contract PSM { /*** Conversion functions ***/ /**********************************************************************************************/ + // TODO: Refactor to use better naming function convertToShares(uint256 assetValue) public view returns (uint256) { uint256 totalValue = getPsmTotalValue(); + console2.log("totalValue ", totalValue); + console2.log("totalShares", totalShares); + console2.log("assetValue ", assetValue); if (totalValue != 0) { return assetValue * totalShares / totalValue; } return assetValue; } + function convertToSharesRoundUp(uint256 assetValue) public view returns (uint256) { + uint256 totalValue = getPsmTotalValue(); + if (totalValue != 0) { + return _divRoundUp(assetValue * totalShares, totalValue); + } + return assetValue; + } + function convertToShares(address asset, uint256 assets) public view returns (uint256) { require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); return convertToShares(_getAssetValue(asset, assets)); } + function convertToSharesRoundUp(address asset, uint256 assets) public view returns (uint256) { + require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); + return convertToSharesRoundUp(_getAssetValue(asset, assets)); + } + function convertToAssetValue(uint256 numShares) public view returns (uint256) { uint256 totalShares_ = totalShares; @@ -182,6 +223,10 @@ contract PSM { /*** Internal helper functions ***/ /**********************************************************************************************/ + function _divRoundUp(uint256 numerator_, uint256 divisor_) internal pure returns (uint256 result_) { + result_ = (numerator_ + divisor_ - 1) / divisor_; + } + function _getAssetValue(address asset, uint256 amount) internal view returns (uint256) { if (asset == address(asset0)) { return _getAsset0Value(amount); diff --git a/test/Constructor.t.sol b/test/Constructor.t.sol index cdec19d..d505441 100644 --- a/test/Constructor.t.sol +++ b/test/Constructor.t.sol @@ -11,22 +11,22 @@ contract PSMConstructorTests is PSMTestBase { function test_constructor_invalidAsset0() public { vm.expectRevert("PSM/invalid-asset0"); - new PSM(address(0), address(sDai), address(rateProvider)); + new PSM(address(0), address(sDai), address(rateProvider), 1000); } function test_constructor_invalidAsset1() public { vm.expectRevert("PSM/invalid-asset1"); - new PSM(address(usdc), address(0), address(rateProvider)); + new PSM(address(usdc), address(0), address(rateProvider), 1000); } function test_constructor_invalidRateProvider() public { vm.expectRevert("PSM/invalid-rateProvider"); - new PSM(address(sDai), address(usdc), address(0)); + new PSM(address(sDai), address(usdc), address(0), 1000); } function test_constructor() public { // Deploy new PSM to get test coverage - psm = new PSM(address(usdc), address(sDai), address(rateProvider)); + psm = new PSM(address(usdc), address(sDai), address(rateProvider), 1000); assertEq(address(psm.asset0()), address(usdc)); assertEq(address(psm.asset1()), address(sDai)); diff --git a/test/Getters.t.sol b/test/Getters.t.sol index 911ff91..895f445 100644 --- a/test/Getters.t.sol +++ b/test/Getters.t.sol @@ -13,7 +13,7 @@ contract PSMHarnessTests is PSMTestBase { function setUp() public override { super.setUp(); - psmHarness = new PSMHarness(address(usdc), address(sDai), address(rateProvider)); + psmHarness = new PSMHarness(address(usdc), address(sDai), address(rateProvider), 1000); } function test_getAsset0Value() public view { diff --git a/test/InflationAttack.t.sol b/test/InflationAttack.t.sol new file mode 100644 index 0000000..3bdcbe6 --- /dev/null +++ b/test/InflationAttack.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import { PSM } from "src/PSM.sol"; + +import { PSMTestBase } from "test/PSMTestBase.sol"; + +contract InflationAttackTests is PSMTestBase { + + function test_inflationAttack_noInitialBurnAmount() public { + psm = new PSM(address(usdc), address(sDai), address(rateProvider), 0); + + address firstDepositor = makeAddr("firstDepositor"); + address frontRunner = makeAddr("frontRunner"); + + // Step 1: Front runner deposits 1 sDAI to get 1 share + + // Have to use sDai because 1 USDC mints 1e12 shares + _deposit(frontRunner, address(sDai), 1); + + assertEq(psm.shares(frontRunner), 1); + + // Step 2: Front runner transfers 1m USDC to inflate the exchange rate to 1:(1m + 1) + + deal(address(usdc), frontRunner, 1_000_000e6); + + vm.prank(frontRunner); + usdc.transfer(address(psm), 1_000_000e6); + + // Highly inflated exchange rate + assertEq(psm.convertToAssetValue(1), 1_000_000e18 + 1); + + // Step 3: First depositor deposits 2 million USDC, only gets one share because rounding + // error gives them 1 instead of 2 shares, worth 1.5m USDC + + _deposit(firstDepositor, address(usdc), 2_000_000e6); + + assertEq(psm.shares(firstDepositor), 1); + + // 1 share = 3 million USDC / 2 shares = 1.5 million USDC + assertEq(psm.convertToAssetValue(1), 1_500_000e18); + + // Step 4: Both users withdraw the max amount of funds they can + + _withdraw(firstDepositor, address(usdc), type(uint256).max); + _withdraw(frontRunner, address(usdc), type(uint256).max); + + assertEq(usdc.balanceOf(address(psm)), 0); + + // Front runner profits 500k USDC, first depositor loses 500k USDC + assertEq(usdc.balanceOf(firstDepositor), 1_500_000e6); + assertEq(usdc.balanceOf(frontRunner), 1_500_000e6); + } +} diff --git a/test/PSMTestBase.sol b/test/PSMTestBase.sol index 23cc97c..8675d7c 100644 --- a/test/PSMTestBase.sol +++ b/test/PSMTestBase.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "../src/PSM.sol"; +import { PSM } from "src/PSM.sol"; import { MockERC20 } from "erc20-helpers/MockERC20.sol"; @@ -38,7 +38,7 @@ contract PSMTestBase is Test { // NOTE: Using 1.25 for easy two way conversions rateProvider.__setConversionRate(1.25e27); - psm = new PSM(address(usdc), address(sDai), address(rateProvider)); + psm = new PSM(address(usdc), address(sDai), address(rateProvider), 1000); vm.label(address(sDai), "sDAI"); vm.label(address(usdc), "USDC"); diff --git a/test/harnesses/PSMHarness.sol b/test/harnesses/PSMHarness.sol index 6a5eaa7..48dfcf9 100644 --- a/test/harnesses/PSMHarness.sol +++ b/test/harnesses/PSMHarness.sol @@ -5,8 +5,8 @@ import { PSM } from "src/PSM.sol"; contract PSMHarness is PSM { - constructor(address asset0_, address asset1_, address rateProvider_) - PSM(asset0_, asset1_, rateProvider_) {} + constructor(address asset0_, address asset1_, address rateProvider_, uint256 initialBurnAmount_) + PSM(asset0_, asset1_, rateProvider_, initialBurnAmount_) {} function getAssetValue(address asset, uint256 amount) external view returns (uint256) { return _getAssetValue(asset, amount); From d6530e1f275b5d3f61f35dfe7f9f4f7287ff4942 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Sat, 18 May 2024 11:03:52 -0400 Subject: [PATCH 02/96] feat: use larger numbers: --- test/InflationAttack.t.sol | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/InflationAttack.t.sol b/test/InflationAttack.t.sol index 3bdcbe6..7214651 100644 --- a/test/InflationAttack.t.sol +++ b/test/InflationAttack.t.sol @@ -22,25 +22,25 @@ contract InflationAttackTests is PSMTestBase { assertEq(psm.shares(frontRunner), 1); - // Step 2: Front runner transfers 1m USDC to inflate the exchange rate to 1:(1m + 1) + // Step 2: Front runner transfers 10m USDC to inflate the exchange rate to 1:(10m + 1) - deal(address(usdc), frontRunner, 1_000_000e6); + deal(address(usdc), frontRunner, 10_000_000e6); vm.prank(frontRunner); - usdc.transfer(address(psm), 1_000_000e6); + usdc.transfer(address(psm), 10_000_000e6); // Highly inflated exchange rate - assertEq(psm.convertToAssetValue(1), 1_000_000e18 + 1); + assertEq(psm.convertToAssetValue(1), 10_000_000e18 + 1); - // Step 3: First depositor deposits 2 million USDC, only gets one share because rounding - // error gives them 1 instead of 2 shares, worth 1.5m USDC + // Step 3: First depositor deposits 20 million USDC, only gets one share because rounding + // error gives them 1 instead of 2 shares, worth 15m USDC - _deposit(firstDepositor, address(usdc), 2_000_000e6); + _deposit(firstDepositor, address(usdc), 20_000_000e6); assertEq(psm.shares(firstDepositor), 1); // 1 share = 3 million USDC / 2 shares = 1.5 million USDC - assertEq(psm.convertToAssetValue(1), 1_500_000e18); + assertEq(psm.convertToAssetValue(1), 15_000_000e18); // Step 4: Both users withdraw the max amount of funds they can @@ -49,8 +49,8 @@ contract InflationAttackTests is PSMTestBase { assertEq(usdc.balanceOf(address(psm)), 0); - // Front runner profits 500k USDC, first depositor loses 500k USDC - assertEq(usdc.balanceOf(firstDepositor), 1_500_000e6); - assertEq(usdc.balanceOf(frontRunner), 1_500_000e6); + // Front runner profits 5m USDC, first depositor loses 5m USDC + assertEq(usdc.balanceOf(firstDepositor), 15_000_000e6); + assertEq(usdc.balanceOf(frontRunner), 15_000_000e6); } } From 8f11df43f407314ab3ca67946713e27496a656ee Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Sat, 18 May 2024 11:18:59 -0400 Subject: [PATCH 03/96] feat: test with initial burn amount passing --- test/InflationAttack.t.sol | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/test/InflationAttack.t.sol b/test/InflationAttack.t.sol index 7214651..a21999c 100644 --- a/test/InflationAttack.t.sol +++ b/test/InflationAttack.t.sol @@ -53,4 +53,68 @@ contract InflationAttackTests is PSMTestBase { assertEq(usdc.balanceOf(firstDepositor), 15_000_000e6); assertEq(usdc.balanceOf(frontRunner), 15_000_000e6); } + + function test_inflationAttack_useInitialBurnAmount_firstDepositOverflowBoundary() public { + psm = new PSM(address(usdc), address(sDai), address(rateProvider), 1000); + + address frontRunner = makeAddr("frontRunner"); + + vm.startPrank(frontRunner); + sDai.mint(frontRunner, 800); + sDai.approve(address(psm), 800); + + vm.expectRevert(stdError.arithmeticError); + psm.deposit(address(sDai), 799); + + // 800 sDAI = 1000 shares + psm.deposit(address(sDai), 800); + } + + function test_inflationAttack_useInitialBurnAmount() public { + psm = new PSM(address(usdc), address(sDai), address(rateProvider), 1000); + + address firstDepositor = makeAddr("firstDepositor"); + address frontRunner = makeAddr("frontRunner"); + + // Step 1: Front runner deposits 801 sDAI to get 1 share + + // 1000 shares get burned, user is left with 1 + _deposit(frontRunner, address(sDai), 801); + + assertEq(psm.shares(frontRunner), 1); + + // Step 2: Front runner transfers 10m USDC to inflate the exchange rate to 1:(10m + 1) + + deal(address(usdc), frontRunner, 10_000_000e6); + + vm.prank(frontRunner); + usdc.transfer(address(psm), 10_000_000e6); + + // Much less inflated exchange rate + assertEq(psm.convertToAssetValue(1), 9990.009990009990009991e18); + + // Step 3: First depositor deposits 20 million USDC, only gets one share because rounding + // error gives them 1 instead of 2 shares, worth 15m USDC + + _deposit(firstDepositor, address(usdc), 20_000_000e6); + + assertEq(psm.shares(firstDepositor), 2001); + + // Higher amount of initial shares means lower rounding error + assertEq(psm.convertToAssetValue(2001), 19_996_668.887408394403731513e18); + + // Step 4: Both users withdraw the max amount of funds they can + + _withdraw(firstDepositor, address(usdc), type(uint256).max); + _withdraw(frontRunner, address(usdc), type(uint256).max); + + // Burnt shares have a claim on these + // TODO: Should this be an admin contract instead of address(0)? + assertEq(usdc.balanceOf(address(psm)), 9_993_337.774818e6); + + // Front runner loses 999k USDC, first depositor loses 4k USDC + assertEq(usdc.balanceOf(firstDepositor), 19_996_668.887408e6); + assertEq(usdc.balanceOf(frontRunner), 9_993.337774e6); + } + } From 78fb4ad59a8b2d8a14a369f105f9cddf0d5b7d14 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Tue, 21 May 2024 10:47:48 -0400 Subject: [PATCH 04/96] feat: update tests to work with updated burn logic, move conversion functions around and use previews --- src/PSM.sol | 145 +++++++++++++++++++------------------ test/Deposit.t.sol | 40 ++++++---- test/InflationAttack.t.sol | 2 +- test/Withdraw.t.sol | 79 +++++++++++++------- 4 files changed, 152 insertions(+), 114 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index e43e52f..604dfa1 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -89,11 +89,10 @@ contract PSM { /*** Liquidity provision functions ***/ /**********************************************************************************************/ - function deposit(address asset, uint256 assetsToDeposit) external { - require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); - - // Convert amount to 1e18 precision denominated in value of asset0 then convert to shares. - uint256 newShares = convertToShares(_getAssetValue(asset, assetsToDeposit)); + function deposit(address asset, uint256 assetsToDeposit) + external returns (uint256 newShares) + { + newShares = previewDeposit(asset, assetsToDeposit); if (totalShares == 0 && initialBurnAmount != 0) { shares[address(0)] += initialBurnAmount; @@ -110,6 +109,32 @@ contract PSM { function withdraw(address asset, uint256 maxAssetsToWithdraw) external returns (uint256 assetsWithdrawn) + { + uint256 sharesToBurn; + + ( sharesToBurn, assetsWithdrawn ) = previewWithdraw(asset, maxAssetsToWithdraw); + + unchecked { + shares[msg.sender] -= sharesToBurn; + totalShares -= sharesToBurn; + } + + IERC20(asset).safeTransfer(msg.sender, assetsWithdrawn); + } + + /**********************************************************************************************/ + /*** Deposit/withdraw preview functions ***/ + /**********************************************************************************************/ + + function previewDeposit(address asset, uint256 assets) public view returns (uint256) { + require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); + + // Convert amount to 1e18 precision denominated in value of asset0 then convert to shares. + return convertToShares(_getAssetValue(asset, assets)); + } + + function previewWithdraw(address asset, uint256 maxAssetsToWithdraw) + public view returns (uint256 sharesToBurn, uint256 assetsWithdrawn) { require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); @@ -119,61 +144,41 @@ contract PSM { ? assetBalance : maxAssetsToWithdraw; - console2.log("assetsWithdrawn", assetsWithdrawn); - console2.log("assetBalance ", assetBalance); - - uint256 sharesToBurn = convertToSharesRoundUp(asset, assetsWithdrawn); - - console2.log("sharesToBurn ", sharesToBurn); + sharesToBurn = _convertToSharesRoundUp(_getAssetValue(asset, assetsWithdrawn)); if (sharesToBurn > shares[msg.sender]) { assetsWithdrawn = convertToAssets(asset, shares[msg.sender]); - sharesToBurn = convertToSharesRoundUp(asset, assetsWithdrawn); + sharesToBurn = _convertToSharesRoundUp(_getAssetValue(asset, assetsWithdrawn)); // TODO: This can cause an underflow, refactor to use full shares balance? } - - console2.log("assetsWithdrawn", assetsWithdrawn); - console2.log("sharesToBurn ", sharesToBurn); - - unchecked { - shares[msg.sender] -= sharesToBurn; - totalShares -= sharesToBurn; - } - - IERC20(asset).safeTransfer(msg.sender, assetsWithdrawn); } /**********************************************************************************************/ - /*** Conversion functions ***/ + /*** Swap preview functions ***/ /**********************************************************************************************/ - // TODO: Refactor to use better naming - function convertToShares(uint256 assetValue) public view returns (uint256) { - uint256 totalValue = getPsmTotalValue(); - console2.log("totalValue ", totalValue); - console2.log("totalShares", totalShares); - console2.log("assetValue ", assetValue); - if (totalValue != 0) { - return assetValue * totalShares / totalValue; - } - return assetValue; + function previewSwapAssetZeroToOne(uint256 amountIn) public view returns (uint256) { + return amountIn + * 1e27 + * asset1Precision + / IRateProviderLike(rateProvider).getConversionRate() + / asset0Precision; } - function convertToSharesRoundUp(uint256 assetValue) public view returns (uint256) { - uint256 totalValue = getPsmTotalValue(); - if (totalValue != 0) { - return _divRoundUp(assetValue * totalShares, totalValue); - } - return assetValue; + function previewSwapAssetOneToZero(uint256 amountIn) public view returns (uint256) { + return amountIn + * IRateProviderLike(rateProvider).getConversionRate() + * asset0Precision + / 1e27 + / asset1Precision; } - function convertToShares(address asset, uint256 assets) public view returns (uint256) { - require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); - return convertToShares(_getAssetValue(asset, assets)); - } + /**********************************************************************************************/ + /*** Conversion functions ***/ + /**********************************************************************************************/ - function convertToSharesRoundUp(address asset, uint256 assets) public view returns (uint256) { + function convertToAssets(address asset, uint256 numShares) public view returns (uint256) { require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); - return convertToSharesRoundUp(_getAssetValue(asset, assets)); + return _getAssetsByValue(asset, convertToAssetValue(numShares)); } function convertToAssetValue(uint256 numShares) public view returns (uint256) { @@ -185,9 +190,17 @@ contract PSM { return numShares; } - function convertToAssets(address asset, uint256 numShares) public view returns (uint256) { + function convertToShares(uint256 assetValue) public view returns (uint256) { + uint256 totalValue = getPsmTotalValue(); + if (totalValue != 0) { + return assetValue * totalShares / totalValue; + } + return assetValue; + } + + function convertToShares(address asset, uint256 assets) public view returns (uint256) { require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); - return _getAssetsByValue(asset, convertToAssetValue(numShares)); + return convertToShares(_getAssetValue(asset, assets)); } /**********************************************************************************************/ @@ -200,39 +213,27 @@ contract PSM { } /**********************************************************************************************/ - /*** Swap preview functions ***/ + /*** Internal helper functions ***/ /**********************************************************************************************/ - function previewSwapAssetZeroToOne(uint256 amountIn) public view returns (uint256) { - return amountIn - * 1e27 - * asset1Precision - / IRateProviderLike(rateProvider).getConversionRate() - / asset0Precision; - } - - function previewSwapAssetOneToZero(uint256 amountIn) public view returns (uint256) { - return amountIn - * IRateProviderLike(rateProvider).getConversionRate() - * asset0Precision - / 1e27 - / asset1Precision; + function _convertToSharesRoundUp(uint256 assetValue) internal view returns (uint256) { + uint256 totalValue = getPsmTotalValue(); + if (totalValue != 0) { + return _divRoundUp(assetValue * totalShares, totalValue); + } + return assetValue; } - /**********************************************************************************************/ - /*** Internal helper functions ***/ - /**********************************************************************************************/ - - function _divRoundUp(uint256 numerator_, uint256 divisor_) internal pure returns (uint256 result_) { + function _divRoundUp(uint256 numerator_, uint256 divisor_) + internal pure returns (uint256 result_) + { result_ = (numerator_ + divisor_ - 1) / divisor_; } function _getAssetValue(address asset, uint256 amount) internal view returns (uint256) { - if (asset == address(asset0)) { - return _getAsset0Value(amount); - } - - return _getAsset1Value(amount); + return asset == address(asset0) + ? _getAsset0Value(amount) + : _getAsset1Value(amount); } function _getAssetsByValue(address asset, uint256 assetValue) internal view returns (uint256) { diff --git a/test/Deposit.t.sol b/test/Deposit.t.sol index 24ce76a..f9924f8 100644 --- a/test/Deposit.t.sol +++ b/test/Deposit.t.sol @@ -11,6 +11,9 @@ contract PSMDepositTests is PSMTestBase { address user1 = makeAddr("user1"); address user2 = makeAddr("user2"); + address burn = address(0); + + uint256 BURN_AMOUNT = 1000; function test_deposit_notAsset0OrAsset1() public { vm.expectRevert("PSM/invalid-asset"); @@ -32,6 +35,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.totalShares(), 0); assertEq(psm.shares(user1), 0); + assertEq(psm.shares(burn), 0); assertEq(psm.convertToShares(1e18), 1e18); @@ -42,7 +46,8 @@ contract PSMDepositTests is PSMTestBase { assertEq(usdc.balanceOf(address(psm)), 100e6); assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 100e18); + assertEq(psm.shares(user1), 100e18 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); } @@ -60,6 +65,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.totalShares(), 0); assertEq(psm.shares(user1), 0); + assertEq(psm.shares(burn), 0); assertEq(psm.convertToShares(1e18), 1e18); @@ -70,7 +76,8 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 100e18); assertEq(psm.totalShares(), 125e18); - assertEq(psm.shares(user1), 125e18); + assertEq(psm.shares(user1), 125e18 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); } @@ -94,7 +101,8 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 0); assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 100e18); + assertEq(psm.shares(user1), 100e18 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); @@ -107,14 +115,16 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 100e18); assertEq(psm.totalShares(), 225e18); - assertEq(psm.shares(user1), 225e18); + assertEq(psm.shares(user1), 225e18 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); // Only burn on first deposit assertEq(psm.convertToShares(1e18), 1e18); } function testFuzz_deposit_usdcThenSDai(uint256 usdcAmount, uint256 sDaiAmount) public { - usdcAmount = _bound(usdcAmount, 0, USDC_TOKEN_MAX); - sDaiAmount = _bound(sDaiAmount, 0, SDAI_TOKEN_MAX); + // NOTE: Deposits revert if deposit amount is less than the burn amount + usdcAmount = _bound(usdcAmount, BURN_AMOUNT, USDC_TOKEN_MAX); + sDaiAmount = _bound(sDaiAmount, BURN_AMOUNT, SDAI_TOKEN_MAX); usdc.mint(user1, usdcAmount); @@ -134,7 +144,8 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 0); assertEq(psm.totalShares(), usdcAmount * 1e12); - assertEq(psm.shares(user1), usdcAmount * 1e12); + assertEq(psm.shares(user1), usdcAmount * 1e12 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); @@ -147,7 +158,8 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), sDaiAmount); assertEq(psm.totalShares(), usdcAmount * 1e12 + sDaiAmount * 125/100); - assertEq(psm.shares(user1), usdcAmount * 1e12 + sDaiAmount * 125/100); + assertEq(psm.shares(user1), usdcAmount * 1e12 + sDaiAmount * 125/100 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); // Only burn on first deposit assertEq(psm.convertToShares(1e18), 1e18); } @@ -175,11 +187,12 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 100e18); assertEq(psm.totalShares(), 225e18); - assertEq(psm.shares(user1), 225e18); + assertEq(psm.shares(user1), 225e18 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); - assertEq(psm.convertToAssetValue(psm.shares(user1)), 225e18); + assertEq(psm.convertToAssetValue(psm.shares(user1)), 225e18 - BURN_AMOUNT); rateProvider.__setConversionRate(1.5e27); @@ -199,7 +212,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(user2), 100e18); assertEq(sDai.balanceOf(address(psm)), 100e18); - assertEq(psm.convertToAssetValue(psm.shares(user1)), 250e18); + assertEq(psm.convertToAssetValue(psm.shares(user1)), 250e18 - 1112); // Burn amount conversion assertEq(psm.convertToAssetValue(psm.shares(user2)), 0); assertEq(psm.getPsmTotalValue(), 250e18); @@ -218,11 +231,12 @@ contract PSMDepositTests is PSMTestBase { assertEq(expectedShares, 135e18); assertEq(psm.totalShares(), 360e18); - assertEq(psm.shares(user1), 225e18); + assertEq(psm.shares(user1), 225e18 - BURN_AMOUNT); assertEq(psm.shares(user2), 135e18); + assertEq(psm.shares(burn), BURN_AMOUNT); // User 1 earned $25 on 225, User 2 has earned nothing - assertEq(psm.convertToAssetValue(psm.shares(user1)), 250e18); + assertEq(psm.convertToAssetValue(psm.shares(user1)), 250e18 - 1112); assertEq(psm.convertToAssetValue(psm.shares(user2)), 150e18); assertEq(psm.getPsmTotalValue(), 400e18); diff --git a/test/InflationAttack.t.sol b/test/InflationAttack.t.sol index a21999c..9f0e189 100644 --- a/test/InflationAttack.t.sol +++ b/test/InflationAttack.t.sol @@ -112,7 +112,7 @@ contract InflationAttackTests is PSMTestBase { // TODO: Should this be an admin contract instead of address(0)? assertEq(usdc.balanceOf(address(psm)), 9_993_337.774818e6); - // Front runner loses 999k USDC, first depositor loses 4k USDC + // Front runner loses 9.99m USDC, first depositor loses 4k USDC assertEq(usdc.balanceOf(firstDepositor), 19_996_668.887408e6); assertEq(usdc.balanceOf(frontRunner), 9_993.337774e6); } diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index 50eeb78..421db3f 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -13,6 +13,9 @@ contract PSMWithdrawTests is PSMTestBase { address user1 = makeAddr("user1"); address user2 = makeAddr("user2"); + address burn = address(0); + + uint256 BURN_AMOUNT = 1000; function test_withdraw_notAsset0OrAsset1() public { vm.expectRevert("PSM/invalid-asset"); @@ -28,20 +31,24 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(usdc.balanceOf(address(psm)), 100e6); assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 100e18); + assertEq(psm.shares(user1), 100e18 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); uint256 amount = psm.withdraw(address(usdc), 100e6); - assertEq(amount, 100e6); + // Burn amount causes shares to round down by one since shares are 99.999... + assertEq(amount, 100e6 - 1); - assertEq(usdc.balanceOf(user1), 100e6); - assertEq(usdc.balanceOf(address(psm)), 0); + assertEq(usdc.balanceOf(user1), 100e6 - 1); + assertEq(usdc.balanceOf(address(psm)), 1); - assertEq(psm.totalShares(), 0); - assertEq(psm.shares(user1), 0); + // User still has left over shares from rounding on 1e6 + assertEq(psm.totalShares(), 1e12); + assertEq(psm.shares(user1), 1e12 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); } @@ -53,20 +60,25 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 80e18); assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 100e18); + assertEq(psm.shares(user1), 100e18 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); + + // This is the amount that this user will not be able to withdraw + assertEq(psm.convertToAssets(address(sDai), BURN_AMOUNT), 800); assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); uint256 amount = psm.withdraw(address(sDai), 80e18); - assertEq(amount, 80e18); + assertEq(amount, 80e18 - 800); - assertEq(sDai.balanceOf(user1), 80e18); - assertEq(sDai.balanceOf(address(psm)), 0); + assertEq(sDai.balanceOf(user1), 80e18 - 800); + assertEq(sDai.balanceOf(address(psm)), 800); - assertEq(psm.totalShares(), 0); + assertEq(psm.totalShares(), BURN_AMOUNT); assertEq(psm.shares(user1), 0); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); } @@ -82,7 +94,8 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 100e18); assertEq(psm.totalShares(), 225e18); - assertEq(psm.shares(user1), 225e18); + assertEq(psm.shares(user1), 225e18 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); @@ -98,23 +111,28 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 100e18); assertEq(psm.totalShares(), 125e18); - assertEq(psm.shares(user1), 125e18); + assertEq(psm.shares(user1), 125e18 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); + // This is the amount that this user will not be able to withdraw + assertEq(psm.convertToAssets(address(sDai), BURN_AMOUNT), 800); + vm.prank(user1); amount = psm.withdraw(address(sDai), 100e18); - assertEq(amount, 100e18); + assertEq(amount, 100e18 - 800); assertEq(usdc.balanceOf(user1), 100e6); assertEq(usdc.balanceOf(address(psm)), 0); - assertEq(sDai.balanceOf(user1), 100e18); - assertEq(sDai.balanceOf(address(psm)), 0); + assertEq(sDai.balanceOf(user1), 100e18 - 800); + assertEq(sDai.balanceOf(address(psm)), 800); - assertEq(psm.totalShares(), 0); + assertEq(psm.totalShares(), BURN_AMOUNT); assertEq(psm.shares(user1), 0); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); } @@ -127,7 +145,8 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(usdc.balanceOf(address(psm)), 100e6); assertEq(psm.totalShares(), 225e18); - assertEq(psm.shares(user1), 225e18); + assertEq(psm.shares(user1), 225e18 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); @@ -140,7 +159,8 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(usdc.balanceOf(address(psm)), 0); assertEq(psm.totalShares(), 125e18); // Only burns $100 of shares - assertEq(psm.shares(user1), 125e18); + assertEq(psm.shares(user1), 125e18 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); } function test_withdraw_amountHigherThanUserShares() public { @@ -178,11 +198,10 @@ contract PSMWithdrawTests is PSMTestBase { ) public { - // NOTE: Not covering zero cases, 1e-2 at 1e6 used as min for now so exact values can - // be asserted - depositAmount1 = bound(depositAmount1, 0, USDC_TOKEN_MAX); - depositAmount2 = bound(depositAmount2, 0, USDC_TOKEN_MAX); - depositAmount3 = bound(depositAmount3, 0, SDAI_TOKEN_MAX); + // NOTE: Deposits revert if deposit amount is less than the burn amount + depositAmount1 = bound(depositAmount1, BURN_AMOUNT, USDC_TOKEN_MAX); + depositAmount2 = bound(depositAmount2, BURN_AMOUNT, USDC_TOKEN_MAX); + depositAmount3 = bound(depositAmount3, BURN_AMOUNT, SDAI_TOKEN_MAX); withdrawAmount1 = bound(withdrawAmount1, 0, USDC_TOKEN_MAX); withdrawAmount2 = bound(withdrawAmount2, 0, USDC_TOKEN_MAX); @@ -198,7 +217,8 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(usdc.balanceOf(user1), 0); assertEq(usdc.balanceOf(address(psm)), totalUsdc); - assertEq(psm.shares(user1), depositAmount1 * 1e12); + assertEq(psm.shares(user1), depositAmount1 * 1e12 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.totalShares(), totalValue); uint256 expectedWithdrawnAmount1 @@ -220,8 +240,9 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(usdc.balanceOf(user2), 0); assertEq(usdc.balanceOf(address(psm)), totalUsdc - expectedWithdrawnAmount1); - assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); + assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12 - BURN_AMOUNT); assertEq(psm.shares(user2), depositAmount2 * 1e12 + depositAmount3 * 125/100); // Includes sDAI deposit + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.totalShares(), totalValue - expectedWithdrawnAmount1 * 1e12); uint256 expectedWithdrawnAmount2 @@ -246,7 +267,8 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(sDai.balanceOf(user2), 0); assertEq(sDai.balanceOf(address(psm)), depositAmount3); - assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); + assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertEq( psm.shares(user2), @@ -280,7 +302,8 @@ contract PSMWithdrawTests is PSMTestBase { assertApproxEqAbs(sDai.balanceOf(user2), expectedWithdrawnAmount3, 1); assertApproxEqAbs(sDai.balanceOf(address(psm)), depositAmount3 - expectedWithdrawnAmount3, 1); - assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); + assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12 - BURN_AMOUNT); + assertEq(psm.shares(burn), BURN_AMOUNT); assertApproxEqAbs( psm.shares(user2), From d32fd2faf5d3d255075f4eef9d6639f1118f5eb3 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Tue, 21 May 2024 11:10:08 -0400 Subject: [PATCH 05/96] feat: remove todos --- src/PSM.sol | 7 +++---- test/Withdraw.t.sol | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index 604dfa1..ff39629 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -16,9 +16,6 @@ interface IRateProviderLike { // TODO: Refactor into inheritance structure // TODO: Add interface with natspec and inherit // TODO: Prove that we're always rounding against user -// TODO: Frontrunning attack, donation attack, virtual balances? -// TODO: Figure out how to optimize require checks for assets in view functions -// TODO: Discuss if we should add ERC20 functionality contract PSM { using SafeERC20 for IERC20; @@ -146,9 +143,11 @@ contract PSM { sharesToBurn = _convertToSharesRoundUp(_getAssetValue(asset, assetsWithdrawn)); + // TODO: Refactor this section to not use convertToAssets because of redundant check + // TODO: This can cause an underflow in shares, refactor to use full shares balance? if (sharesToBurn > shares[msg.sender]) { assetsWithdrawn = convertToAssets(asset, shares[msg.sender]); - sharesToBurn = _convertToSharesRoundUp(_getAssetValue(asset, assetsWithdrawn)); // TODO: This can cause an underflow, refactor to use full shares balance? + sharesToBurn = _convertToSharesRoundUp(_getAssetValue(asset, assetsWithdrawn)); } } diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index 421db3f..fb84371 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -357,7 +357,7 @@ contract PSMWithdrawTests is PSMTestBase { userAssets = userAssets * 1e27 / rateProvider.getConversionRate(); } - // Return the min of + // Return the min of assets, balance, and amount withdrawAmount = userAssets < balance ? userAssets : balance; withdrawAmount = amount < withdrawAmount ? amount : withdrawAmount; } From 2290417607390681c553a68cf2194f31951b567f Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Tue, 21 May 2024 13:07:15 -0400 Subject: [PATCH 06/96] fix: update to remove console and update comment --- src/PSM.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index ff39629..e5b363c 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import { console2 } from "forge-std/console2.sol"; - import { IERC20 } from "erc20-helpers/interfaces/IERC20.sol"; import { SafeERC20 } from "erc20-helpers/SafeERC20.sol"; @@ -144,7 +142,7 @@ contract PSM { sharesToBurn = _convertToSharesRoundUp(_getAssetValue(asset, assetsWithdrawn)); // TODO: Refactor this section to not use convertToAssets because of redundant check - // TODO: This can cause an underflow in shares, refactor to use full shares balance? + // TODO: Can this cause an underflow in shares? Refactor to use full shares balance? if (sharesToBurn > shares[msg.sender]) { assetsWithdrawn = convertToAssets(asset, shares[msg.sender]); sharesToBurn = _convertToSharesRoundUp(_getAssetValue(asset, assetsWithdrawn)); From 1fe351b46f6583a944236deb27beb37b233bd32d Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 10:42:10 -0400 Subject: [PATCH 07/96] feat: get swap tests working --- src/PSM.sol | 114 +++++++--- test/Constructor.t.sol | 23 +- test/Getters.t.sol | 8 +- test/InflationAttack.t.sol | 6 +- test/PSMTestBase.sol | 18 +- test/Previews.t.sol | 142 ++++++------ test/Swaps.t.sol | 408 +++++++++++++++++++++------------- test/harnesses/PSMHarness.sol | 9 +- 8 files changed, 448 insertions(+), 280 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index e5b363c..3f23489 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; +import { console2 } from "forge-std/console2.sol"; + import { IERC20 } from "erc20-helpers/interfaces/IERC20.sol"; import { SafeERC20 } from "erc20-helpers/SafeERC20.sol"; @@ -18,15 +20,17 @@ contract PSM { using SafeERC20 for IERC20; - // NOTE: Assumption is made that asset1 is the yield-bearing counterpart of asset0. - // Examples: asset0 = USDC and asset1 = sDAI, asset0 = WETH and asset1 = wstETH. + // NOTE: Assumption is made that asset2 is the yield-bearing counterpart of asset0 and asset1. + // Examples: asset0 = USDC, asset1 = DAI, asset2 = sDAI IERC20 public immutable asset0; IERC20 public immutable asset1; + IERC20 public immutable asset2; address public immutable rateProvider; uint256 public immutable asset0Precision; uint256 public immutable asset1Precision; + uint256 public immutable asset2Precision; uint256 public immutable initialBurnAmount; uint256 public totalShares; @@ -36,19 +40,25 @@ contract PSM { constructor( address asset0_, address asset1_, + address asset2_, address rateProvider_, uint256 initialBurnAmount_ ) { require(asset0_ != address(0), "PSM/invalid-asset0"); require(asset1_ != address(0), "PSM/invalid-asset1"); + require(asset2_ != address(0), "PSM/invalid-asset2"); require(rateProvider_ != address(0), "PSM/invalid-rateProvider"); - asset0 = IERC20(asset0_); - asset1 = IERC20(asset1_); + asset0 = IERC20(asset0_); + asset1 = IERC20(asset1_); + asset2 = IERC20(asset2_); + rateProvider = rateProvider_; - asset0Precision = 10 ** IERC20(asset0_).decimals(); - asset1Precision = 10 ** IERC20(asset1_).decimals(); + asset0Precision = 10 ** IERC20(asset0_).decimals(); + asset1Precision = 10 ** IERC20(asset1_).decimals(); + asset2Precision = 10 ** IERC20(asset2_).decimals(); + initialBurnAmount = initialBurnAmount_; } @@ -56,28 +66,24 @@ contract PSM { /*** Swap functions ***/ /**********************************************************************************************/ - function swapAssetZeroToOne(uint256 amountIn, uint256 minAmountOut, address receiver) external { - require(amountIn != 0, "PSM/invalid-amountIn"); - require(receiver != address(0), "PSM/invalid-receiver"); - - uint256 amountOut = previewSwapAssetZeroToOne(amountIn); - - require(amountOut >= minAmountOut, "PSM/amountOut-too-low"); - - asset0.safeTransferFrom(msg.sender, address(this), amountIn); - asset1.safeTransfer(receiver, amountOut); - } - - function swapAssetOneToZero(uint256 amountIn, uint256 minAmountOut, address receiver) external { + function swap( + address assetIn, + address assetOut, + uint256 amountIn, + uint256 minAmountOut, + address receiver + ) + external + { require(amountIn != 0, "PSM/invalid-amountIn"); require(receiver != address(0), "PSM/invalid-receiver"); - uint256 amountOut = previewSwapAssetOneToZero(amountIn); + uint256 amountOut = previewSwap(assetIn, assetOut, amountIn); require(amountOut >= minAmountOut, "PSM/amountOut-too-low"); - asset1.safeTransferFrom(msg.sender, address(this), amountIn); - asset0.safeTransfer(receiver, amountOut); + IERC20(assetIn).safeTransferFrom(msg.sender, address(this), amountIn); + IERC20(assetOut).safeTransfer(receiver, amountOut); } /**********************************************************************************************/ @@ -153,20 +159,25 @@ contract PSM { /*** Swap preview functions ***/ /**********************************************************************************************/ - function previewSwapAssetZeroToOne(uint256 amountIn) public view returns (uint256) { - return amountIn - * 1e27 - * asset1Precision - / IRateProviderLike(rateProvider).getConversionRate() - / asset0Precision; - } + function previewSwap(address assetIn, address assetOut, uint256 amountIn) + public view returns (uint256 amountOut) + { + if (assetIn == address(asset0)) { + if (assetOut == address(asset1)) return _previewOneToOneSwap(amountIn, asset0Precision, asset1Precision); + else if (assetOut == address(asset2)) return _previewSwapToAsset2(amountIn, asset0Precision); + } - function previewSwapAssetOneToZero(uint256 amountIn) public view returns (uint256) { - return amountIn - * IRateProviderLike(rateProvider).getConversionRate() - * asset0Precision - / 1e27 - / asset1Precision; + else if (assetIn == address(asset1)) { + if (assetOut == address(asset0)) return _previewOneToOneSwap(amountIn, asset1Precision, asset0Precision); + else if (assetOut == address(asset2)) return _previewSwapToAsset2(amountIn, asset1Precision); + } + + else if (assetIn == address(asset2)) { + if (assetOut == address(asset0)) return _previewSwapFromAsset2(amountIn, asset0Precision); + else if (assetOut == address(asset1)) return _previewSwapFromAsset2(amountIn, asset1Precision); + } + + revert("PSM/invalid-asset"); } /**********************************************************************************************/ @@ -204,6 +215,7 @@ contract PSM { /*** Asset value functions ***/ /**********************************************************************************************/ + // TODO: Refactor for three assets function getPsmTotalValue() public view returns (uint256) { return _getAsset0Value(asset0.balanceOf(address(this))) + _getAsset1Value(asset1.balanceOf(address(this))); @@ -254,4 +266,36 @@ contract PSM { return amount * IRateProviderLike(rateProvider).getConversionRate() / 1e9 / asset1Precision; } + function _previewSwapToAsset2(uint256 amountIn, uint256 assetInPrecision) + internal view returns (uint256) + { + return amountIn + * 1e27 + * asset2Precision + / IRateProviderLike(rateProvider).getConversionRate() + / assetInPrecision; + } + + function _previewSwapFromAsset2(uint256 amountIn, uint256 assetInPrecision) + internal view returns (uint256) + { + return amountIn + * IRateProviderLike(rateProvider).getConversionRate() + * assetInPrecision + / 1e27 + / asset2Precision; + } + + function _previewOneToOneSwap( + uint256 amountIn, + uint256 assetInPrecision, + uint256 assetOutPrecision + ) + internal pure returns (uint256) + { + return amountIn + * assetOutPrecision + / assetInPrecision; + } + } diff --git a/test/Constructor.t.sol b/test/Constructor.t.sol index d505441..a2edd82 100644 --- a/test/Constructor.t.sol +++ b/test/Constructor.t.sol @@ -11,29 +11,36 @@ contract PSMConstructorTests is PSMTestBase { function test_constructor_invalidAsset0() public { vm.expectRevert("PSM/invalid-asset0"); - new PSM(address(0), address(sDai), address(rateProvider), 1000); + new PSM(address(0), address(usdc), address(sDai), address(rateProvider), 1000); } function test_constructor_invalidAsset1() public { vm.expectRevert("PSM/invalid-asset1"); - new PSM(address(usdc), address(0), address(rateProvider), 1000); + new PSM(address(dai), address(0), address(sDai), address(rateProvider), 1000); + } + + function test_constructor_invalidAsset2() public { + vm.expectRevert("PSM/invalid-asset2"); + new PSM(address(dai), address(usdc), address(0), address(rateProvider), 1000); } function test_constructor_invalidRateProvider() public { vm.expectRevert("PSM/invalid-rateProvider"); - new PSM(address(sDai), address(usdc), address(0), 1000); + new PSM(address(dai), address(sDai), address(usdc), address(0), 1000); } function test_constructor() public { // Deploy new PSM to get test coverage - psm = new PSM(address(usdc), address(sDai), address(rateProvider), 1000); + psm = new PSM(address(dai), address(usdc), address(sDai), address(rateProvider), 1000); - assertEq(address(psm.asset0()), address(usdc)); - assertEq(address(psm.asset1()), address(sDai)); + assertEq(address(psm.asset0()), address(dai)); + assertEq(address(psm.asset1()), address(usdc)); + assertEq(address(psm.asset2()), address(sDai)); assertEq(address(psm.rateProvider()), address(rateProvider)); - assertEq(psm.asset0Precision(), 10 ** usdc.decimals()); - assertEq(psm.asset1Precision(), 10 ** sDai.decimals()); + assertEq(psm.asset0Precision(), 10 ** dai.decimals()); + assertEq(psm.asset1Precision(), 10 ** usdc.decimals()); + assertEq(psm.asset2Precision(), 10 ** sDai.decimals()); } } diff --git a/test/Getters.t.sol b/test/Getters.t.sol index 895f445..6daba33 100644 --- a/test/Getters.t.sol +++ b/test/Getters.t.sol @@ -13,7 +13,13 @@ contract PSMHarnessTests is PSMTestBase { function setUp() public override { super.setUp(); - psmHarness = new PSMHarness(address(usdc), address(sDai), address(rateProvider), 1000); + psmHarness = new PSMHarness( + address(dai), + address(usdc), + address(sDai), + address(rateProvider), + 1000 + ); } function test_getAsset0Value() public view { diff --git a/test/InflationAttack.t.sol b/test/InflationAttack.t.sol index 9f0e189..b14e505 100644 --- a/test/InflationAttack.t.sol +++ b/test/InflationAttack.t.sol @@ -10,7 +10,7 @@ import { PSMTestBase } from "test/PSMTestBase.sol"; contract InflationAttackTests is PSMTestBase { function test_inflationAttack_noInitialBurnAmount() public { - psm = new PSM(address(usdc), address(sDai), address(rateProvider), 0); + psm = new PSM(address(dai), address(usdc), address(sDai), address(rateProvider), 0); address firstDepositor = makeAddr("firstDepositor"); address frontRunner = makeAddr("frontRunner"); @@ -55,7 +55,7 @@ contract InflationAttackTests is PSMTestBase { } function test_inflationAttack_useInitialBurnAmount_firstDepositOverflowBoundary() public { - psm = new PSM(address(usdc), address(sDai), address(rateProvider), 1000); + psm = new PSM(address(dai), address(usdc), address(sDai), address(rateProvider), 1000); address frontRunner = makeAddr("frontRunner"); @@ -71,7 +71,7 @@ contract InflationAttackTests is PSMTestBase { } function test_inflationAttack_useInitialBurnAmount() public { - psm = new PSM(address(usdc), address(sDai), address(rateProvider), 1000); + psm = new PSM(address(dai), address(usdc), address(sDai), address(rateProvider), 1000); address firstDepositor = makeAddr("firstDepositor"); address frontRunner = makeAddr("frontRunner"); diff --git a/test/PSMTestBase.sol b/test/PSMTestBase.sol index 8675d7c..7b47516 100644 --- a/test/PSMTestBase.sol +++ b/test/PSMTestBase.sol @@ -13,9 +13,10 @@ contract PSMTestBase is Test { PSM public psm; - // NOTE: Using sDAI and USDC as example assets - MockERC20 public sDai; + // NOTE: Using DAI, sDAI and USDC as example assets + MockERC20 public dai; MockERC20 public usdc; + MockERC20 public sDai; MockRateProvider public rateProvider; @@ -28,25 +29,30 @@ contract PSMTestBase is Test { // 1,000,000,000,000 of each token uint256 public constant USDC_TOKEN_MAX = 1e18; uint256 public constant SDAI_TOKEN_MAX = 1e30; + uint256 public constant DAI_TOKEN_MAX = 1e30; function setUp() public virtual { - sDai = new MockERC20("sDai", "sDai", 18); + dai = new MockERC20("dai", "dai", 18); usdc = new MockERC20("usdc", "usdc", 6); + sDai = new MockERC20("sDai", "sDai", 18); rateProvider = new MockRateProvider(); // NOTE: Using 1.25 for easy two way conversions rateProvider.__setConversionRate(1.25e27); - psm = new PSM(address(usdc), address(sDai), address(rateProvider), 1000); + psm = new PSM(address(dai), address(usdc), address(sDai), address(rateProvider), 1000); - vm.label(address(sDai), "sDAI"); + vm.label(address(dai), "DAI"); vm.label(address(usdc), "USDC"); + vm.label(address(sDai), "sDAI"); } + // TODO: Refactor for three assets function _getPsmValue() internal view returns (uint256) { return (sDai.balanceOf(address(psm)) * rateProvider.getConversionRate() / 1e27) - + usdc.balanceOf(address(psm)) * 1e12; + + usdc.balanceOf(address(psm)) * 1e12 + + dai.balanceOf(address(psm)); } function _deposit(address user, address asset, uint256 amount) internal { diff --git a/test/Previews.t.sol b/test/Previews.t.sol index 8b42c08..b00218a 100644 --- a/test/Previews.t.sol +++ b/test/Previews.t.sol @@ -5,98 +5,98 @@ import "forge-std/Test.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -contract PSMPreviewFunctionTests is PSMTestBase { +// contract PSMPreviewFunctionTests is PSMTestBase { - function test_previewSwapAssetZeroToOne() public { - assertEq(psm.previewSwapAssetZeroToOne(1), 0.8e12); - assertEq(psm.previewSwapAssetZeroToOne(2), 1.6e12); - assertEq(psm.previewSwapAssetZeroToOne(3), 2.4e12); +// function test_previewSwapAssetZeroToOne() public { +// assertEq(psm.previewSwapAssetZeroToOne(1), 0.8e12); +// assertEq(psm.previewSwapAssetZeroToOne(2), 1.6e12); +// assertEq(psm.previewSwapAssetZeroToOne(3), 2.4e12); - assertEq(psm.previewSwapAssetZeroToOne(1e6), 0.8e18); - assertEq(psm.previewSwapAssetZeroToOne(2e6), 1.6e18); - assertEq(psm.previewSwapAssetZeroToOne(3e6), 2.4e18); +// assertEq(psm.previewSwapAssetZeroToOne(1e6), 0.8e18); +// assertEq(psm.previewSwapAssetZeroToOne(2e6), 1.6e18); +// assertEq(psm.previewSwapAssetZeroToOne(3e6), 2.4e18); - assertEq(psm.previewSwapAssetZeroToOne(1.000001e6), 0.8000008e18); +// assertEq(psm.previewSwapAssetZeroToOne(1.000001e6), 0.8000008e18); - rateProvider.__setConversionRate(1.6e27); +// rateProvider.__setConversionRate(1.6e27); - assertEq(psm.previewSwapAssetZeroToOne(1), 0.625e12); - assertEq(psm.previewSwapAssetZeroToOne(2), 1.25e12); - assertEq(psm.previewSwapAssetZeroToOne(3), 1.875e12); +// assertEq(psm.previewSwapAssetZeroToOne(1), 0.625e12); +// assertEq(psm.previewSwapAssetZeroToOne(2), 1.25e12); +// assertEq(psm.previewSwapAssetZeroToOne(3), 1.875e12); - assertEq(psm.previewSwapAssetZeroToOne(1e6), 0.625e18); - assertEq(psm.previewSwapAssetZeroToOne(2e6), 1.25e18); - assertEq(psm.previewSwapAssetZeroToOne(3e6), 1.875e18); +// assertEq(psm.previewSwapAssetZeroToOne(1e6), 0.625e18); +// assertEq(psm.previewSwapAssetZeroToOne(2e6), 1.25e18); +// assertEq(psm.previewSwapAssetZeroToOne(3e6), 1.875e18); - assertEq(psm.previewSwapAssetZeroToOne(1.000001e6), 0.625000625e18); +// assertEq(psm.previewSwapAssetZeroToOne(1.000001e6), 0.625000625e18); - rateProvider.__setConversionRate(0.8e27); +// rateProvider.__setConversionRate(0.8e27); - assertEq(psm.previewSwapAssetZeroToOne(1), 1.25e12); - assertEq(psm.previewSwapAssetZeroToOne(2), 2.5e12); - assertEq(psm.previewSwapAssetZeroToOne(3), 3.75e12); +// assertEq(psm.previewSwapAssetZeroToOne(1), 1.25e12); +// assertEq(psm.previewSwapAssetZeroToOne(2), 2.5e12); +// assertEq(psm.previewSwapAssetZeroToOne(3), 3.75e12); - assertEq(psm.previewSwapAssetZeroToOne(1e6), 1.25e18); - assertEq(psm.previewSwapAssetZeroToOne(2e6), 2.5e18); - assertEq(psm.previewSwapAssetZeroToOne(3e6), 3.75e18); +// assertEq(psm.previewSwapAssetZeroToOne(1e6), 1.25e18); +// assertEq(psm.previewSwapAssetZeroToOne(2e6), 2.5e18); +// assertEq(psm.previewSwapAssetZeroToOne(3e6), 3.75e18); - assertEq(psm.previewSwapAssetZeroToOne(1.000001e6), 1.25000125e18); - } +// assertEq(psm.previewSwapAssetZeroToOne(1.000001e6), 1.25000125e18); +// } - function test_previewSwapAssetOneToZero() public { - assertEq(psm.previewSwapAssetOneToZero(1), 0); - assertEq(psm.previewSwapAssetOneToZero(2), 0); - assertEq(psm.previewSwapAssetOneToZero(3), 0); - assertEq(psm.previewSwapAssetOneToZero(4), 0); +// function test_previewSwapAssetOneToZero() public { +// assertEq(psm.previewSwapAssetOneToZero(1), 0); +// assertEq(psm.previewSwapAssetOneToZero(2), 0); +// assertEq(psm.previewSwapAssetOneToZero(3), 0); +// assertEq(psm.previewSwapAssetOneToZero(4), 0); - // 1e-6 with 18 decimal precision - assertEq(psm.previewSwapAssetOneToZero(1e12), 1); - assertEq(psm.previewSwapAssetOneToZero(2e12), 2); - assertEq(psm.previewSwapAssetOneToZero(3e12), 3); - assertEq(psm.previewSwapAssetOneToZero(4e12), 5); +// // 1e-6 with 18 decimal precision +// assertEq(psm.previewSwapAssetOneToZero(1e12), 1); +// assertEq(psm.previewSwapAssetOneToZero(2e12), 2); +// assertEq(psm.previewSwapAssetOneToZero(3e12), 3); +// assertEq(psm.previewSwapAssetOneToZero(4e12), 5); - assertEq(psm.previewSwapAssetOneToZero(1e18), 1.25e6); - assertEq(psm.previewSwapAssetOneToZero(2e18), 2.5e6); - assertEq(psm.previewSwapAssetOneToZero(3e18), 3.75e6); - assertEq(psm.previewSwapAssetOneToZero(4e18), 5e6); +// assertEq(psm.previewSwapAssetOneToZero(1e18), 1.25e6); +// assertEq(psm.previewSwapAssetOneToZero(2e18), 2.5e6); +// assertEq(psm.previewSwapAssetOneToZero(3e18), 3.75e6); +// assertEq(psm.previewSwapAssetOneToZero(4e18), 5e6); - assertEq(psm.previewSwapAssetOneToZero(1.000001e18), 1.250001e6); +// assertEq(psm.previewSwapAssetOneToZero(1.000001e18), 1.250001e6); - rateProvider.__setConversionRate(1.6e27); +// rateProvider.__setConversionRate(1.6e27); - assertEq(psm.previewSwapAssetOneToZero(1), 0); - assertEq(psm.previewSwapAssetOneToZero(2), 0); - assertEq(psm.previewSwapAssetOneToZero(3), 0); - assertEq(psm.previewSwapAssetOneToZero(4), 0); +// assertEq(psm.previewSwapAssetOneToZero(1), 0); +// assertEq(psm.previewSwapAssetOneToZero(2), 0); +// assertEq(psm.previewSwapAssetOneToZero(3), 0); +// assertEq(psm.previewSwapAssetOneToZero(4), 0); - // 1e-6 with 18 decimal precision - assertEq(psm.previewSwapAssetOneToZero(1e12), 1); - assertEq(psm.previewSwapAssetOneToZero(2e12), 3); - assertEq(psm.previewSwapAssetOneToZero(3e12), 4); - assertEq(psm.previewSwapAssetOneToZero(4e12), 6); +// // 1e-6 with 18 decimal precision +// assertEq(psm.previewSwapAssetOneToZero(1e12), 1); +// assertEq(psm.previewSwapAssetOneToZero(2e12), 3); +// assertEq(psm.previewSwapAssetOneToZero(3e12), 4); +// assertEq(psm.previewSwapAssetOneToZero(4e12), 6); - assertEq(psm.previewSwapAssetOneToZero(1e18), 1.6e6); - assertEq(psm.previewSwapAssetOneToZero(2e18), 3.2e6); - assertEq(psm.previewSwapAssetOneToZero(3e18), 4.8e6); - assertEq(psm.previewSwapAssetOneToZero(4e18), 6.4e6); +// assertEq(psm.previewSwapAssetOneToZero(1e18), 1.6e6); +// assertEq(psm.previewSwapAssetOneToZero(2e18), 3.2e6); +// assertEq(psm.previewSwapAssetOneToZero(3e18), 4.8e6); +// assertEq(psm.previewSwapAssetOneToZero(4e18), 6.4e6); - rateProvider.__setConversionRate(0.8e27); +// rateProvider.__setConversionRate(0.8e27); - assertEq(psm.previewSwapAssetOneToZero(1), 0); - assertEq(psm.previewSwapAssetOneToZero(2), 0); - assertEq(psm.previewSwapAssetOneToZero(3), 0); - assertEq(psm.previewSwapAssetOneToZero(4), 0); +// assertEq(psm.previewSwapAssetOneToZero(1), 0); +// assertEq(psm.previewSwapAssetOneToZero(2), 0); +// assertEq(psm.previewSwapAssetOneToZero(3), 0); +// assertEq(psm.previewSwapAssetOneToZero(4), 0); - // 1e-6 with 18 decimal precision - assertEq(psm.previewSwapAssetOneToZero(1e12), 0); - assertEq(psm.previewSwapAssetOneToZero(2e12), 1); - assertEq(psm.previewSwapAssetOneToZero(3e12), 2); - assertEq(psm.previewSwapAssetOneToZero(4e12), 3); +// // 1e-6 with 18 decimal precision +// assertEq(psm.previewSwapAssetOneToZero(1e12), 0); +// assertEq(psm.previewSwapAssetOneToZero(2e12), 1); +// assertEq(psm.previewSwapAssetOneToZero(3e12), 2); +// assertEq(psm.previewSwapAssetOneToZero(4e12), 3); - assertEq(psm.previewSwapAssetOneToZero(1e18), 0.8e6); - assertEq(psm.previewSwapAssetOneToZero(2e18), 1.6e6); - assertEq(psm.previewSwapAssetOneToZero(3e18), 2.4e6); - assertEq(psm.previewSwapAssetOneToZero(4e18), 3.2e6); - } +// assertEq(psm.previewSwapAssetOneToZero(1e18), 0.8e6); +// assertEq(psm.previewSwapAssetOneToZero(2e18), 1.6e6); +// assertEq(psm.previewSwapAssetOneToZero(3e18), 2.4e6); +// assertEq(psm.previewSwapAssetOneToZero(4e18), 3.2e6); +// } -} +// } diff --git a/test/Swaps.t.sol b/test/Swaps.t.sol index a8c923f..400b48c 100644 --- a/test/Swaps.t.sol +++ b/test/Swaps.t.sol @@ -5,289 +5,389 @@ import "forge-std/Test.sol"; import { PSM } from "../src/PSM.sol"; -import { PSMTestBase } from "test/PSMTestBase.sol"; +import { MockERC20, PSMTestBase } from "test/PSMTestBase.sol"; -contract PSMSwapAssetZeroToOneTests is PSMTestBase { +contract PSMSwapFailureTests is PSMTestBase { - address public buyer = makeAddr("buyer"); + address public swapper = makeAddr("swapper"); address public receiver = makeAddr("receiver"); function setUp() public override { super.setUp(); + // Needed for boundary success conditions usdc.mint(address(psm), 100e6); sDai.mint(address(psm), 100e18); } - function test_swapAssetZeroToOne_amountZero() public { + function test_swap_amountZero() public { vm.expectRevert("PSM/invalid-amountIn"); - psm.swapAssetZeroToOne(0, 0, receiver); + psm.swap(address(usdc), address(sDai), 0, 0, receiver); } - function test_swapAssetZeroToOne_receiverZero() public { + function test_swap_receiverZero() public { vm.expectRevert("PSM/invalid-receiver"); - psm.swapAssetZeroToOne(100e6, 80e18, address(0)); + psm.swap(address(usdc), address(sDai), 100e6, 80e18, address(0)); } - function test_swapAssetZeroToOne_minAmountOutBoundary() public { - usdc.mint(buyer, 100e6); + function test_swap_invalid_assetIn() public { + vm.expectRevert("PSM/invalid-asset"); + psm.swap(makeAddr("other-token"), address(sDai), 100e6, 80e18, receiver); + } + + function test_swap_invalid_assetOut() public { + vm.expectRevert("PSM/invalid-asset"); + psm.swap(address(usdc), makeAddr("other-token"), 100e6, 80e18, receiver); + } - vm.startPrank(buyer); + function test_swap_minAmountOutBoundary() public { + usdc.mint(swapper, 100e6); + + vm.startPrank(swapper); usdc.approve(address(psm), 100e6); - uint256 expectedAmountOut = psm.previewSwapAssetZeroToOne(100e6); + uint256 expectedAmountOut = psm.previewSwap(address(usdc), address(sDai), 100e6); assertEq(expectedAmountOut, 80e18); vm.expectRevert("PSM/amountOut-too-low"); - psm.swapAssetZeroToOne(100e6, 80e18 + 1, receiver); + psm.swap(address(usdc), address(sDai), 100e6, 80e18 + 1, receiver); - psm.swapAssetZeroToOne(100e6, 80e18, receiver); + psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver); } - function test_swapAssetZeroToOne_insufficientApproveBoundary() public { - usdc.mint(buyer, 100e6); + function test_swap_insufficientApproveBoundary() public { + usdc.mint(swapper, 100e6); - vm.startPrank(buyer); + vm.startPrank(swapper); usdc.approve(address(psm), 100e6 - 1); vm.expectRevert("SafeERC20/transfer-from-failed"); - psm.swapAssetZeroToOne(100e6, 80e18, receiver); + psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver); usdc.approve(address(psm), 100e6); - psm.swapAssetZeroToOne(100e6, 80e18, receiver); + psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver); } - function test_swapAssetZeroToOne_insufficientUserBalanceBoundary() public { - usdc.mint(buyer, 100e6 - 1); + function test_swap_insufficientUserBalanceBoundary() public { + usdc.mint(swapper, 100e6 - 1); - vm.startPrank(buyer); + vm.startPrank(swapper); usdc.approve(address(psm), 100e6); vm.expectRevert("SafeERC20/transfer-from-failed"); - psm.swapAssetZeroToOne(100e6, 80e18, receiver); + psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver); - usdc.mint(buyer, 1); + usdc.mint(swapper, 1); - psm.swapAssetZeroToOne(100e6, 80e18, receiver); + psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver); } - function test_swapAssetZeroToOne_insufficientPsmBalanceBoundary() public { - usdc.mint(buyer, 125e6 + 1); + function test_swap_insufficientPsmBalanceBoundary() public { + usdc.mint(swapper, 125e6 + 1); - vm.startPrank(buyer); + vm.startPrank(swapper); usdc.approve(address(psm), 125e6 + 1); + uint256 expectedAmountOut = psm.previewSwap(address(usdc), address(sDai), 125e6 + 1); + + assertEq(expectedAmountOut, 100.0000008e18); // More than balance of sDAI + vm.expectRevert("SafeERC20/transfer-failed"); - psm.swapAssetZeroToOne(125e6 + 1, 100e18, receiver); + psm.swap(address(usdc), address(sDai), 125e6 + 1, 100e18, receiver); - psm.swapAssetZeroToOne(125e6, 100e18, receiver); + psm.swap(address(usdc), address(sDai), 125e6, 100e18, receiver); } - function test_swapAssetZeroToOne_sameReceiver() public assertAtomicPsmValueDoesNotChange { - usdc.mint(buyer, 100e6); +} - vm.startPrank(buyer); +contract PSMSuccessTestsBase is PSMTestBase { - usdc.approve(address(psm), 100e6); + function setUp() public override { + super.setUp(); + + dai.mint(address(psm), 1_000_000e18); + usdc.mint(address(psm), 1_000_000e6); + sDai.mint(address(psm), 1_000_000e18); + } + + function _swapTest( + MockERC20 assetIn, + MockERC20 assetOut, + uint256 amountIn, + uint256 amountOut, + address swapper, + address receiver + ) internal { + uint256 psmAssetInBalance = 1_000_000 * 10 ** assetIn.decimals(); + uint256 psmAssetOutBalance = 1_000_000 * 10 ** assetOut.decimals(); - assertEq(usdc.allowance(buyer, address(psm)), 100e6); + assetIn.mint(swapper, amountIn); - assertEq(sDai.balanceOf(buyer), 0); - assertEq(sDai.balanceOf(address(psm)), 100e18); + vm.startPrank(swapper); - assertEq(usdc.balanceOf(buyer), 100e6); - assertEq(usdc.balanceOf(address(psm)), 100e6); + assetIn.approve(address(psm), amountIn); - psm.swapAssetZeroToOne(100e6, 80e18, buyer); + assertEq(assetIn.allowance(swapper, address(psm)), amountIn); - assertEq(usdc.allowance(buyer, address(psm)), 0); + assertEq(assetIn.balanceOf(swapper), amountIn); + assertEq(assetIn.balanceOf(address(psm)), psmAssetInBalance); - assertEq(sDai.balanceOf(buyer), 80e18); - assertEq(sDai.balanceOf(address(psm)), 20e18); + assertEq(assetOut.balanceOf(receiver), 0); + assertEq(assetOut.balanceOf(address(psm)), psmAssetOutBalance); - assertEq(usdc.balanceOf(buyer), 0); - assertEq(usdc.balanceOf(address(psm)), 200e6); + psm.swap(address(assetIn), address(assetOut), amountIn, amountOut, receiver); + + assertEq(assetIn.allowance(swapper, address(psm)), 0); + + assertEq(assetIn.balanceOf(swapper), 0); + assertEq(assetIn.balanceOf(address(psm)), psmAssetInBalance + amountIn); + + assertEq(assetOut.balanceOf(receiver), amountOut); + assertEq(assetOut.balanceOf(address(psm)), psmAssetOutBalance - amountOut); } - function test_swapAssetZeroToOne_differentReceiver() public assertAtomicPsmValueDoesNotChange { - usdc.mint(buyer, 100e6); +} - vm.startPrank(buyer); +contract PSMSwapTests is PSMSuccessTestsBase { - usdc.approve(address(psm), 100e6); + address public swapper = makeAddr("swapper"); + address public receiver = makeAddr("receiver"); - assertEq(usdc.allowance(buyer, address(psm)), 100e6); + // DAI assetIn tests - assertEq(sDai.balanceOf(buyer), 0); - assertEq(sDai.balanceOf(receiver), 0); - assertEq(sDai.balanceOf(address(psm)), 100e18); + function test_swap_daiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapTest(dai, usdc, 100e18, 100e6, swapper, swapper); + } - assertEq(usdc.balanceOf(buyer), 100e6); - assertEq(usdc.balanceOf(receiver), 0); - assertEq(usdc.balanceOf(address(psm)), 100e6); + function test_swap_daiToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapTest(dai, sDai, 100e18, 80e18, swapper, swapper); + } - psm.swapAssetZeroToOne(100e6, 80e18, receiver); + function test_swap_daiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapTest(dai, usdc, 100e18, 100e6, swapper, receiver); + } - assertEq(usdc.allowance(buyer, address(psm)), 0); + function test_swap_daiToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapTest(dai, sDai, 100e18, 80e18, swapper, receiver); + } - assertEq(sDai.balanceOf(buyer), 0); - assertEq(sDai.balanceOf(receiver), 80e18); - assertEq(sDai.balanceOf(address(psm)), 20e18); + // USDC assetIn tests - assertEq(usdc.balanceOf(buyer), 0); - assertEq(usdc.balanceOf(receiver), 0); - assertEq(usdc.balanceOf(address(psm)), 200e6); + function test_swap_usdcToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapTest(usdc, dai, 100e6, 100e18, swapper, swapper); } -} + function test_swap_usdcToSDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapTest(usdc, sDai, 100e6, 80e18, swapper, swapper); + } -contract PSMSwapAssetOneToZeroTests is PSMTestBase { + function test_swap_usdcToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapTest(usdc, dai, 100e6, 100e18, swapper, receiver); + } - address public buyer = makeAddr("buyer"); - address public receiver = makeAddr("receiver"); + function test_swap_usdcToSDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapTest(usdc, sDai, 100e6, 80e18, swapper, receiver); + } - function setUp() public override { - super.setUp(); + // sDai assetIn tests - usdc.mint(address(psm), 100e6); - sDai.mint(address(psm), 100e18); + function test_swap_sDaiToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapTest(sDai, dai, 100e18, 125e18, swapper, swapper); } - function test_swapAssetOneToZero_amountZero() public { - vm.expectRevert("PSM/invalid-amountIn"); - psm.swapAssetOneToZero(0, 0, receiver); + function test_swap_sDaiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { + _swapTest(sDai, usdc, 100e18, 125e6, swapper, swapper); } - function test_swapAssetZeroToOne_receiverZero() public { - vm.expectRevert("PSM/invalid-receiver"); - psm.swapAssetOneToZero(100e6, 80e18, address(0)); + function test_swap_sDaiToDai_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapTest(sDai, dai, 100e18, 125e18, swapper, receiver); } - function test_swapAssetOneToZero_minAmountOutBoundary() public { - sDai.mint(buyer, 80e18); + function test_swap_sDaiToUsdc_differentReceiver() public assertAtomicPsmValueDoesNotChange { + _swapTest(sDai, usdc, 100e18, 125e6, swapper, receiver); + } - vm.startPrank(buyer); - sDai.approve(address(psm), 80e18); - uint256 expectedAmountOut = psm.previewSwapAssetOneToZero(80e18); + // function test_swapAssetZeroToOne_differentReceiver() public assertAtomicPsmValueDoesNotChange { + // usdc.mint(buyer, 100e6); - assertEq(expectedAmountOut, 100e6); + // vm.startPrank(buyer); - vm.expectRevert("PSM/amountOut-too-low"); - psm.swapAssetOneToZero(80e18, 100e6 + 1, receiver); + // usdc.approve(address(psm), 100e6); - psm.swapAssetOneToZero(80e18, 100e6, receiver); - } + // assertEq(usdc.allowance(buyer, address(psm)), 100e6); - function test_swapAssetOneToZero_insufficientApproveBoundary() public { - sDai.mint(buyer, 80e18); + // assertEq(sDai.balanceOf(buyer), 0); + // assertEq(sDai.balanceOf(receiver), 0); + // assertEq(sDai.balanceOf(address(psm)), 100e18); - vm.startPrank(buyer); + // assertEq(usdc.balanceOf(buyer), 100e6); + // assertEq(usdc.balanceOf(receiver), 0); + // assertEq(usdc.balanceOf(address(psm)), 100e6); - sDai.approve(address(psm), 80e18 - 1); + // psm.swapAssetZeroToOne(100e6, 80e18, receiver); - vm.expectRevert("SafeERC20/transfer-from-failed"); - psm.swapAssetOneToZero(80e18, 100e6, receiver); + // assertEq(usdc.allowance(buyer, address(psm)), 0); - sDai.approve(address(psm), 80e18); + // assertEq(sDai.balanceOf(buyer), 0); + // assertEq(sDai.balanceOf(receiver), 80e18); + // assertEq(sDai.balanceOf(address(psm)), 20e18); - psm.swapAssetOneToZero(80e18, 100e6, receiver); - } + // assertEq(usdc.balanceOf(buyer), 0); + // assertEq(usdc.balanceOf(receiver), 0); + // assertEq(usdc.balanceOf(address(psm)), 200e6); + // } - function test_swapAssetOneToZero_insufficientUserBalanceBoundary() public { - sDai.mint(buyer, 80e18 - 1); +} - vm.startPrank(buyer); +// contract PSMSwapAssetOneToZeroTests is PSMTestBase { - sDai.approve(address(psm), 80e18); +// address public buyer = makeAddr("buyer"); +// address public receiver = makeAddr("receiver"); - vm.expectRevert("SafeERC20/transfer-from-failed"); - psm.swapAssetOneToZero(80e18, 100e6, receiver); +// function setUp() public override { +// super.setUp(); - sDai.mint(buyer, 1); +// usdc.mint(address(psm), 100e6); +// sDai.mint(address(psm), 100e18); +// } - psm.swapAssetOneToZero(80e18, 100e6, receiver); - } +// function test_swapAssetOneToZero_amountZero() public { +// vm.expectRevert("PSM/invalid-amountIn"); +// psm.swapAssetOneToZero(0, 0, receiver); +// } - function test_swapAssetOneToZero_insufficientPsmBalanceBoundary() public { - // Prove that values yield balance boundary - // 0.8e12 * 1.25 = 1e12 == 1-e6 in 18 decimal precision - assertEq(psm.previewSwapAssetOneToZero(80e18 + 0.8e12), 100e6 + 1); - assertEq(psm.previewSwapAssetOneToZero(80e18 + 0.8e12 - 1), 100e6); +// function test_swapAssetZeroToOne_receiverZero() public { +// vm.expectRevert("PSM/invalid-receiver"); +// psm.swapAssetOneToZero(100e6, 80e18, address(0)); +// } - sDai.mint(buyer, 80e18 + 0.8e12); +// function test_swapAssetOneToZero_minAmountOutBoundary() public { +// sDai.mint(buyer, 80e18); - vm.startPrank(buyer); +// vm.startPrank(buyer); - sDai.approve(address(psm), 80e18 + 0.8e12); +// sDai.approve(address(psm), 80e18); - vm.expectRevert("SafeERC20/transfer-failed"); - psm.swapAssetOneToZero(80e18 + 0.8e12, 100e6, receiver); +// uint256 expectedAmountOut = psm.previewSwapAssetOneToZero(80e18); - psm.swapAssetOneToZero(80e18 + 0.8e12 - 1, 100e6, receiver); - } +// assertEq(expectedAmountOut, 100e6); - function test_swapAssetOneToZero_sameReceiver() public assertAtomicPsmValueDoesNotChange { - sDai.mint(buyer, 80e18); +// vm.expectRevert("PSM/amountOut-too-low"); +// psm.swapAssetOneToZero(80e18, 100e6 + 1, receiver); - vm.startPrank(buyer); +// psm.swapAssetOneToZero(80e18, 100e6, receiver); +// } - sDai.approve(address(psm), 80e18); +// function test_swapAssetOneToZero_insufficientApproveBoundary() public { +// sDai.mint(buyer, 80e18); - assertEq(sDai.allowance(buyer, address(psm)), 80e18); +// vm.startPrank(buyer); - assertEq(sDai.balanceOf(buyer), 80e18); - assertEq(sDai.balanceOf(address(psm)), 100e18); +// sDai.approve(address(psm), 80e18 - 1); - assertEq(usdc.balanceOf(buyer), 0); - assertEq(usdc.balanceOf(address(psm)), 100e6); +// vm.expectRevert("SafeERC20/transfer-from-failed"); +// psm.swapAssetOneToZero(80e18, 100e6, receiver); - psm.swapAssetOneToZero(80e18, 100e6, buyer); +// sDai.approve(address(psm), 80e18); - assertEq(usdc.allowance(buyer, address(psm)), 0); +// psm.swapAssetOneToZero(80e18, 100e6, receiver); +// } - assertEq(sDai.balanceOf(buyer), 0); - assertEq(sDai.balanceOf(address(psm)), 180e18); +// function test_swapAssetOneToZero_insufficientUserBalanceBoundary() public { +// sDai.mint(buyer, 80e18 - 1); - assertEq(usdc.balanceOf(buyer), 100e6); - assertEq(usdc.balanceOf(address(psm)), 0); - } +// vm.startPrank(buyer); - function test_swapAssetOneToZero_differentReceiver() public assertAtomicPsmValueDoesNotChange { - sDai.mint(buyer, 80e18); +// sDai.approve(address(psm), 80e18); - vm.startPrank(buyer); +// vm.expectRevert("SafeERC20/transfer-from-failed"); +// psm.swapAssetOneToZero(80e18, 100e6, receiver); - sDai.approve(address(psm), 80e18); +// sDai.mint(buyer, 1); - assertEq(sDai.allowance(buyer, address(psm)), 80e18); +// psm.swapAssetOneToZero(80e18, 100e6, receiver); +// } - assertEq(sDai.balanceOf(buyer), 80e18); - assertEq(sDai.balanceOf(receiver), 0); - assertEq(sDai.balanceOf(address(psm)), 100e18); +// function test_swapAssetOneToZero_insufficientPsmBalanceBoundary() public { +// // Prove that values yield balance boundary +// // 0.8e12 * 1.25 = 1e12 == 1-e6 in 18 decimal precision +// assertEq(psm.previewSwapAssetOneToZero(80e18 + 0.8e12), 100e6 + 1); +// assertEq(psm.previewSwapAssetOneToZero(80e18 + 0.8e12 - 1), 100e6); - assertEq(usdc.balanceOf(buyer), 0); - assertEq(usdc.balanceOf(receiver), 0); - assertEq(usdc.balanceOf(address(psm)), 100e6); +// sDai.mint(buyer, 80e18 + 0.8e12); - psm.swapAssetOneToZero(80e18, 100e6, receiver); +// vm.startPrank(buyer); - assertEq(usdc.allowance(buyer, address(psm)), 0); +// sDai.approve(address(psm), 80e18 + 0.8e12); - assertEq(sDai.balanceOf(buyer), 0); - assertEq(sDai.balanceOf(receiver), 0); - assertEq(sDai.balanceOf(address(psm)), 180e18); +// vm.expectRevert("SafeERC20/transfer-failed"); +// psm.swapAssetOneToZero(80e18 + 0.8e12, 100e6, receiver); - assertEq(usdc.balanceOf(buyer), 0); - assertEq(usdc.balanceOf(receiver), 100e6); - assertEq(usdc.balanceOf(address(psm)), 0); - } +// psm.swapAssetOneToZero(80e18 + 0.8e12 - 1, 100e6, receiver); +// } -} +// function test_swapAssetOneToZero_sameReceiver() public assertAtomicPsmValueDoesNotChange { +// sDai.mint(buyer, 80e18); + +// vm.startPrank(buyer); + +// sDai.approve(address(psm), 80e18); + +// assertEq(sDai.allowance(buyer, address(psm)), 80e18); + +// assertEq(sDai.balanceOf(buyer), 80e18); +// assertEq(sDai.balanceOf(address(psm)), 100e18); + +// assertEq(usdc.balanceOf(buyer), 0); +// assertEq(usdc.balanceOf(address(psm)), 100e6); + +// psm.swapAssetOneToZero(80e18, 100e6, buyer); + +// assertEq(usdc.allowance(buyer, address(psm)), 0); + +// assertEq(sDai.balanceOf(buyer), 0); +// assertEq(sDai.balanceOf(address(psm)), 180e18); + +// assertEq(usdc.balanceOf(buyer), 100e6); +// assertEq(usdc.balanceOf(address(psm)), 0); +// } + +// function test_swapAssetOneToZero_differentReceiver() public assertAtomicPsmValueDoesNotChange { +// sDai.mint(buyer, 80e18); + +// vm.startPrank(buyer); + +// sDai.approve(address(psm), 80e18); + +// assertEq(sDai.allowance(buyer, address(psm)), 80e18); + +// assertEq(sDai.balanceOf(buyer), 80e18); +// assertEq(sDai.balanceOf(receiver), 0); +// assertEq(sDai.balanceOf(address(psm)), 100e18); + +// assertEq(usdc.balanceOf(buyer), 0); +// assertEq(usdc.balanceOf(receiver), 0); +// assertEq(usdc.balanceOf(address(psm)), 100e6); + +// psm.swapAssetOneToZero(80e18, 100e6, receiver); + +// assertEq(usdc.allowance(buyer, address(psm)), 0); + +// assertEq(sDai.balanceOf(buyer), 0); +// assertEq(sDai.balanceOf(receiver), 0); +// assertEq(sDai.balanceOf(address(psm)), 180e18); + +// assertEq(usdc.balanceOf(buyer), 0); +// assertEq(usdc.balanceOf(receiver), 100e6); +// assertEq(usdc.balanceOf(address(psm)), 0); +// } + +// } diff --git a/test/harnesses/PSMHarness.sol b/test/harnesses/PSMHarness.sol index 48dfcf9..2ad180d 100644 --- a/test/harnesses/PSMHarness.sol +++ b/test/harnesses/PSMHarness.sol @@ -5,8 +5,13 @@ import { PSM } from "src/PSM.sol"; contract PSMHarness is PSM { - constructor(address asset0_, address asset1_, address rateProvider_, uint256 initialBurnAmount_) - PSM(asset0_, asset1_, rateProvider_, initialBurnAmount_) {} + constructor( + address asset0_, + address asset1_, + address asset2_, + address rateProvider_, + uint256 initialBurnAmount_ + ) PSM(asset0_, asset1_, asset2_, rateProvider_, initialBurnAmount_) {} function getAssetValue(address asset, uint256 amount) external view returns (uint256) { return _getAssetValue(asset, amount); From cbea0eea44509d6ab806d3c45dfe8f07171c52d8 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 11:40:48 -0400 Subject: [PATCH 08/96] feat: get all swap tests working --- test/Swaps.t.sol | 290 +++++++++++++++++++---------------------------- 1 file changed, 114 insertions(+), 176 deletions(-) diff --git a/test/Swaps.t.sol b/test/Swaps.t.sol index 400b48c..4f76b7e 100644 --- a/test/Swaps.t.sol +++ b/test/Swaps.t.sol @@ -111,9 +111,9 @@ contract PSMSuccessTestsBase is PSMTestBase { function setUp() public override { super.setUp(); - dai.mint(address(psm), 1_000_000e18); - usdc.mint(address(psm), 1_000_000e6); - sDai.mint(address(psm), 1_000_000e18); + dai.mint(address(psm), DAI_TOKEN_MAX); + usdc.mint(address(psm), USDC_TOKEN_MAX); + sDai.mint(address(psm), SDAI_TOKEN_MAX); } function _swapTest( @@ -124,8 +124,9 @@ contract PSMSuccessTestsBase is PSMTestBase { address swapper, address receiver ) internal { - uint256 psmAssetInBalance = 1_000_000 * 10 ** assetIn.decimals(); - uint256 psmAssetOutBalance = 1_000_000 * 10 ** assetOut.decimals(); + // 1 trillion of each token corresponds to MAX values + uint256 psmAssetInBalance = 1_000_000_000_000 * 10 ** assetIn.decimals(); + uint256 psmAssetOutBalance = 1_000_000_000_000 * 10 ** assetOut.decimals(); assetIn.mint(swapper, amountIn); @@ -154,12 +155,14 @@ contract PSMSuccessTestsBase is PSMTestBase { } -contract PSMSwapTests is PSMSuccessTestsBase { +contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { address public swapper = makeAddr("swapper"); address public receiver = makeAddr("receiver"); - // DAI assetIn tests + /**********************************************************************************************/ + /*** DAI assetIn tests ***/ + /**********************************************************************************************/ function test_swap_daiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { _swapTest(dai, usdc, 100e18, 100e6, swapper, swapper); @@ -177,7 +180,43 @@ contract PSMSwapTests is PSMSuccessTestsBase { _swapTest(dai, sDai, 100e18, 80e18, swapper, receiver); } - // USDC assetIn tests + function testFuzz_swap_daiToUsdc( + uint256 amountIn, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); // Zero amount reverts + uint256 amountOut = amountIn / 1e12; + _swapTest(dai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + + function testFuzz_swap_daiToSDai( + uint256 amountIn, + uint256 exchangeRate, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates + exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + + rateProvider.__setConversionRate(exchangeRate); + + uint256 amountOut = amountIn * 1e27 / exchangeRate; + + _swapTest(dai, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + + /**********************************************************************************************/ + /*** USDC assetIn tests ***/ + /**********************************************************************************************/ function test_swap_usdcToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { _swapTest(usdc, dai, 100e6, 100e18, swapper, swapper); @@ -195,7 +234,43 @@ contract PSMSwapTests is PSMSuccessTestsBase { _swapTest(usdc, sDai, 100e6, 80e18, swapper, receiver); } - // sDai assetIn tests + function testFuzz_swap_usdcToDai( + uint256 amountIn, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); // Zero amount reverts + uint256 amountOut = amountIn * 1e12; + _swapTest(usdc, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + + function testFuzz_swap_usdcToSDai( + uint256 amountIn, + uint256 exchangeRate, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + + amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates + exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + + rateProvider.__setConversionRate(exchangeRate); + + uint256 amountOut = amountIn * 1e27 * 1e12 / exchangeRate; + + _swapTest(usdc, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } + + /**********************************************************************************************/ + /*** sDAI assetIn tests ***/ + /**********************************************************************************************/ function test_swap_sDaiToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { _swapTest(sDai, dai, 100e18, 125e18, swapper, swapper); @@ -213,181 +288,44 @@ contract PSMSwapTests is PSMSuccessTestsBase { _swapTest(sDai, usdc, 100e18, 125e6, swapper, receiver); } + function testFuzz_swap_sDaiToDai( + uint256 amountIn, + uint256 exchangeRate, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); + amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates + exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate - // function test_swapAssetZeroToOne_differentReceiver() public assertAtomicPsmValueDoesNotChange { - // usdc.mint(buyer, 100e6); - - // vm.startPrank(buyer); - - // usdc.approve(address(psm), 100e6); + rateProvider.__setConversionRate(exchangeRate); - // assertEq(usdc.allowance(buyer, address(psm)), 100e6); + uint256 amountOut = amountIn * exchangeRate / 1e27; - // assertEq(sDai.balanceOf(buyer), 0); - // assertEq(sDai.balanceOf(receiver), 0); - // assertEq(sDai.balanceOf(address(psm)), 100e18); + _swapTest(sDai, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } - // assertEq(usdc.balanceOf(buyer), 100e6); - // assertEq(usdc.balanceOf(receiver), 0); - // assertEq(usdc.balanceOf(address(psm)), 100e6); + function testFuzz_swap_sDaiToUsdc( + uint256 amountIn, + uint256 exchangeRate, + address fuzzSwapper, + address fuzzReceiver + ) public { + vm.assume(fuzzSwapper != address(psm)); + vm.assume(fuzzReceiver != address(psm)); + vm.assume(fuzzReceiver != address(0)); - // psm.swapAssetZeroToOne(100e6, 80e18, receiver); + amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates + exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate - // assertEq(usdc.allowance(buyer, address(psm)), 0); + rateProvider.__setConversionRate(exchangeRate); - // assertEq(sDai.balanceOf(buyer), 0); - // assertEq(sDai.balanceOf(receiver), 80e18); - // assertEq(sDai.balanceOf(address(psm)), 20e18); + uint256 amountOut = amountIn * exchangeRate / 1e27 / 1e12; - // assertEq(usdc.balanceOf(buyer), 0); - // assertEq(usdc.balanceOf(receiver), 0); - // assertEq(usdc.balanceOf(address(psm)), 200e6); - // } + _swapTest(sDai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); + } } - -// contract PSMSwapAssetOneToZeroTests is PSMTestBase { - -// address public buyer = makeAddr("buyer"); -// address public receiver = makeAddr("receiver"); - -// function setUp() public override { -// super.setUp(); - -// usdc.mint(address(psm), 100e6); -// sDai.mint(address(psm), 100e18); -// } - -// function test_swapAssetOneToZero_amountZero() public { -// vm.expectRevert("PSM/invalid-amountIn"); -// psm.swapAssetOneToZero(0, 0, receiver); -// } - -// function test_swapAssetZeroToOne_receiverZero() public { -// vm.expectRevert("PSM/invalid-receiver"); -// psm.swapAssetOneToZero(100e6, 80e18, address(0)); -// } - -// function test_swapAssetOneToZero_minAmountOutBoundary() public { -// sDai.mint(buyer, 80e18); - -// vm.startPrank(buyer); - -// sDai.approve(address(psm), 80e18); - -// uint256 expectedAmountOut = psm.previewSwapAssetOneToZero(80e18); - -// assertEq(expectedAmountOut, 100e6); - -// vm.expectRevert("PSM/amountOut-too-low"); -// psm.swapAssetOneToZero(80e18, 100e6 + 1, receiver); - -// psm.swapAssetOneToZero(80e18, 100e6, receiver); -// } - -// function test_swapAssetOneToZero_insufficientApproveBoundary() public { -// sDai.mint(buyer, 80e18); - -// vm.startPrank(buyer); - -// sDai.approve(address(psm), 80e18 - 1); - -// vm.expectRevert("SafeERC20/transfer-from-failed"); -// psm.swapAssetOneToZero(80e18, 100e6, receiver); - -// sDai.approve(address(psm), 80e18); - -// psm.swapAssetOneToZero(80e18, 100e6, receiver); -// } - -// function test_swapAssetOneToZero_insufficientUserBalanceBoundary() public { -// sDai.mint(buyer, 80e18 - 1); - -// vm.startPrank(buyer); - -// sDai.approve(address(psm), 80e18); - -// vm.expectRevert("SafeERC20/transfer-from-failed"); -// psm.swapAssetOneToZero(80e18, 100e6, receiver); - -// sDai.mint(buyer, 1); - -// psm.swapAssetOneToZero(80e18, 100e6, receiver); -// } - -// function test_swapAssetOneToZero_insufficientPsmBalanceBoundary() public { -// // Prove that values yield balance boundary -// // 0.8e12 * 1.25 = 1e12 == 1-e6 in 18 decimal precision -// assertEq(psm.previewSwapAssetOneToZero(80e18 + 0.8e12), 100e6 + 1); -// assertEq(psm.previewSwapAssetOneToZero(80e18 + 0.8e12 - 1), 100e6); - -// sDai.mint(buyer, 80e18 + 0.8e12); - -// vm.startPrank(buyer); - -// sDai.approve(address(psm), 80e18 + 0.8e12); - -// vm.expectRevert("SafeERC20/transfer-failed"); -// psm.swapAssetOneToZero(80e18 + 0.8e12, 100e6, receiver); - -// psm.swapAssetOneToZero(80e18 + 0.8e12 - 1, 100e6, receiver); -// } - -// function test_swapAssetOneToZero_sameReceiver() public assertAtomicPsmValueDoesNotChange { -// sDai.mint(buyer, 80e18); - -// vm.startPrank(buyer); - -// sDai.approve(address(psm), 80e18); - -// assertEq(sDai.allowance(buyer, address(psm)), 80e18); - -// assertEq(sDai.balanceOf(buyer), 80e18); -// assertEq(sDai.balanceOf(address(psm)), 100e18); - -// assertEq(usdc.balanceOf(buyer), 0); -// assertEq(usdc.balanceOf(address(psm)), 100e6); - -// psm.swapAssetOneToZero(80e18, 100e6, buyer); - -// assertEq(usdc.allowance(buyer, address(psm)), 0); - -// assertEq(sDai.balanceOf(buyer), 0); -// assertEq(sDai.balanceOf(address(psm)), 180e18); - -// assertEq(usdc.balanceOf(buyer), 100e6); -// assertEq(usdc.balanceOf(address(psm)), 0); -// } - -// function test_swapAssetOneToZero_differentReceiver() public assertAtomicPsmValueDoesNotChange { -// sDai.mint(buyer, 80e18); - -// vm.startPrank(buyer); - -// sDai.approve(address(psm), 80e18); - -// assertEq(sDai.allowance(buyer, address(psm)), 80e18); - -// assertEq(sDai.balanceOf(buyer), 80e18); -// assertEq(sDai.balanceOf(receiver), 0); -// assertEq(sDai.balanceOf(address(psm)), 100e18); - -// assertEq(usdc.balanceOf(buyer), 0); -// assertEq(usdc.balanceOf(receiver), 0); -// assertEq(usdc.balanceOf(address(psm)), 100e6); - -// psm.swapAssetOneToZero(80e18, 100e6, receiver); - -// assertEq(usdc.allowance(buyer, address(psm)), 0); - -// assertEq(sDai.balanceOf(buyer), 0); -// assertEq(sDai.balanceOf(receiver), 0); -// assertEq(sDai.balanceOf(address(psm)), 180e18); - -// assertEq(usdc.balanceOf(buyer), 0); -// assertEq(usdc.balanceOf(receiver), 100e6); -// assertEq(usdc.balanceOf(address(psm)), 0); -// } - -// } From 7d1101d313891751f4a952421fdd94eb69261ae7 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 11:49:55 -0400 Subject: [PATCH 09/96] fix: update for three assets in logic --- src/PSM.sol | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index 3f23489..da5fbb1 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -128,7 +128,7 @@ contract PSM { /**********************************************************************************************/ function previewDeposit(address asset, uint256 assets) public view returns (uint256) { - require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); + require(_isValidAsset(asset), "PSM/invalid-asset"); // Convert amount to 1e18 precision denominated in value of asset0 then convert to shares. return convertToShares(_getAssetValue(asset, assets)); @@ -137,7 +137,7 @@ contract PSM { function previewWithdraw(address asset, uint256 maxAssetsToWithdraw) public view returns (uint256 sharesToBurn, uint256 assetsWithdrawn) { - require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); + require(_isValidAsset(asset), "PSM/invalid-asset"); uint256 assetBalance = IERC20(asset).balanceOf(address(this)); @@ -185,7 +185,7 @@ contract PSM { /**********************************************************************************************/ function convertToAssets(address asset, uint256 numShares) public view returns (uint256) { - require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); + require(_isValidAsset(asset), "PSM/invalid-asset"); return _getAssetsByValue(asset, convertToAssetValue(numShares)); } @@ -207,7 +207,7 @@ contract PSM { } function convertToShares(address asset, uint256 assets) public view returns (uint256) { - require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); + require(_isValidAsset(asset), "PSM/invalid-asset"); return convertToShares(_getAssetValue(asset, assets)); } @@ -218,7 +218,8 @@ contract PSM { // TODO: Refactor for three assets function getPsmTotalValue() public view returns (uint256) { return _getAsset0Value(asset0.balanceOf(address(this))) - + _getAsset1Value(asset1.balanceOf(address(this))); + + _getAsset1Value(asset1.balanceOf(address(this))) + + _getAsset2Value(asset2.balanceOf(address(this))); } /**********************************************************************************************/ @@ -240,9 +241,10 @@ contract PSM { } function _getAssetValue(address asset, uint256 amount) internal view returns (uint256) { - return asset == address(asset0) - ? _getAsset0Value(amount) - : _getAsset1Value(amount); + if (asset == address(asset0)) return _getAsset0Value(amount); + else if (asset == address(asset1)) return _getAsset1Value(amount); + else if (asset == address(asset2)) return _getAsset2Value(amount); + else revert("PSM/invalid-asset"); } function _getAssetsByValue(address asset, uint256 assetValue) internal view returns (uint256) { @@ -262,8 +264,16 @@ contract PSM { } function _getAsset1Value(uint256 amount) internal view returns (uint256) { + return amount * 1e18 / asset1Precision; + } + + function _getAsset2Value(uint256 amount) internal view returns (uint256) { // NOTE: Multiplying by 1e18 and dividing by 1e9 cancels to 1e9 in denominator - return amount * IRateProviderLike(rateProvider).getConversionRate() / 1e9 / asset1Precision; + return amount * IRateProviderLike(rateProvider).getConversionRate() / 1e9 / asset2Precision; + } + + function _isValidAsset(address asset) internal view returns (bool) { + return asset == address(asset0) || asset == address(asset1) || asset == address(asset2); } function _previewSwapToAsset2(uint256 amountIn, uint256 assetInPrecision) From 80440e0539b6b42c2613ccf60ccbf21faecd9a29 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 12:02:57 -0400 Subject: [PATCH 10/96] feat: all tests passing --- src/PSM.sol | 7 +- test/Getters.t.sol | 178 +++++++++++++++++++++------------- test/harnesses/PSMHarness.sol | 4 + 3 files changed, 117 insertions(+), 72 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index da5fbb1..3c7c9a0 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -248,14 +248,13 @@ contract PSM { } function _getAssetsByValue(address asset, uint256 assetValue) internal view returns (uint256) { - if (asset == address(asset0)) { - return assetValue * asset0Precision / 1e18; - } + if (asset == address(asset0)) return assetValue * asset0Precision / 1e18; + else if (asset == address(asset1)) return assetValue * asset1Precision / 1e18; // NOTE: Multiplying by 1e27 and dividing by 1e18 cancels to 1e9 in numerator return assetValue * 1e9 - * asset1Precision + * asset2Precision / IRateProviderLike(rateProvider).getConversionRate(); } diff --git a/test/Getters.t.sol b/test/Getters.t.sol index 6daba33..43fc61d 100644 --- a/test/Getters.t.sol +++ b/test/Getters.t.sol @@ -23,126 +23,168 @@ contract PSMHarnessTests is PSMTestBase { } function test_getAsset0Value() public view { - assertEq(psmHarness.getAsset0Value(1), 1e12); - assertEq(psmHarness.getAsset0Value(2), 2e12); - assertEq(psmHarness.getAsset0Value(3), 3e12); + assertEq(psmHarness.getAsset0Value(1), 1); + assertEq(psmHarness.getAsset0Value(2), 2); + assertEq(psmHarness.getAsset0Value(3), 3); - assertEq(psmHarness.getAsset0Value(100e6), 100e18); - assertEq(psmHarness.getAsset0Value(200e6), 200e18); - assertEq(psmHarness.getAsset0Value(300e6), 300e18); + assertEq(psmHarness.getAsset0Value(100e6), 100e6); + assertEq(psmHarness.getAsset0Value(200e6), 200e6); + assertEq(psmHarness.getAsset0Value(300e6), 300e6); - assertEq(psmHarness.getAsset0Value(100_000_000_000e6), 100_000_000_000e18); - assertEq(psmHarness.getAsset0Value(200_000_000_000e6), 200_000_000_000e18); - assertEq(psmHarness.getAsset0Value(300_000_000_000e6), 300_000_000_000e18); + assertEq(psmHarness.getAsset0Value(100_000_000_000e6), 100_000_000_000e6); + assertEq(psmHarness.getAsset0Value(200_000_000_000e6), 200_000_000_000e6); + assertEq(psmHarness.getAsset0Value(300_000_000_000e6), 300_000_000_000e6); } function testFuzz_getAsset0Value(uint256 amount) public view { amount = _bound(amount, 0, 1e45); - assertEq(psmHarness.getAsset0Value(amount), amount * 1e12); + assertEq(psmHarness.getAsset0Value(amount), amount); } - function test_getAsset1Value() public { - assertEq(psmHarness.getAsset1Value(1), 1); - assertEq(psmHarness.getAsset1Value(2), 2); - assertEq(psmHarness.getAsset1Value(3), 3); - assertEq(psmHarness.getAsset1Value(4), 5); + function test_getAsset1Value() public view { + assertEq(psmHarness.getAsset1Value(1), 1e12); + assertEq(psmHarness.getAsset1Value(2), 2e12); + assertEq(psmHarness.getAsset1Value(3), 3e12); - assertEq(psmHarness.getAsset1Value(1e18), 1.25e18); - assertEq(psmHarness.getAsset1Value(2e18), 2.5e18); - assertEq(psmHarness.getAsset1Value(3e18), 3.75e18); - assertEq(psmHarness.getAsset1Value(4e18), 5e18); + assertEq(psmHarness.getAsset1Value(100e6), 100e18); + assertEq(psmHarness.getAsset1Value(200e6), 200e18); + assertEq(psmHarness.getAsset1Value(300e6), 300e18); + + assertEq(psmHarness.getAsset1Value(100_000_000_000e6), 100_000_000_000e18); + assertEq(psmHarness.getAsset1Value(200_000_000_000e6), 200_000_000_000e18); + assertEq(psmHarness.getAsset1Value(300_000_000_000e6), 300_000_000_000e18); + } + + function testFuzz_getAsset1Value(uint256 amount) public view { + amount = _bound(amount, 0, 1e45); + + assertEq(psmHarness.getAsset1Value(amount), amount * 1e12); + } + + function test_getAsset2Value() public { + assertEq(psmHarness.getAsset2Value(1), 1); + assertEq(psmHarness.getAsset2Value(2), 2); + assertEq(psmHarness.getAsset2Value(3), 3); + assertEq(psmHarness.getAsset2Value(4), 5); + + assertEq(psmHarness.getAsset2Value(1e18), 1.25e18); + assertEq(psmHarness.getAsset2Value(2e18), 2.5e18); + assertEq(psmHarness.getAsset2Value(3e18), 3.75e18); + assertEq(psmHarness.getAsset2Value(4e18), 5e18); rateProvider.__setConversionRate(1.6e27); - assertEq(psmHarness.getAsset1Value(1), 1); - assertEq(psmHarness.getAsset1Value(2), 3); - assertEq(psmHarness.getAsset1Value(3), 4); - assertEq(psmHarness.getAsset1Value(4), 6); + assertEq(psmHarness.getAsset2Value(1), 1); + assertEq(psmHarness.getAsset2Value(2), 3); + assertEq(psmHarness.getAsset2Value(3), 4); + assertEq(psmHarness.getAsset2Value(4), 6); - assertEq(psmHarness.getAsset1Value(1e18), 1.6e18); - assertEq(psmHarness.getAsset1Value(2e18), 3.2e18); - assertEq(psmHarness.getAsset1Value(3e18), 4.8e18); - assertEq(psmHarness.getAsset1Value(4e18), 6.4e18); + assertEq(psmHarness.getAsset2Value(1e18), 1.6e18); + assertEq(psmHarness.getAsset2Value(2e18), 3.2e18); + assertEq(psmHarness.getAsset2Value(3e18), 4.8e18); + assertEq(psmHarness.getAsset2Value(4e18), 6.4e18); rateProvider.__setConversionRate(0.8e27); - assertEq(psmHarness.getAsset1Value(1), 0); - assertEq(psmHarness.getAsset1Value(2), 1); - assertEq(psmHarness.getAsset1Value(3), 2); - assertEq(psmHarness.getAsset1Value(4), 3); + assertEq(psmHarness.getAsset2Value(1), 0); + assertEq(psmHarness.getAsset2Value(2), 1); + assertEq(psmHarness.getAsset2Value(3), 2); + assertEq(psmHarness.getAsset2Value(4), 3); - assertEq(psmHarness.getAsset1Value(1e18), 0.8e18); - assertEq(psmHarness.getAsset1Value(2e18), 1.6e18); - assertEq(psmHarness.getAsset1Value(3e18), 2.4e18); - assertEq(psmHarness.getAsset1Value(4e18), 3.2e18); + assertEq(psmHarness.getAsset2Value(1e18), 0.8e18); + assertEq(psmHarness.getAsset2Value(2e18), 1.6e18); + assertEq(psmHarness.getAsset2Value(3e18), 2.4e18); + assertEq(psmHarness.getAsset2Value(4e18), 3.2e18); } - function testFuzz_getAsset1Value(uint256 conversionRate, uint256 amount) public { + function testFuzz_getAsset2Value(uint256 conversionRate, uint256 amount) public { conversionRate = _bound(conversionRate, 0, 1000e27); amount = _bound(amount, 0, SDAI_TOKEN_MAX); rateProvider.__setConversionRate(conversionRate); - assertEq(psmHarness.getAsset1Value(amount), amount * conversionRate / 1e27); + assertEq(psmHarness.getAsset2Value(amount), amount * conversionRate / 1e27); } function test_getAssetValue() public view { - assertEq(psmHarness.getAssetValue(address(usdc), 1), psmHarness.getAsset0Value(1)); - assertEq(psmHarness.getAssetValue(address(usdc), 2), psmHarness.getAsset0Value(2)); - assertEq(psmHarness.getAssetValue(address(usdc), 3), psmHarness.getAsset0Value(3)); + assertEq(psmHarness.getAssetValue(address(dai), 1), psmHarness.getAsset0Value(1)); + assertEq(psmHarness.getAssetValue(address(dai), 2), psmHarness.getAsset0Value(2)); + assertEq(psmHarness.getAssetValue(address(dai), 3), psmHarness.getAsset0Value(3)); - assertEq(psmHarness.getAssetValue(address(usdc), 1e6), psmHarness.getAsset0Value(1e6)); - assertEq(psmHarness.getAssetValue(address(usdc), 2e6), psmHarness.getAsset0Value(2e6)); - assertEq(psmHarness.getAssetValue(address(usdc), 3e6), psmHarness.getAsset0Value(3e6)); + assertEq(psmHarness.getAssetValue(address(dai), 1e18), psmHarness.getAsset0Value(1e18)); + assertEq(psmHarness.getAssetValue(address(dai), 2e18), psmHarness.getAsset0Value(2e18)); + assertEq(psmHarness.getAssetValue(address(dai), 3e18), psmHarness.getAsset0Value(3e18)); - assertEq(psmHarness.getAssetValue(address(sDai), 1), psmHarness.getAsset1Value(1)); - assertEq(psmHarness.getAssetValue(address(sDai), 2), psmHarness.getAsset1Value(2)); - assertEq(psmHarness.getAssetValue(address(sDai), 3), psmHarness.getAsset1Value(3)); + assertEq(psmHarness.getAssetValue(address(usdc), 1), psmHarness.getAsset1Value(1)); + assertEq(psmHarness.getAssetValue(address(usdc), 2), psmHarness.getAsset1Value(2)); + assertEq(psmHarness.getAssetValue(address(usdc), 3), psmHarness.getAsset1Value(3)); - assertEq(psmHarness.getAssetValue(address(sDai), 1e18), psmHarness.getAsset1Value(1e18)); - assertEq(psmHarness.getAssetValue(address(sDai), 2e18), psmHarness.getAsset1Value(2e18)); - assertEq(psmHarness.getAssetValue(address(sDai), 3e18), psmHarness.getAsset1Value(3e18)); + assertEq(psmHarness.getAssetValue(address(usdc), 1e6), psmHarness.getAsset1Value(1e6)); + assertEq(psmHarness.getAssetValue(address(usdc), 2e6), psmHarness.getAsset1Value(2e6)); + assertEq(psmHarness.getAssetValue(address(usdc), 3e6), psmHarness.getAsset1Value(3e6)); + + assertEq(psmHarness.getAssetValue(address(sDai), 1), psmHarness.getAsset2Value(1)); + assertEq(psmHarness.getAssetValue(address(sDai), 2), psmHarness.getAsset2Value(2)); + assertEq(psmHarness.getAssetValue(address(sDai), 3), psmHarness.getAsset2Value(3)); + + assertEq(psmHarness.getAssetValue(address(sDai), 1e18), psmHarness.getAsset2Value(1e18)); + assertEq(psmHarness.getAssetValue(address(sDai), 2e18), psmHarness.getAsset2Value(2e18)); + assertEq(psmHarness.getAssetValue(address(sDai), 3e18), psmHarness.getAsset2Value(3e18)); } function testFuzz_getAssetValue(uint256 amount) public view { amount = _bound(amount, 0, SDAI_TOKEN_MAX); - assertEq(psmHarness.getAssetValue(address(usdc), amount), psmHarness.getAsset0Value(amount)); - assertEq(psmHarness.getAssetValue(address(sDai), amount), psmHarness.getAsset1Value(amount)); + assertEq(psmHarness.getAssetValue(address(dai), amount), psmHarness.getAsset0Value(amount)); + assertEq(psmHarness.getAssetValue(address(usdc), amount), psmHarness.getAsset1Value(amount)); + assertEq(psmHarness.getAssetValue(address(sDai), amount), psmHarness.getAsset2Value(amount)); } - // NOTE: This is because of structure of function, but all functions that call the internal - // function have a `require(asset == address(asset0) || asset == address(asset1))` - function test_getAssetValue_zeroAddress() public view { - assertEq(psmHarness.getAssetValue(address(0), 1e18), psmHarness.getAsset1Value(1e18)); + function test_getAssetValue_zeroAddress() public { + vm.expectRevert("PSM/invalid-asset"); + psmHarness.getAssetValue(address(0), 1); } function test_getAssetsByValue() public view { - assertEq(psmHarness.getAssetsByValue(address(usdc), 1), 0); - assertEq(psmHarness.getAssetsByValue(address(usdc), 2), 0); - assertEq(psmHarness.getAssetsByValue(address(usdc), 3), 0); + assertEq(psmHarness.getAssetsByValue(address(dai), 1), 1); + assertEq(psmHarness.getAssetsByValue(address(dai), 2), 2); + assertEq(psmHarness.getAssetsByValue(address(dai), 3), 3); + + assertEq(psmHarness.getAssetsByValue(address(dai), 1e18), 1e18); + assertEq(psmHarness.getAssetsByValue(address(dai), 2e18), 2e18); + assertEq(psmHarness.getAssetsByValue(address(dai), 3e18), 3e18); + + // assertEq(psmHarness.getAssetsByValue(address(usdc), 1), 0); + // assertEq(psmHarness.getAssetsByValue(address(usdc), 2), 0); + // assertEq(psmHarness.getAssetsByValue(address(usdc), 3), 0); - assertEq(psmHarness.getAssetsByValue(address(usdc), 1e18), 1e6); - assertEq(psmHarness.getAssetsByValue(address(usdc), 2e18), 2e6); - assertEq(psmHarness.getAssetsByValue(address(usdc), 3e18), 3e6); + // assertEq(psmHarness.getAssetsByValue(address(usdc), 1e18), 1e6); + // assertEq(psmHarness.getAssetsByValue(address(usdc), 2e18), 2e6); + // assertEq(psmHarness.getAssetsByValue(address(usdc), 3e18), 3e6); - assertEq(psmHarness.getAssetsByValue(address(sDai), 1), 0); - assertEq(psmHarness.getAssetsByValue(address(sDai), 2), 1); - assertEq(psmHarness.getAssetsByValue(address(sDai), 3), 2); + // assertEq(psmHarness.getAssetsByValue(address(sDai), 1), 0); + // assertEq(psmHarness.getAssetsByValue(address(sDai), 2), 1); + // assertEq(psmHarness.getAssetsByValue(address(sDai), 3), 2); - assertEq(psmHarness.getAssetsByValue(address(sDai), 1e18), 0.8e18); - assertEq(psmHarness.getAssetsByValue(address(sDai), 2e18), 1.6e18); - assertEq(psmHarness.getAssetsByValue(address(sDai), 3e18), 2.4e18); + // assertEq(psmHarness.getAssetsByValue(address(sDai), 1e18), 0.8e18); + // assertEq(psmHarness.getAssetsByValue(address(sDai), 2e18), 1.6e18); + // assertEq(psmHarness.getAssetsByValue(address(sDai), 3e18), 2.4e18); } function testFuzz_getAssetsByValue_asset0(uint256 amount) public view { + amount = _bound(amount, 0, DAI_TOKEN_MAX); + + assertEq(psmHarness.getAssetsByValue(address(dai), amount), amount); + } + + function testFuzz_getAssetsByValue_asset1(uint256 amount) public view { amount = _bound(amount, 0, USDC_TOKEN_MAX); assertEq(psmHarness.getAssetsByValue(address(usdc), amount), amount / 1e12); } - function testFuzz_getAssetsByValue_asset1(uint256 conversionRate, uint256 amount) public { + function testFuzz_getAssetsByValue_asset2(uint256 conversionRate, uint256 amount) public { // NOTE: 0.0001e27 considered lower bound for overflow considerations conversionRate = bound(conversionRate, 0.0001e27, 1000e27); amount = bound(amount, 0, SDAI_TOKEN_MAX); diff --git a/test/harnesses/PSMHarness.sol b/test/harnesses/PSMHarness.sol index 2ad180d..7775cd5 100644 --- a/test/harnesses/PSMHarness.sol +++ b/test/harnesses/PSMHarness.sol @@ -29,4 +29,8 @@ contract PSMHarness is PSM { return _getAsset1Value(amount); } + function getAsset2Value(uint256 amount) external view returns (uint256) { + return _getAsset2Value(amount); + } + } From 51b980de1223e335674532194aeba8b4275d873f Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 12:04:09 -0400 Subject: [PATCH 11/96] fix: rm commented out test --- test/Getters.t.sol | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/Getters.t.sol b/test/Getters.t.sol index 43fc61d..d2302fd 100644 --- a/test/Getters.t.sol +++ b/test/Getters.t.sol @@ -155,21 +155,21 @@ contract PSMHarnessTests is PSMTestBase { assertEq(psmHarness.getAssetsByValue(address(dai), 2e18), 2e18); assertEq(psmHarness.getAssetsByValue(address(dai), 3e18), 3e18); - // assertEq(psmHarness.getAssetsByValue(address(usdc), 1), 0); - // assertEq(psmHarness.getAssetsByValue(address(usdc), 2), 0); - // assertEq(psmHarness.getAssetsByValue(address(usdc), 3), 0); + assertEq(psmHarness.getAssetsByValue(address(usdc), 1), 0); + assertEq(psmHarness.getAssetsByValue(address(usdc), 2), 0); + assertEq(psmHarness.getAssetsByValue(address(usdc), 3), 0); - // assertEq(psmHarness.getAssetsByValue(address(usdc), 1e18), 1e6); - // assertEq(psmHarness.getAssetsByValue(address(usdc), 2e18), 2e6); - // assertEq(psmHarness.getAssetsByValue(address(usdc), 3e18), 3e6); + assertEq(psmHarness.getAssetsByValue(address(usdc), 1e18), 1e6); + assertEq(psmHarness.getAssetsByValue(address(usdc), 2e18), 2e6); + assertEq(psmHarness.getAssetsByValue(address(usdc), 3e18), 3e6); - // assertEq(psmHarness.getAssetsByValue(address(sDai), 1), 0); - // assertEq(psmHarness.getAssetsByValue(address(sDai), 2), 1); - // assertEq(psmHarness.getAssetsByValue(address(sDai), 3), 2); + assertEq(psmHarness.getAssetsByValue(address(sDai), 1), 0); + assertEq(psmHarness.getAssetsByValue(address(sDai), 2), 1); + assertEq(psmHarness.getAssetsByValue(address(sDai), 3), 2); - // assertEq(psmHarness.getAssetsByValue(address(sDai), 1e18), 0.8e18); - // assertEq(psmHarness.getAssetsByValue(address(sDai), 2e18), 1.6e18); - // assertEq(psmHarness.getAssetsByValue(address(sDai), 3e18), 2.4e18); + assertEq(psmHarness.getAssetsByValue(address(sDai), 1e18), 0.8e18); + assertEq(psmHarness.getAssetsByValue(address(sDai), 2e18), 1.6e18); + assertEq(psmHarness.getAssetsByValue(address(sDai), 3e18), 2.4e18); } function testFuzz_getAssetsByValue_asset0(uint256 amount) public view { From e6b67c6de3b8f86e6a2a523e41960ea4a56a821b Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 15:44:00 -0400 Subject: [PATCH 12/96] feat: add preview swap tests --- test/Getters.t.sol | 1 + test/InflationAttack.t.sol | 1 + test/Previews.t.sol | 161 +++++++++++++++++++++---------------- 3 files changed, 92 insertions(+), 71 deletions(-) diff --git a/test/Getters.t.sol b/test/Getters.t.sol index d2302fd..502625c 100644 --- a/test/Getters.t.sol +++ b/test/Getters.t.sol @@ -196,6 +196,7 @@ contract PSMHarnessTests is PSMTestBase { } +// TODO: Update for three assets contract GetPsmTotalValueTests is PSMTestBase { function test_getPsmTotalValue_balanceChanges() public { diff --git a/test/InflationAttack.t.sol b/test/InflationAttack.t.sol index b14e505..79ec0eb 100644 --- a/test/InflationAttack.t.sol +++ b/test/InflationAttack.t.sol @@ -9,6 +9,7 @@ import { PSMTestBase } from "test/PSMTestBase.sol"; contract InflationAttackTests is PSMTestBase { + // TODO: Decide if DAI test is needed function test_inflationAttack_noInitialBurnAmount() public { psm = new PSM(address(dai), address(usdc), address(sDai), address(rateProvider), 0); diff --git a/test/Previews.t.sol b/test/Previews.t.sol index b00218a..3e255ef 100644 --- a/test/Previews.t.sol +++ b/test/Previews.t.sol @@ -5,98 +5,117 @@ import "forge-std/Test.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -// contract PSMPreviewFunctionTests is PSMTestBase { +contract PSMPreviewSwapFailureTests is PSMTestBase { -// function test_previewSwapAssetZeroToOne() public { -// assertEq(psm.previewSwapAssetZeroToOne(1), 0.8e12); -// assertEq(psm.previewSwapAssetZeroToOne(2), 1.6e12); -// assertEq(psm.previewSwapAssetZeroToOne(3), 2.4e12); + function test_previewSwap_invalidAssetIn() public { + vm.expectRevert("PSM/invalid-asset"); + psm.previewSwap(makeAddr("other-token"), address(usdc), 1); + } -// assertEq(psm.previewSwapAssetZeroToOne(1e6), 0.8e18); -// assertEq(psm.previewSwapAssetZeroToOne(2e6), 1.6e18); -// assertEq(psm.previewSwapAssetZeroToOne(3e6), 2.4e18); + function test_previewSwap_invalidAssetOut() public { + vm.expectRevert("PSM/invalid-asset"); + psm.previewSwap(address(usdc), makeAddr("other-token"), 1); + } -// assertEq(psm.previewSwapAssetZeroToOne(1.000001e6), 0.8000008e18); +} -// rateProvider.__setConversionRate(1.6e27); +// TODO: Determine if 10 billion is too low of an upper bound for sDAI swaps, +// if exchange rate lower bound should be raised (applies to swap tests too). -// assertEq(psm.previewSwapAssetZeroToOne(1), 0.625e12); -// assertEq(psm.previewSwapAssetZeroToOne(2), 1.25e12); -// assertEq(psm.previewSwapAssetZeroToOne(3), 1.875e12); +contract PSMPreviewSwapDaiAssetInTests is PSMTestBase { -// assertEq(psm.previewSwapAssetZeroToOne(1e6), 0.625e18); -// assertEq(psm.previewSwapAssetZeroToOne(2e6), 1.25e18); -// assertEq(psm.previewSwapAssetZeroToOne(3e6), 1.875e18); + function test_previewSwap_daiToUsdc() public view { + assertEq(psm.previewSwap(address(dai), address(usdc), 1e12 - 1), 0); + assertEq(psm.previewSwap(address(dai), address(usdc), 1e12), 1); -// assertEq(psm.previewSwapAssetZeroToOne(1.000001e6), 0.625000625e18); + assertEq(psm.previewSwap(address(dai), address(usdc), 1e18), 1e6); + } -// rateProvider.__setConversionRate(0.8e27); + function testFuzz_previewSwap_daiToUsdc(uint256 amountIn) public view { + amountIn = _bound(amountIn, 0, DAI_TOKEN_MAX); -// assertEq(psm.previewSwapAssetZeroToOne(1), 1.25e12); -// assertEq(psm.previewSwapAssetZeroToOne(2), 2.5e12); -// assertEq(psm.previewSwapAssetZeroToOne(3), 3.75e12); + assertEq(psm.previewSwap(address(dai), address(usdc), amountIn), amountIn / 1e12); + } -// assertEq(psm.previewSwapAssetZeroToOne(1e6), 1.25e18); -// assertEq(psm.previewSwapAssetZeroToOne(2e6), 2.5e18); -// assertEq(psm.previewSwapAssetZeroToOne(3e6), 3.75e18); + function test_previewSwap_daiToSDai() public view { + assertEq(psm.previewSwap(address(dai), address(sDai), 1e18), 0.8e18); + } -// assertEq(psm.previewSwapAssetZeroToOne(1.000001e6), 1.25000125e18); -// } + function testFuzz_previewSwap_daiToSDai(uint256 amountIn, uint256 exchangeRate) public { + amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates + exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate -// function test_previewSwapAssetOneToZero() public { -// assertEq(psm.previewSwapAssetOneToZero(1), 0); -// assertEq(psm.previewSwapAssetOneToZero(2), 0); -// assertEq(psm.previewSwapAssetOneToZero(3), 0); -// assertEq(psm.previewSwapAssetOneToZero(4), 0); + rateProvider.__setConversionRate(exchangeRate); -// // 1e-6 with 18 decimal precision -// assertEq(psm.previewSwapAssetOneToZero(1e12), 1); -// assertEq(psm.previewSwapAssetOneToZero(2e12), 2); -// assertEq(psm.previewSwapAssetOneToZero(3e12), 3); -// assertEq(psm.previewSwapAssetOneToZero(4e12), 5); + uint256 amountOut = amountIn * 1e27 / exchangeRate; -// assertEq(psm.previewSwapAssetOneToZero(1e18), 1.25e6); -// assertEq(psm.previewSwapAssetOneToZero(2e18), 2.5e6); -// assertEq(psm.previewSwapAssetOneToZero(3e18), 3.75e6); -// assertEq(psm.previewSwapAssetOneToZero(4e18), 5e6); + assertEq(psm.previewSwap(address(dai), address(sDai), amountIn), amountOut); + } -// assertEq(psm.previewSwapAssetOneToZero(1.000001e18), 1.250001e6); +} -// rateProvider.__setConversionRate(1.6e27); +contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { -// assertEq(psm.previewSwapAssetOneToZero(1), 0); -// assertEq(psm.previewSwapAssetOneToZero(2), 0); -// assertEq(psm.previewSwapAssetOneToZero(3), 0); -// assertEq(psm.previewSwapAssetOneToZero(4), 0); + function test_previewSwap_usdcToDai() public view { + assertEq(psm.previewSwap(address(usdc), address(dai), 1e6), 1e18); + } -// // 1e-6 with 18 decimal precision -// assertEq(psm.previewSwapAssetOneToZero(1e12), 1); -// assertEq(psm.previewSwapAssetOneToZero(2e12), 3); -// assertEq(psm.previewSwapAssetOneToZero(3e12), 4); -// assertEq(psm.previewSwapAssetOneToZero(4e12), 6); + function testFuzz_previewSwap_usdcToDai(uint256 amountIn) public view { + amountIn = _bound(amountIn, 0, USDC_TOKEN_MAX); -// assertEq(psm.previewSwapAssetOneToZero(1e18), 1.6e6); -// assertEq(psm.previewSwapAssetOneToZero(2e18), 3.2e6); -// assertEq(psm.previewSwapAssetOneToZero(3e18), 4.8e6); -// assertEq(psm.previewSwapAssetOneToZero(4e18), 6.4e6); + assertEq(psm.previewSwap(address(usdc), address(dai), amountIn), amountIn * 1e12); + } -// rateProvider.__setConversionRate(0.8e27); + function test_previewSwap_usdcToSDai() public view { + assertEq(psm.previewSwap(address(usdc), address(sDai), 1e6), 0.8e18); + } -// assertEq(psm.previewSwapAssetOneToZero(1), 0); -// assertEq(psm.previewSwapAssetOneToZero(2), 0); -// assertEq(psm.previewSwapAssetOneToZero(3), 0); -// assertEq(psm.previewSwapAssetOneToZero(4), 0); + function testFuzz_previewSwap_daiToSDai(uint256 amountIn, uint256 exchangeRate) public { + amountIn = _bound(amountIn, 0, USDC_TOKEN_MAX); -// // 1e-6 with 18 decimal precision -// assertEq(psm.previewSwapAssetOneToZero(1e12), 0); -// assertEq(psm.previewSwapAssetOneToZero(2e12), 1); -// assertEq(psm.previewSwapAssetOneToZero(3e12), 2); -// assertEq(psm.previewSwapAssetOneToZero(4e12), 3); + amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates + exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate -// assertEq(psm.previewSwapAssetOneToZero(1e18), 0.8e6); -// assertEq(psm.previewSwapAssetOneToZero(2e18), 1.6e6); -// assertEq(psm.previewSwapAssetOneToZero(3e18), 2.4e6); -// assertEq(psm.previewSwapAssetOneToZero(4e18), 3.2e6); -// } + rateProvider.__setConversionRate(exchangeRate); + + uint256 amountOut = amountIn * 1e27 * 1e12 / exchangeRate; + + assertEq(psm.previewSwap(address(usdc), address(sDai), amountIn), amountOut); + } + +} + +contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { + + function test_previewSwap_sDaiToDai() public view { + assertEq(psm.previewSwap(address(sDai), address(dai), 1e18), 1.25e18); + } + + function testFuzz_previewSwap_sDaiToDai(uint256 amountIn, uint256 exchangeRate) public { + amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates + exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + + rateProvider.__setConversionRate(exchangeRate); + + uint256 amountOut = amountIn * exchangeRate / 1e27; + + assertEq(psm.previewSwap(address(sDai), address(dai), amountIn), amountOut); + } + + function test_previewSwap_sDaiToUsdc() public view { + assertEq(psm.previewSwap(address(sDai), address(usdc), 1e18), 1.25e6); + } + + function testFuzz_previewSwap_sDaiToUsdc(uint256 amountIn, uint256 exchangeRate) public { + amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates + exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + + rateProvider.__setConversionRate(exchangeRate); + + uint256 amountOut = amountIn * exchangeRate / 1e27 / 1e12; + + assertEq(psm.previewSwap(address(sDai), address(usdc), amountIn), amountOut); + } + +} -// } From ffa4513c534ba132a2404a30a20e31b489af0796 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 15:53:28 -0400 Subject: [PATCH 13/96] feat: move logic out of single use internal and use conversion rate everywhere --- src/PSM.sol | 23 ++++++++-------- test/Conversions.t.sol | 52 ++++++++++++++++++++++++++++++++++- test/Deposit.t.sol | 2 +- test/Getters.t.sol | 50 +-------------------------------- test/Previews.t.sol | 40 +++++++++++++-------------- test/Swaps.t.sol | 40 +++++++++++++-------------- test/Withdraw.t.sol | 6 ++-- test/harnesses/PSMHarness.sol | 4 --- 8 files changed, 107 insertions(+), 110 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index 3c7c9a0..fd7fffc 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -186,7 +186,17 @@ contract PSM { function convertToAssets(address asset, uint256 numShares) public view returns (uint256) { require(_isValidAsset(asset), "PSM/invalid-asset"); - return _getAssetsByValue(asset, convertToAssetValue(numShares)); + + uint256 assetValue = convertToAssetValue(numShares); + + if (asset == address(asset0)) return assetValue * asset0Precision / 1e18; + else if (asset == address(asset1)) return assetValue * asset1Precision / 1e18; + + // NOTE: Multiplying by 1e27 and dividing by 1e18 cancels to 1e9 in numerator + return assetValue + * 1e9 + * asset2Precision + / IRateProviderLike(rateProvider).getConversionRate(); } function convertToAssetValue(uint256 numShares) public view returns (uint256) { @@ -247,17 +257,6 @@ contract PSM { else revert("PSM/invalid-asset"); } - function _getAssetsByValue(address asset, uint256 assetValue) internal view returns (uint256) { - if (asset == address(asset0)) return assetValue * asset0Precision / 1e18; - else if (asset == address(asset1)) return assetValue * asset1Precision / 1e18; - - // NOTE: Multiplying by 1e27 and dividing by 1e18 cancels to 1e9 in numerator - return assetValue - * 1e9 - * asset2Precision - / IRateProviderLike(rateProvider).getConversionRate(); - } - function _getAsset0Value(uint256 amount) internal view returns (uint256) { return amount * 1e18 / asset0Precision; } diff --git a/test/Conversions.t.sol b/test/Conversions.t.sol index 2b892ee..a3df73a 100644 --- a/test/Conversions.t.sol +++ b/test/Conversions.t.sol @@ -217,6 +217,56 @@ contract PSMConvertToAssetValueTests is PSMTestBase { } -contract PSMConvertToAssetsTests is PSMTestBase {} +contract PSMConvertToAssetsTests is PSMTestBase { + + function test_convertToAssets() public view { + assertEq(psm.convertToAssets(address(dai), 1), 1); + assertEq(psm.convertToAssets(address(dai), 2), 2); + assertEq(psm.convertToAssets(address(dai), 3), 3); + + assertEq(psm.convertToAssets(address(dai), 1e18), 1e18); + assertEq(psm.convertToAssets(address(dai), 2e18), 2e18); + assertEq(psm.convertToAssets(address(dai), 3e18), 3e18); + + assertEq(psm.convertToAssets(address(usdc), 1), 0); + assertEq(psm.convertToAssets(address(usdc), 2), 0); + assertEq(psm.convertToAssets(address(usdc), 3), 0); + + assertEq(psm.convertToAssets(address(usdc), 1e18), 1e6); + assertEq(psm.convertToAssets(address(usdc), 2e18), 2e6); + assertEq(psm.convertToAssets(address(usdc), 3e18), 3e6); + + assertEq(psm.convertToAssets(address(sDai), 1), 0); + assertEq(psm.convertToAssets(address(sDai), 2), 1); + assertEq(psm.convertToAssets(address(sDai), 3), 2); + + assertEq(psm.convertToAssets(address(sDai), 1e18), 0.8e18); + assertEq(psm.convertToAssets(address(sDai), 2e18), 1.6e18); + assertEq(psm.convertToAssets(address(sDai), 3e18), 2.4e18); + } + + function testFuzz_convertToAssets_asset0(uint256 amount) public view { + amount = _bound(amount, 0, DAI_TOKEN_MAX); + + assertEq(psm.convertToAssets(address(dai), amount), amount); + } + + function testFuzz_convertToAssets_asset1(uint256 amount) public view { + amount = _bound(amount, 0, USDC_TOKEN_MAX); + + assertEq(psm.convertToAssets(address(usdc), amount), amount / 1e12); + } + + function testFuzz_convertToAssets_asset2(uint256 conversionRate, uint256 amount) public { + // NOTE: 0.0001e27 considered lower bound for overflow considerations + conversionRate = bound(conversionRate, 0.0001e27, 1000e27); + amount = bound(amount, 0, SDAI_TOKEN_MAX); + + rateProvider.__setConversionRate(conversionRate); + + assertEq(psm.convertToAssets(address(sDai), amount), amount * 1e27 / conversionRate); + } + +} diff --git a/test/Deposit.t.sol b/test/Deposit.t.sol index f9924f8..5070229 100644 --- a/test/Deposit.t.sol +++ b/test/Deposit.t.sol @@ -164,7 +164,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); } - function test_deposit_multiUser_changeExchangeRate() public { + function test_deposit_multiUser_changeConversionRate() public { usdc.mint(user1, 100e6); vm.startPrank(user1); diff --git a/test/Getters.t.sol b/test/Getters.t.sol index 502625c..0dcdbda 100644 --- a/test/Getters.t.sol +++ b/test/Getters.t.sol @@ -146,54 +146,6 @@ contract PSMHarnessTests is PSMTestBase { psmHarness.getAssetValue(address(0), 1); } - function test_getAssetsByValue() public view { - assertEq(psmHarness.getAssetsByValue(address(dai), 1), 1); - assertEq(psmHarness.getAssetsByValue(address(dai), 2), 2); - assertEq(psmHarness.getAssetsByValue(address(dai), 3), 3); - - assertEq(psmHarness.getAssetsByValue(address(dai), 1e18), 1e18); - assertEq(psmHarness.getAssetsByValue(address(dai), 2e18), 2e18); - assertEq(psmHarness.getAssetsByValue(address(dai), 3e18), 3e18); - - assertEq(psmHarness.getAssetsByValue(address(usdc), 1), 0); - assertEq(psmHarness.getAssetsByValue(address(usdc), 2), 0); - assertEq(psmHarness.getAssetsByValue(address(usdc), 3), 0); - - assertEq(psmHarness.getAssetsByValue(address(usdc), 1e18), 1e6); - assertEq(psmHarness.getAssetsByValue(address(usdc), 2e18), 2e6); - assertEq(psmHarness.getAssetsByValue(address(usdc), 3e18), 3e6); - - assertEq(psmHarness.getAssetsByValue(address(sDai), 1), 0); - assertEq(psmHarness.getAssetsByValue(address(sDai), 2), 1); - assertEq(psmHarness.getAssetsByValue(address(sDai), 3), 2); - - assertEq(psmHarness.getAssetsByValue(address(sDai), 1e18), 0.8e18); - assertEq(psmHarness.getAssetsByValue(address(sDai), 2e18), 1.6e18); - assertEq(psmHarness.getAssetsByValue(address(sDai), 3e18), 2.4e18); - } - - function testFuzz_getAssetsByValue_asset0(uint256 amount) public view { - amount = _bound(amount, 0, DAI_TOKEN_MAX); - - assertEq(psmHarness.getAssetsByValue(address(dai), amount), amount); - } - - function testFuzz_getAssetsByValue_asset1(uint256 amount) public view { - amount = _bound(amount, 0, USDC_TOKEN_MAX); - - assertEq(psmHarness.getAssetsByValue(address(usdc), amount), amount / 1e12); - } - - function testFuzz_getAssetsByValue_asset2(uint256 conversionRate, uint256 amount) public { - // NOTE: 0.0001e27 considered lower bound for overflow considerations - conversionRate = bound(conversionRate, 0.0001e27, 1000e27); - amount = bound(amount, 0, SDAI_TOKEN_MAX); - - rateProvider.__setConversionRate(conversionRate); - - assertEq(psmHarness.getAssetsByValue(address(sDai), amount), amount * 1e27 / conversionRate); - } - } // TODO: Update for three assets @@ -219,7 +171,7 @@ contract GetPsmTotalValueTests is PSMTestBase { assertEq(psm.getPsmTotalValue(), 0); } - function test_getPsmTotalValue_exchangeRateChanges() public { + function test_getPsmTotalValue_conversionRateChanges() public { assertEq(psm.getPsmTotalValue(), 0); usdc.mint(address(psm), 1e6); diff --git a/test/Previews.t.sol b/test/Previews.t.sol index 3e255ef..a0ef4e6 100644 --- a/test/Previews.t.sol +++ b/test/Previews.t.sol @@ -41,13 +41,13 @@ contract PSMPreviewSwapDaiAssetInTests is PSMTestBase { assertEq(psm.previewSwap(address(dai), address(sDai), 1e18), 0.8e18); } - function testFuzz_previewSwap_daiToSDai(uint256 amountIn, uint256 exchangeRate) public { - amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates - exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + function testFuzz_previewSwap_daiToSDai(uint256 amountIn, uint256 conversionRate) public { + amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates + conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate - rateProvider.__setConversionRate(exchangeRate); + rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * 1e27 / exchangeRate; + uint256 amountOut = amountIn * 1e27 / conversionRate; assertEq(psm.previewSwap(address(dai), address(sDai), amountIn), amountOut); } @@ -70,15 +70,15 @@ contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { assertEq(psm.previewSwap(address(usdc), address(sDai), 1e6), 0.8e18); } - function testFuzz_previewSwap_daiToSDai(uint256 amountIn, uint256 exchangeRate) public { + function testFuzz_previewSwap_daiToSDai(uint256 amountIn, uint256 conversionRate) public { amountIn = _bound(amountIn, 0, USDC_TOKEN_MAX); - amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates - exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates + conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate - rateProvider.__setConversionRate(exchangeRate); + rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * 1e27 * 1e12 / exchangeRate; + uint256 amountOut = amountIn * 1e27 * 1e12 / conversionRate; assertEq(psm.previewSwap(address(usdc), address(sDai), amountIn), amountOut); } @@ -91,13 +91,13 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { assertEq(psm.previewSwap(address(sDai), address(dai), 1e18), 1.25e18); } - function testFuzz_previewSwap_sDaiToDai(uint256 amountIn, uint256 exchangeRate) public { - amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates - exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + function testFuzz_previewSwap_sDaiToDai(uint256 amountIn, uint256 conversionRate) public { + amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates + conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate - rateProvider.__setConversionRate(exchangeRate); + rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * exchangeRate / 1e27; + uint256 amountOut = amountIn * conversionRate / 1e27; assertEq(psm.previewSwap(address(sDai), address(dai), amountIn), amountOut); } @@ -106,13 +106,13 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { assertEq(psm.previewSwap(address(sDai), address(usdc), 1e18), 1.25e6); } - function testFuzz_previewSwap_sDaiToUsdc(uint256 amountIn, uint256 exchangeRate) public { - amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates - exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + function testFuzz_previewSwap_sDaiToUsdc(uint256 amountIn, uint256 conversionRate) public { + amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates + conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate - rateProvider.__setConversionRate(exchangeRate); + rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * exchangeRate / 1e27 / 1e12; + uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; assertEq(psm.previewSwap(address(sDai), address(usdc), amountIn), amountOut); } diff --git a/test/Swaps.t.sol b/test/Swaps.t.sol index 4f76b7e..23ba548 100644 --- a/test/Swaps.t.sol +++ b/test/Swaps.t.sol @@ -196,7 +196,7 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { function testFuzz_swap_daiToSDai( uint256 amountIn, - uint256 exchangeRate, + uint256 conversionRate, address fuzzSwapper, address fuzzReceiver ) public { @@ -204,12 +204,12 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates - exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates + conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate - rateProvider.__setConversionRate(exchangeRate); + rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * 1e27 / exchangeRate; + uint256 amountOut = amountIn * 1e27 / conversionRate; _swapTest(dai, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } @@ -250,7 +250,7 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { function testFuzz_swap_usdcToSDai( uint256 amountIn, - uint256 exchangeRate, + uint256 conversionRate, address fuzzSwapper, address fuzzReceiver ) public { @@ -258,12 +258,12 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates - exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates + conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate - rateProvider.__setConversionRate(exchangeRate); + rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * 1e27 * 1e12 / exchangeRate; + uint256 amountOut = amountIn * 1e27 * 1e12 / conversionRate; _swapTest(usdc, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } @@ -290,7 +290,7 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { function testFuzz_swap_sDaiToDai( uint256 amountIn, - uint256 exchangeRate, + uint256 conversionRate, address fuzzSwapper, address fuzzReceiver ) public { @@ -298,19 +298,19 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates - exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates + conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate - rateProvider.__setConversionRate(exchangeRate); + rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * exchangeRate / 1e27; + uint256 amountOut = amountIn * conversionRate / 1e27; _swapTest(sDai, dai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } function testFuzz_swap_sDaiToUsdc( uint256 amountIn, - uint256 exchangeRate, + uint256 conversionRate, address fuzzSwapper, address fuzzReceiver ) public { @@ -318,12 +318,12 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates - exchangeRate = _bound(exchangeRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates + conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate - rateProvider.__setConversionRate(exchangeRate); + rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * exchangeRate / 1e27 / 1e12; + uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; _swapTest(sDai, usdc, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index fb84371..f86eb43 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -362,7 +362,7 @@ contract PSMWithdrawTests is PSMTestBase { withdrawAmount = amount < withdrawAmount ? amount : withdrawAmount; } - // function test_withdraw_changeExchangeRate_smallBalances_nonRoundingCode() public { + // function test_withdraw_changeConversionRate_smallBalances_nonRoundingCode() public { // _deposit(user1, address(usdc), 100e6); // _deposit(user2, address(sDai), 100e18); @@ -465,7 +465,7 @@ contract PSMWithdrawTests is PSMTestBase { // assertEq((user2ResultingValue - 125e18) * 1e18 / 125e18, 0.111111118518518567e18); // } - // function test_withdraw_changeExchangeRate_bigBalances_roundingCode() public { + // function test_withdraw_changeConversionRate_bigBalances_roundingCode() public { // _deposit(user1, address(usdc), 100_000_000e6); // _deposit(user2, address(sDai), 100_000_000e18); @@ -574,7 +574,7 @@ contract PSMWithdrawTests is PSMTestBase { // assertEq((user2ResultingValue - 125_000_000e18) * 1e18 / 125_000_000e18, 0.111111111111111111e18); // } - // function test_withdraw_changeExchangeRate_bigBalances_nonRoundingCode() public { + // function test_withdraw_changeConversionRate_bigBalances_nonRoundingCode() public { // _deposit(user1, address(usdc), 100_000_000e6); // _deposit(user2, address(sDai), 100_000_000e18); diff --git a/test/harnesses/PSMHarness.sol b/test/harnesses/PSMHarness.sol index 7775cd5..de8272e 100644 --- a/test/harnesses/PSMHarness.sol +++ b/test/harnesses/PSMHarness.sol @@ -17,10 +17,6 @@ contract PSMHarness is PSM { return _getAssetValue(asset, amount); } - function getAssetsByValue(address asset, uint256 assetValue) external view returns (uint256) { - return _getAssetsByValue(asset, assetValue); - } - function getAsset0Value(uint256 amount) external view returns (uint256) { return _getAsset0Value(amount); } From e9c519bd6b4221d0fc4ec816103da8e8b73cd130 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 15:55:53 -0400 Subject: [PATCH 14/96] feat: move divRoundUp out of single use internal --- src/PSM.sol | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index fd7fffc..97d10c7 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -239,17 +239,11 @@ contract PSM { function _convertToSharesRoundUp(uint256 assetValue) internal view returns (uint256) { uint256 totalValue = getPsmTotalValue(); if (totalValue != 0) { - return _divRoundUp(assetValue * totalShares, totalValue); + return ((assetValue * totalShares) + totalValue - 1) / totalValue; } return assetValue; } - function _divRoundUp(uint256 numerator_, uint256 divisor_) - internal pure returns (uint256 result_) - { - result_ = (numerator_ + divisor_ - 1) / divisor_; - } - function _getAssetValue(address asset, uint256 amount) internal view returns (uint256) { if (asset == address(asset0)) return _getAsset0Value(amount); else if (asset == address(asset1)) return _getAsset1Value(amount); From 6ec2b23b63a2db1564587d17aa056ac5123845f5 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 16:12:53 -0400 Subject: [PATCH 15/96] feat: add full coverage for conversion tests --- src/PSM.sol | 1 - test/Conversions.t.sol | 226 +++++++++++++++++++++++++++++------------ 2 files changed, 159 insertions(+), 68 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index 97d10c7..a4a5c24 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -225,7 +225,6 @@ contract PSM { /*** Asset value functions ***/ /**********************************************************************************************/ - // TODO: Refactor for three assets function getPsmTotalValue() public view returns (uint256) { return _getAsset0Value(asset0.balanceOf(address(this))) + _getAsset1Value(asset1.balanceOf(address(this))) diff --git a/test/Conversions.t.sol b/test/Conversions.t.sol index a3df73a..f75011a 100644 --- a/test/Conversions.t.sol +++ b/test/Conversions.t.sol @@ -9,6 +9,87 @@ import { PSMTestBase } from "test/PSMTestBase.sol"; // TODO: Add failure modes tests +contract PSMConvertToAssetsTests is PSMTestBase { + + function test_convertToAssets_invalidAsset() public { + vm.expectRevert("PSM/invalid-asset"); + psm.convertToAssets(makeAddr("new-asset"), 100); + } + + function test_convertToAssets() public view { + assertEq(psm.convertToAssets(address(dai), 1), 1); + assertEq(psm.convertToAssets(address(dai), 2), 2); + assertEq(psm.convertToAssets(address(dai), 3), 3); + + assertEq(psm.convertToAssets(address(dai), 1e18), 1e18); + assertEq(psm.convertToAssets(address(dai), 2e18), 2e18); + assertEq(psm.convertToAssets(address(dai), 3e18), 3e18); + + assertEq(psm.convertToAssets(address(usdc), 1), 0); + assertEq(psm.convertToAssets(address(usdc), 2), 0); + assertEq(psm.convertToAssets(address(usdc), 3), 0); + + assertEq(psm.convertToAssets(address(usdc), 1e18), 1e6); + assertEq(psm.convertToAssets(address(usdc), 2e18), 2e6); + assertEq(psm.convertToAssets(address(usdc), 3e18), 3e6); + + assertEq(psm.convertToAssets(address(sDai), 1), 0); + assertEq(psm.convertToAssets(address(sDai), 2), 1); + assertEq(psm.convertToAssets(address(sDai), 3), 2); + + assertEq(psm.convertToAssets(address(sDai), 1e18), 0.8e18); + assertEq(psm.convertToAssets(address(sDai), 2e18), 1.6e18); + assertEq(psm.convertToAssets(address(sDai), 3e18), 2.4e18); + } + + function testFuzz_convertToAssets_asset0(uint256 amount) public view { + amount = _bound(amount, 0, DAI_TOKEN_MAX); + + assertEq(psm.convertToAssets(address(dai), amount), amount); + } + + function testFuzz_convertToAssets_asset1(uint256 amount) public view { + amount = _bound(amount, 0, USDC_TOKEN_MAX); + + assertEq(psm.convertToAssets(address(usdc), amount), amount / 1e12); + } + + function testFuzz_convertToAssets_asset2(uint256 conversionRate, uint256 amount) public { + // NOTE: 0.0001e27 considered lower bound for overflow considerations + conversionRate = bound(conversionRate, 0.0001e27, 1000e27); + amount = bound(amount, 0, SDAI_TOKEN_MAX); + + rateProvider.__setConversionRate(conversionRate); + + assertEq(psm.convertToAssets(address(sDai), amount), amount * 1e27 / conversionRate); + } + +} + +contract PSMConvertToAssetValueTests is PSMTestBase { + + function testFuzz_convertToAssetValue_noValue(uint256 amount) public view { + assertEq(psm.convertToAssetValue(amount), amount); + } + + function test_convertToAssetValue() public { + _deposit(address(this), address(dai), 100e18); + _deposit(address(this), address(usdc), 100e6); + _deposit(address(this), address(sDai), 80e18); + + assertEq(psm.convertToAssetValue(1e18), 1e18); + + rateProvider.__setConversionRate(2e27); + + // $300 dollars of value deposited, 300 shares minted. + // sDAI portion becomes worth $160, full pool worth $360, each share worth $1.20 + assertEq(psm.convertToAssetValue(1e18), 1.2e18); + } + + // TODO: Add fuzz test + +} + contract PSMConvertToSharesTests is PSMTestBase { function test_convertToShares_noValue() public view { @@ -69,6 +150,76 @@ contract PSMConvertToSharesTests is PSMTestBase { } +contract PSMConvertToSharesFailureTests is PSMTestBase { + + function test_convertToShares_invalidAsset() public { + vm.expectRevert("PSM/invalid-asset"); + psm.convertToShares(makeAddr("new-asset"), 100); + } + +} + +contract PSMConvertToSharesWithDaiTests is PSMTestBase { + + function test_convertToShares_noValue() public view { + _assertOneToOneConversionDai(); + } + + function testFuzz_convertToShares_noValue(uint256 amount) public view { + amount = _bound(amount, 0, DAI_TOKEN_MAX); + assertEq(psm.convertToShares(address(dai), amount), amount); + } + + function test_convertToShares_depositAndWithdrawDaiAndSDai_noChange() public { + _assertOneToOneConversionDai(); + + _deposit(address(this), address(dai), 100e18); + _assertOneToOneConversionDai(); + + _deposit(address(this), address(sDai), 80e18); + _assertOneToOneConversionDai(); + + _withdraw(address(this), address(dai), 100e18); + _assertOneToOneConversionDai(); + + _withdraw(address(this), address(sDai), 80e18); + _assertOneToOneConversionDai(); + } + + function test_convertToShares_updateSDaiValue() public { + // 200 shares minted at 1:1 ratio, $200 of value in pool + _deposit(address(this), address(dai), 100e18); + _deposit(address(this), address(sDai), 80e18); + + _assertOneToOneConversionDai(); + + // 80 sDAI now worth $120, 200 shares in pool with $220 of value + // Each share should be worth $1.10. + rateProvider.__setConversionRate(1.5e27); + + assertEq(psm.convertToShares(address(dai), 10), 9); + assertEq(psm.convertToShares(address(dai), 11), 10); + assertEq(psm.convertToShares(address(dai), 12), 10); + + assertEq(psm.convertToShares(address(dai), 10e18), 9.090909090909090909e18); + assertEq(psm.convertToShares(address(dai), 11e18), 10e18); + assertEq(psm.convertToShares(address(dai), 12e18), 10.909090909090909090e18); + } + + function _assertOneToOneConversionDai() internal view { + assertEq(psm.convertToShares(address(dai), 1), 1); + assertEq(psm.convertToShares(address(dai), 2), 2); + assertEq(psm.convertToShares(address(dai), 3), 3); + assertEq(psm.convertToShares(address(dai), 4), 4); + + assertEq(psm.convertToShares(address(dai), 1e18), 1e18); + assertEq(psm.convertToShares(address(dai), 2e18), 2e18); + assertEq(psm.convertToShares(address(dai), 3e18), 3e18); + assertEq(psm.convertToShares(address(dai), 4e18), 4e18); + } + +} + contract PSMConvertToSharesWithUsdcTests is PSMTestBase { function test_convertToShares_noValue() public view { @@ -136,11 +287,14 @@ contract PSMConvertToSharesWithSDaiTests is PSMTestBase { _assertOneToOneConversion(); } - // TODO: Figure out growing diff - // function testFuzz_convertToShares_noValue(uint256 amount) public view { - // amount = _bound(amount, 0, SDAI_TOKEN_MAX); - // assertApproxEqAbs(psm.convertToShares(address(sDai), amount), amount * 100/125, 2); - // } + function testFuzz_convertToShares_noValue(uint256 amount, uint256 conversionRate) public { + amount = _bound(amount, 1000, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 1000e27); + + rateProvider.__setConversionRate(conversionRate); + + assertEq(psm.convertToShares(address(sDai), amount), amount * conversionRate / 1e27); + } function test_convertToShares_depositAndWithdrawUsdcAndSDai_noChange() public { _assertOneToOneConversion(); @@ -208,65 +362,3 @@ contract PSMConvertToSharesWithSDaiTests is PSMTestBase { } } - -contract PSMConvertToAssetValueTests is PSMTestBase { - - function testFuzz_convertToAssetValue_noValue(uint256 amount) public view { - assertEq(psm.convertToAssetValue(amount), amount); - } - -} - -contract PSMConvertToAssetsTests is PSMTestBase { - - function test_convertToAssets() public view { - assertEq(psm.convertToAssets(address(dai), 1), 1); - assertEq(psm.convertToAssets(address(dai), 2), 2); - assertEq(psm.convertToAssets(address(dai), 3), 3); - - assertEq(psm.convertToAssets(address(dai), 1e18), 1e18); - assertEq(psm.convertToAssets(address(dai), 2e18), 2e18); - assertEq(psm.convertToAssets(address(dai), 3e18), 3e18); - - assertEq(psm.convertToAssets(address(usdc), 1), 0); - assertEq(psm.convertToAssets(address(usdc), 2), 0); - assertEq(psm.convertToAssets(address(usdc), 3), 0); - - assertEq(psm.convertToAssets(address(usdc), 1e18), 1e6); - assertEq(psm.convertToAssets(address(usdc), 2e18), 2e6); - assertEq(psm.convertToAssets(address(usdc), 3e18), 3e6); - - assertEq(psm.convertToAssets(address(sDai), 1), 0); - assertEq(psm.convertToAssets(address(sDai), 2), 1); - assertEq(psm.convertToAssets(address(sDai), 3), 2); - - assertEq(psm.convertToAssets(address(sDai), 1e18), 0.8e18); - assertEq(psm.convertToAssets(address(sDai), 2e18), 1.6e18); - assertEq(psm.convertToAssets(address(sDai), 3e18), 2.4e18); - } - - function testFuzz_convertToAssets_asset0(uint256 amount) public view { - amount = _bound(amount, 0, DAI_TOKEN_MAX); - - assertEq(psm.convertToAssets(address(dai), amount), amount); - } - - function testFuzz_convertToAssets_asset1(uint256 amount) public view { - amount = _bound(amount, 0, USDC_TOKEN_MAX); - - assertEq(psm.convertToAssets(address(usdc), amount), amount / 1e12); - } - - function testFuzz_convertToAssets_asset2(uint256 conversionRate, uint256 amount) public { - // NOTE: 0.0001e27 considered lower bound for overflow considerations - conversionRate = bound(conversionRate, 0.0001e27, 1000e27); - amount = bound(amount, 0, SDAI_TOKEN_MAX); - - rateProvider.__setConversionRate(conversionRate); - - assertEq(psm.convertToAssets(address(sDai), amount), amount * 1e27 / conversionRate); - } - -} - - From 2be5afc6e011b7681f507a09ba361196a96703d8 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 16:15:39 -0400 Subject: [PATCH 16/96] feat: add more preview cases --- test/Previews.t.sol | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/Previews.t.sol b/test/Previews.t.sol index a0ef4e6..1cd3cb6 100644 --- a/test/Previews.t.sol +++ b/test/Previews.t.sol @@ -29,6 +29,8 @@ contract PSMPreviewSwapDaiAssetInTests is PSMTestBase { assertEq(psm.previewSwap(address(dai), address(usdc), 1e12), 1); assertEq(psm.previewSwap(address(dai), address(usdc), 1e18), 1e6); + assertEq(psm.previewSwap(address(dai), address(usdc), 2e18), 2e6); + assertEq(psm.previewSwap(address(dai), address(usdc), 3e18), 3e6); } function testFuzz_previewSwap_daiToUsdc(uint256 amountIn) public view { @@ -39,6 +41,8 @@ contract PSMPreviewSwapDaiAssetInTests is PSMTestBase { function test_previewSwap_daiToSDai() public view { assertEq(psm.previewSwap(address(dai), address(sDai), 1e18), 0.8e18); + assertEq(psm.previewSwap(address(dai), address(sDai), 2e18), 1.6e18); + assertEq(psm.previewSwap(address(dai), address(sDai), 3e18), 2.4e18); } function testFuzz_previewSwap_daiToSDai(uint256 amountIn, uint256 conversionRate) public { @@ -57,7 +61,9 @@ contract PSMPreviewSwapDaiAssetInTests is PSMTestBase { contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { function test_previewSwap_usdcToDai() public view { - assertEq(psm.previewSwap(address(usdc), address(dai), 1e6), 1e18); + assertEq(psm.previewSwap(address(usdc), address(dai), 1e6), 1e18); + assertEq(psm.previewSwap(address(usdc), address(dai), 2e6), 2e18); + assertEq(psm.previewSwap(address(usdc), address(dai), 3e6), 3e18); } function testFuzz_previewSwap_usdcToDai(uint256 amountIn) public view { @@ -68,6 +74,8 @@ contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { function test_previewSwap_usdcToSDai() public view { assertEq(psm.previewSwap(address(usdc), address(sDai), 1e6), 0.8e18); + assertEq(psm.previewSwap(address(usdc), address(sDai), 2e6), 1.6e18); + assertEq(psm.previewSwap(address(usdc), address(sDai), 3e6), 2.4e18); } function testFuzz_previewSwap_daiToSDai(uint256 amountIn, uint256 conversionRate) public { @@ -88,7 +96,9 @@ contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { function test_previewSwap_sDaiToDai() public view { - assertEq(psm.previewSwap(address(sDai), address(dai), 1e18), 1.25e18); + assertEq(psm.previewSwap(address(sDai), address(dai), 1e18), 1.25e18); + assertEq(psm.previewSwap(address(sDai), address(dai), 2e18), 2.5e18); + assertEq(psm.previewSwap(address(sDai), address(dai), 3e18), 3.75e18); } function testFuzz_previewSwap_sDaiToDai(uint256 amountIn, uint256 conversionRate) public { @@ -104,6 +114,8 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { function test_previewSwap_sDaiToUsdc() public view { assertEq(psm.previewSwap(address(sDai), address(usdc), 1e18), 1.25e6); + assertEq(psm.previewSwap(address(sDai), address(usdc), 2e18), 2.5e6); + assertEq(psm.previewSwap(address(sDai), address(usdc), 3e18), 3.75e6); } function testFuzz_previewSwap_sDaiToUsdc(uint256 amountIn, uint256 conversionRate) public { From 4299e032034450832a80b132adfc6fcebc426d3b Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 16:19:44 -0400 Subject: [PATCH 17/96] feat: refactor PSM to use three assets --- test/Getters.t.sol | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/test/Getters.t.sol b/test/Getters.t.sol index 0dcdbda..63a9088 100644 --- a/test/Getters.t.sol +++ b/test/Getters.t.sol @@ -148,18 +148,23 @@ contract PSMHarnessTests is PSMTestBase { } -// TODO: Update for three assets contract GetPsmTotalValueTests is PSMTestBase { function test_getPsmTotalValue_balanceChanges() public { - assertEq(psm.getPsmTotalValue(), 0); + dai.mint(address(psm), 1e18); + + assertEq(psm.getPsmTotalValue(), 1e18); usdc.mint(address(psm), 1e6); - assertEq(psm.getPsmTotalValue(), 1e18); + assertEq(psm.getPsmTotalValue(), 2e18); sDai.mint(address(psm), 1e18); + assertEq(psm.getPsmTotalValue(), 3.25e18); + + dai.burn(address(psm), 1e18); + assertEq(psm.getPsmTotalValue(), 2.25e18); usdc.burn(address(psm), 1e6); @@ -174,54 +179,62 @@ contract GetPsmTotalValueTests is PSMTestBase { function test_getPsmTotalValue_conversionRateChanges() public { assertEq(psm.getPsmTotalValue(), 0); + dai.mint(address(psm), 1e18); usdc.mint(address(psm), 1e6); sDai.mint(address(psm), 1e18); - assertEq(psm.getPsmTotalValue(), 2.25e18); + assertEq(psm.getPsmTotalValue(), 3.25e18); rateProvider.__setConversionRate(1.5e27); - assertEq(psm.getPsmTotalValue(), 2.5e18); + assertEq(psm.getPsmTotalValue(), 3.5e18); rateProvider.__setConversionRate(0.8e27); - assertEq(psm.getPsmTotalValue(), 1.8e18); + assertEq(psm.getPsmTotalValue(), 2.8e18); } function test_getPsmTotalValue_bothChange() public { assertEq(psm.getPsmTotalValue(), 0); + dai.mint(address(psm), 1e18); usdc.mint(address(psm), 1e6); sDai.mint(address(psm), 1e18); - assertEq(psm.getPsmTotalValue(), 2.25e18); + assertEq(psm.getPsmTotalValue(), 3.25e18); rateProvider.__setConversionRate(1.5e27); - assertEq(psm.getPsmTotalValue(), 2.5e18); + assertEq(psm.getPsmTotalValue(), 3.5e18); sDai.mint(address(psm), 1e18); - assertEq(psm.getPsmTotalValue(), 4e18); + assertEq(psm.getPsmTotalValue(), 5e18); } function testFuzz_getPsmTotalValue( + uint256 daiAmount, uint256 usdcAmount, uint256 sDaiAmount, uint256 conversionRate ) public { - usdcAmount = _bound(usdcAmount, 0, USDC_TOKEN_MAX); - sDaiAmount = _bound(sDaiAmount, 0, SDAI_TOKEN_MAX); - conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); + daiAmount = _bound(daiAmount, 0, DAI_TOKEN_MAX); + usdcAmount = _bound(usdcAmount, 0, USDC_TOKEN_MAX); + sDaiAmount = _bound(sDaiAmount, 0, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); + dai.mint(address(psm), daiAmount); usdc.mint(address(psm), usdcAmount); sDai.mint(address(psm), sDaiAmount); rateProvider.__setConversionRate(conversionRate); - assertEq(psm.getPsmTotalValue(), (usdcAmount * 1e12) + (sDaiAmount * conversionRate / 1e27)); + assertEq( + psm.getPsmTotalValue(), + daiAmount + (usdcAmount * 1e12) + (sDaiAmount * conversionRate / 1e27) + ); } } From 64c993984700f0d33d33e879d4c3b2dd0d559ef2 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 16:22:10 -0400 Subject: [PATCH 18/96] fix: rm comment --- test/PSMTestBase.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/test/PSMTestBase.sol b/test/PSMTestBase.sol index 7b47516..a01ba4f 100644 --- a/test/PSMTestBase.sol +++ b/test/PSMTestBase.sol @@ -48,7 +48,6 @@ contract PSMTestBase is Test { vm.label(address(sDai), "sDAI"); } - // TODO: Refactor for three assets function _getPsmValue() internal view returns (uint256) { return (sDai.balanceOf(address(psm)) * rateProvider.getConversionRate() / 1e27) + usdc.balanceOf(address(psm)) * 1e12 From 2c43f9269d3d435014ffd5788b2d9f4b89ed5ea9 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 16:56:07 -0400 Subject: [PATCH 19/96] feat: add interface, natspec, events, referral code, tests passing --- src/PSM.sol | 40 ++++--- src/interfaces/IPSM.sol | 207 +++++++++++++++++++++++++++++++++++++ test/Deposit.t.sol | 20 ++-- test/InflationAttack.t.sol | 4 +- test/PSMTestBase.sol | 4 +- test/Swaps.t.sol | 26 ++--- test/Withdraw.t.sol | 60 +++++------ 7 files changed, 289 insertions(+), 72 deletions(-) create mode 100644 src/interfaces/IPSM.sol diff --git a/src/PSM.sol b/src/PSM.sol index a4a5c24..00b82b9 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -7,6 +7,8 @@ import { IERC20 } from "erc20-helpers/interfaces/IERC20.sol"; import { SafeERC20 } from "erc20-helpers/SafeERC20.sol"; +import { IPSM } from "src/interfaces/IPSM.sol"; + interface IRateProviderLike { function getConversionRate() external view returns (uint256); } @@ -14,9 +16,8 @@ interface IRateProviderLike { // TODO: Add events and corresponding tests // TODO: Determine what admin functionality we want (fees?) // TODO: Refactor into inheritance structure -// TODO: Add interface with natspec and inherit // TODO: Prove that we're always rounding against user -contract PSM { +contract PSM is IPSM { using SafeERC20 for IERC20; @@ -71,7 +72,8 @@ contract PSM { address assetOut, uint256 amountIn, uint256 minAmountOut, - address receiver + address receiver, + uint16 referralCode ) external { @@ -84,14 +86,16 @@ contract PSM { IERC20(assetIn).safeTransferFrom(msg.sender, address(this), amountIn); IERC20(assetOut).safeTransfer(receiver, amountOut); + + emit Swap(assetIn, assetOut, msg.sender, receiver, amountIn, amountOut, referralCode); } /**********************************************************************************************/ /*** Liquidity provision functions ***/ /**********************************************************************************************/ - function deposit(address asset, uint256 assetsToDeposit) - external returns (uint256 newShares) + function deposit(address asset, uint256 assetsToDeposit, uint16 referralCode) + external override returns (uint256 newShares) { newShares = previewDeposit(asset, assetsToDeposit); @@ -106,10 +110,12 @@ contract PSM { totalShares += newShares; IERC20(asset).safeTransferFrom(msg.sender, address(this), assetsToDeposit); + + emit Deposit(asset, msg.sender, assetsToDeposit, newShares, referralCode); } - function withdraw(address asset, uint256 maxAssetsToWithdraw) - external returns (uint256 assetsWithdrawn) + function withdraw(address asset, uint256 maxAssetsToWithdraw, uint16 referralCode) + external override returns (uint256 assetsWithdrawn) { uint256 sharesToBurn; @@ -121,13 +127,15 @@ contract PSM { } IERC20(asset).safeTransfer(msg.sender, assetsWithdrawn); + + emit Withdraw(asset, msg.sender, assetsWithdrawn, sharesToBurn, referralCode); } /**********************************************************************************************/ /*** Deposit/withdraw preview functions ***/ /**********************************************************************************************/ - function previewDeposit(address asset, uint256 assets) public view returns (uint256) { + function previewDeposit(address asset, uint256 assets) public view override returns (uint256) { require(_isValidAsset(asset), "PSM/invalid-asset"); // Convert amount to 1e18 precision denominated in value of asset0 then convert to shares. @@ -135,7 +143,7 @@ contract PSM { } function previewWithdraw(address asset, uint256 maxAssetsToWithdraw) - public view returns (uint256 sharesToBurn, uint256 assetsWithdrawn) + public view override returns (uint256 sharesToBurn, uint256 assetsWithdrawn) { require(_isValidAsset(asset), "PSM/invalid-asset"); @@ -160,7 +168,7 @@ contract PSM { /**********************************************************************************************/ function previewSwap(address assetIn, address assetOut, uint256 amountIn) - public view returns (uint256 amountOut) + public view override returns (uint256 amountOut) { if (assetIn == address(asset0)) { if (assetOut == address(asset1)) return _previewOneToOneSwap(amountIn, asset0Precision, asset1Precision); @@ -184,7 +192,9 @@ contract PSM { /*** Conversion functions ***/ /**********************************************************************************************/ - function convertToAssets(address asset, uint256 numShares) public view returns (uint256) { + function convertToAssets(address asset, uint256 numShares) + public view override returns (uint256) + { require(_isValidAsset(asset), "PSM/invalid-asset"); uint256 assetValue = convertToAssetValue(numShares); @@ -199,7 +209,7 @@ contract PSM { / IRateProviderLike(rateProvider).getConversionRate(); } - function convertToAssetValue(uint256 numShares) public view returns (uint256) { + function convertToAssetValue(uint256 numShares) public view override returns (uint256) { uint256 totalShares_ = totalShares; if (totalShares_ != 0) { @@ -208,7 +218,7 @@ contract PSM { return numShares; } - function convertToShares(uint256 assetValue) public view returns (uint256) { + function convertToShares(uint256 assetValue) public view override returns (uint256) { uint256 totalValue = getPsmTotalValue(); if (totalValue != 0) { return assetValue * totalShares / totalValue; @@ -216,7 +226,7 @@ contract PSM { return assetValue; } - function convertToShares(address asset, uint256 assets) public view returns (uint256) { + function convertToShares(address asset, uint256 assets) public view override returns (uint256) { require(_isValidAsset(asset), "PSM/invalid-asset"); return convertToShares(_getAssetValue(asset, assets)); } @@ -225,7 +235,7 @@ contract PSM { /*** Asset value functions ***/ /**********************************************************************************************/ - function getPsmTotalValue() public view returns (uint256) { + function getPsmTotalValue() public view override returns (uint256) { return _getAsset0Value(asset0.balanceOf(address(this))) + _getAsset1Value(asset1.balanceOf(address(this))) + _getAsset2Value(asset2.balanceOf(address(this))); diff --git a/src/interfaces/IPSM.sol b/src/interfaces/IPSM.sol new file mode 100644 index 0000000..82cc133 --- /dev/null +++ b/src/interfaces/IPSM.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +interface IPSM { + + // TODO: Determine priority for indexing + + /**********************************************************************************************/ + /*** Events ***/ + /**********************************************************************************************/ + + /** + * @dev Emitted when an asset is swapped in the PSM. + * @param assetIn Address of the asset swapped in. + * @param assetOut Address of the asset swapped out. + * @param sender Address of the sender of the swap. + * @param receiver Address of the receiver of the swap. + * @param amountIn Amount of the asset swapped in. + * @param amountOut Amount of the asset swapped out. + * @param referralCode Referral code for the swap. + */ + event Swap( + address indexed assetIn, + address indexed assetOut, + address sender, + address indexed receiver, + uint256 amountIn, + uint256 amountOut, + uint16 referralCode + ); + + /** + * @dev Emitted when an asset is deposited into the PSM. + * @param asset Address of the asset deposited. + * @param user Address of the user that deposited the asset. + * @param assetsDeposited Amount of the asset deposited. + * @param sharesMinted Number of shares minted to the user. + * @param referralCode Referral code for the deposit. + */ + event Deposit( + address indexed asset, + address indexed user, + uint256 assetsDeposited, + uint256 sharesMinted, + uint16 referralCode + ); + + /** + * @dev Emitted when an asset is withdrawn from the PSM. + * @param asset Address of the asset withdrawn. + * @param user Address of the user that withdrew the asset. + * @param assetsWithdrawn Amount of the asset withdrawn. + * @param sharesBurned Number of shares burned from the user. + * @param referralCode Referral code for the withdrawal. + */ + event Withdraw( + address indexed asset, + address indexed user, + uint256 assetsWithdrawn, + uint256 sharesBurned, + uint16 referralCode + ); + + /**********************************************************************************************/ + /*** Swap functions ***/ + /**********************************************************************************************/ + + /** + * @dev Swaps an amount of assetIn for assetOut in the PSM. The amount swapped is converted + * based on the current value of the two assets used in the swap. This function will + * revert if there is not enough balance in the PSM to facilitate the swap. Both assets + * must be supported in the PSM in order to succeed. + * @param assetIn Address of the ERC-20 asset to swap in. + * @param assetOut Address of the ERC-20 asset to swap out. + * @param amountIn Amount of the asset to swap in. + * @param minAmountOut Minimum amount of the asset to receive. + * @param receiver Address of the receiver of the swapped assets. + * @param referralCode Referral code for the swap. + */ + function swap( + address assetIn, + address assetOut, + uint256 amountIn, + uint256 minAmountOut, + address receiver, + uint16 referralCode + ) external; + + /**********************************************************************************************/ + /*** Liquidity provision functions ***/ + /**********************************************************************************************/ + + /** + * @dev Deposits an amount of a given asset into the PSM. Must be one of the supported + * assets in order to succeed. The amount deposited is converted to shares based on + * the current exchange rate. + * @param asset Address of the ERC-20 asset to deposit. + * @param assetsToDeposit Amount of the asset to deposit into the PSM. + * @param referralCode Referral code for the deposit. + * @return newShares Number of shares minted to the user. + */ + function deposit(address asset, uint256 assetsToDeposit, uint16 referralCode) + external returns (uint256 newShares); + + /** + * @dev Withdraws an amount of a given asset from the PSM up to `maxAssetsToWithdraw`. + * Must be one of the supported assets in order to succeed. The amount withdrawn is + * the minimum of the balance of the PSM, the max amount, and the max amount of assets + * that the user's shares can be converted to. + * @param asset Address of the ERC-20 asset to deposit. + * @param maxAssetsToWithdraw Max amount that the user is willing to withdraw. + * @param referralCode Referral code for the withdrawal. + * @return assetsWithdrawn Resulting amount of the asset withdrawn from the PSM. + */ + function withdraw(address asset, uint256 maxAssetsToWithdraw, uint16 referralCode) + external returns (uint256 assetsWithdrawn); + + /**********************************************************************************************/ + /*** Deposit/withdraw preview functions ***/ + /**********************************************************************************************/ + + /** + * @dev Returns the exact number of shares that would be minted for a given asset and + * amount to deposit. + * @param asset Address of the ERC-20 asset to deposit. + * @param assets Amount of the asset to deposit into the PSM. + * @return shares Number of shares to be minted to the user. + */ + function previewDeposit(address asset, uint256 assets) external view returns (uint256 shares); + + /** + * @dev Returns the exact number of assets that would be withdrawn and corresponding shares + * that would be burned in a withdrawal for a given asset and max withdraw amount. The + * amount returned is the minimum of the balance of the PSM, the max amount, and the + * max amount of assets that the user's shares can be converted to. + * @param asset Address of the ERC-20 asset to withdraw. + * @param maxAssetsToWithdraw Max amount that the user is willing to withdraw. + * @return sharesToBurn Number of shares that would be burned in the withdrawal. + * @return assetsWithdrawn Resulting amount of the asset withdrawn from the PSM. + */ + function previewWithdraw(address asset, uint256 maxAssetsToWithdraw) + external view returns (uint256 sharesToBurn, uint256 assetsWithdrawn); + + /**********************************************************************************************/ + /*** Swap preview functions ***/ + /**********************************************************************************************/ + + /** + * @dev Returns the exact amount of assetOut that would be received for a given amount of + * assetIn in a swap. The amount returned is converted based on the current value + * of the two assets used in the swap. + * @param assetIn Address of the ERC-20 asset to swap in. + * @param assetOut Address of the ERC-20 asset to swap out. + * @param amountIn Amount of the asset to swap in. + * @return amountOut Amount of the asset that will be received in the swap. + */ + function previewSwap(address assetIn, address assetOut, uint256 amountIn) + external view returns (uint256 amountOut); + + /**********************************************************************************************/ + /*** Conversion functions ***/ + /**********************************************************************************************/ + + /** + * @dev Converts an amount of a given shares to the equivalent amount of + * assets for a specified asset. + * @param asset Address of the asset to use to convert. + * @param numShares Number of shares to convert to assets. + * @return assets Value of assets in asset-native units. + */ + function convertToAssets(address asset, uint256 numShares) external view returns (uint256); + + /** + * @dev Converts an amount of a given shares to the equivalent amount of assetValue. + * @param numShares Number of shares to convert to assetValue. + * @return assetValue Value of assets in asset0 denominated in 18 decimals. + */ + function convertToAssetValue(uint256 numShares) external view returns (uint256); + + /** + * @dev Converts an amount of assetValue (18 decimal value denominated in asset0) + * to shares in the PSM based on the current exchange rate. + * @param assetValue 18 decimal value denominated in asset0 (e.g., 1e6 USDC = 1e18) + * @return shares Number of shares that the assetValue is equivalent to. + */ + function convertToShares(uint256 assetValue) external view returns (uint256); + + /** + * @dev Converts an amount of a given asset to shares in the PSM based on the + * current exchange rate. + * @param asset Address of the ERC-20 asset to convert to shares. + * @param assets Amount of assets in asset-native units. + * @return shares Number of shares that the assetValue is equivalent to. + */ + function convertToShares(address asset, uint256 assets) external view returns (uint256); + + /**********************************************************************************************/ + /*** Asset value functions ***/ + /**********************************************************************************************/ + + /** + * @dev Returns the total value of the balance of all assets in the PSM converted to + * asset0 denominated in 18 decimal precision. + */ + function getPsmTotalValue() external view returns (uint256); + +} diff --git a/test/Deposit.t.sol b/test/Deposit.t.sol index 5070229..7feabd9 100644 --- a/test/Deposit.t.sol +++ b/test/Deposit.t.sol @@ -17,7 +17,7 @@ contract PSMDepositTests is PSMTestBase { function test_deposit_notAsset0OrAsset1() public { vm.expectRevert("PSM/invalid-asset"); - psm.deposit(makeAddr("new-asset"), 100e6); + psm.deposit(makeAddr("new-asset"), 100e6, 0); } // TODO: Add balance/approve failure tests @@ -39,7 +39,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(usdc), 100e6); + psm.deposit(address(usdc), 100e6, 0); assertEq(usdc.allowance(user1, address(psm)), 0); assertEq(usdc.balanceOf(user1), 0); @@ -69,7 +69,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), 100e18); + psm.deposit(address(sDai), 100e18, 0); assertEq(sDai.allowance(user1, address(psm)), 0); assertEq(sDai.balanceOf(user1), 0); @@ -89,7 +89,7 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), 100e6); - psm.deposit(address(usdc), 100e6); + psm.deposit(address(usdc), 100e6, 0); sDai.mint(user1, 100e18); sDai.approve(address(psm), 100e18); @@ -106,7 +106,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), 100e18); + psm.deposit(address(sDai), 100e18, 0); assertEq(usdc.balanceOf(address(psm)), 100e6); @@ -132,7 +132,7 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), usdcAmount); - psm.deposit(address(usdc), usdcAmount); + psm.deposit(address(usdc), usdcAmount, 0); sDai.mint(user1, sDaiAmount); sDai.approve(address(psm), sDaiAmount); @@ -149,7 +149,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), sDaiAmount); + psm.deposit(address(sDai), sDaiAmount, 0); assertEq(usdc.balanceOf(address(psm)), usdcAmount); @@ -171,12 +171,12 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), 100e6); - psm.deposit(address(usdc), 100e6); + psm.deposit(address(usdc), 100e6, 0); sDai.mint(user1, 100e18); sDai.approve(address(psm), 100e18); - psm.deposit(address(sDai), 100e18); + psm.deposit(address(sDai), 100e18, 0); vm.stopPrank(); @@ -217,7 +217,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.getPsmTotalValue(), 250e18); - psm.deposit(address(sDai), 100e18); + psm.deposit(address(sDai), 100e18, 0); assertEq(psm.shares(user2), 135e18); diff --git a/test/InflationAttack.t.sol b/test/InflationAttack.t.sol index 79ec0eb..701bd61 100644 --- a/test/InflationAttack.t.sol +++ b/test/InflationAttack.t.sol @@ -65,10 +65,10 @@ contract InflationAttackTests is PSMTestBase { sDai.approve(address(psm), 800); vm.expectRevert(stdError.arithmeticError); - psm.deposit(address(sDai), 799); + psm.deposit(address(sDai), 799, 0); // 800 sDAI = 1000 shares - psm.deposit(address(sDai), 800); + psm.deposit(address(sDai), 800, 0); } function test_inflationAttack_useInitialBurnAmount() public { diff --git a/test/PSMTestBase.sol b/test/PSMTestBase.sol index a01ba4f..11dec92 100644 --- a/test/PSMTestBase.sol +++ b/test/PSMTestBase.sol @@ -58,13 +58,13 @@ contract PSMTestBase is Test { vm.startPrank(user); MockERC20(asset).mint(user, amount); MockERC20(asset).approve(address(psm), amount); - psm.deposit(asset, amount); + psm.deposit(asset, amount, 0); vm.stopPrank(); } function _withdraw(address user, address asset, uint256 amount) internal { vm.prank(user); - psm.withdraw(asset, amount); + psm.withdraw(asset, amount, 0); vm.stopPrank(); } diff --git a/test/Swaps.t.sol b/test/Swaps.t.sol index 23ba548..91ab81d 100644 --- a/test/Swaps.t.sol +++ b/test/Swaps.t.sol @@ -22,22 +22,22 @@ contract PSMSwapFailureTests is PSMTestBase { function test_swap_amountZero() public { vm.expectRevert("PSM/invalid-amountIn"); - psm.swap(address(usdc), address(sDai), 0, 0, receiver); + psm.swap(address(usdc), address(sDai), 0, 0, receiver, 0); } function test_swap_receiverZero() public { vm.expectRevert("PSM/invalid-receiver"); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, address(0)); + psm.swap(address(usdc), address(sDai), 100e6, 80e18, address(0), 0); } function test_swap_invalid_assetIn() public { vm.expectRevert("PSM/invalid-asset"); - psm.swap(makeAddr("other-token"), address(sDai), 100e6, 80e18, receiver); + psm.swap(makeAddr("other-token"), address(sDai), 100e6, 80e18, receiver, 0); } function test_swap_invalid_assetOut() public { vm.expectRevert("PSM/invalid-asset"); - psm.swap(address(usdc), makeAddr("other-token"), 100e6, 80e18, receiver); + psm.swap(address(usdc), makeAddr("other-token"), 100e6, 80e18, receiver, 0); } function test_swap_minAmountOutBoundary() public { @@ -52,9 +52,9 @@ contract PSMSwapFailureTests is PSMTestBase { assertEq(expectedAmountOut, 80e18); vm.expectRevert("PSM/amountOut-too-low"); - psm.swap(address(usdc), address(sDai), 100e6, 80e18 + 1, receiver); + psm.swap(address(usdc), address(sDai), 100e6, 80e18 + 1, receiver, 0); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver); + psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); } function test_swap_insufficientApproveBoundary() public { @@ -65,11 +65,11 @@ contract PSMSwapFailureTests is PSMTestBase { usdc.approve(address(psm), 100e6 - 1); vm.expectRevert("SafeERC20/transfer-from-failed"); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver); + psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); usdc.approve(address(psm), 100e6); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver); + psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); } function test_swap_insufficientUserBalanceBoundary() public { @@ -80,11 +80,11 @@ contract PSMSwapFailureTests is PSMTestBase { usdc.approve(address(psm), 100e6); vm.expectRevert("SafeERC20/transfer-from-failed"); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver); + psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); usdc.mint(swapper, 1); - psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver); + psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); } function test_swap_insufficientPsmBalanceBoundary() public { @@ -99,9 +99,9 @@ contract PSMSwapFailureTests is PSMTestBase { assertEq(expectedAmountOut, 100.0000008e18); // More than balance of sDAI vm.expectRevert("SafeERC20/transfer-failed"); - psm.swap(address(usdc), address(sDai), 125e6 + 1, 100e18, receiver); + psm.swap(address(usdc), address(sDai), 125e6 + 1, 100e18, receiver, 0); - psm.swap(address(usdc), address(sDai), 125e6, 100e18, receiver); + psm.swap(address(usdc), address(sDai), 125e6, 100e18, receiver, 0); } } @@ -142,7 +142,7 @@ contract PSMSuccessTestsBase is PSMTestBase { assertEq(assetOut.balanceOf(receiver), 0); assertEq(assetOut.balanceOf(address(psm)), psmAssetOutBalance); - psm.swap(address(assetIn), address(assetOut), amountIn, amountOut, receiver); + psm.swap(address(assetIn), address(assetOut), amountIn, amountOut, receiver, 0); assertEq(assetIn.allowance(swapper, address(psm)), 0); diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index f86eb43..a000fad 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -19,7 +19,7 @@ contract PSMWithdrawTests is PSMTestBase { function test_withdraw_notAsset0OrAsset1() public { vm.expectRevert("PSM/invalid-asset"); - psm.withdraw(makeAddr("new-asset"), 100e6); + psm.withdraw(makeAddr("new-asset"), 100e6, 0); } // TODO: Add balance/approve failure tests @@ -37,7 +37,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - uint256 amount = psm.withdraw(address(usdc), 100e6); + uint256 amount = psm.withdraw(address(usdc), 100e6, 0); // Burn amount causes shares to round down by one since shares are 99.999... assertEq(amount, 100e6 - 1); @@ -69,7 +69,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - uint256 amount = psm.withdraw(address(sDai), 80e18); + uint256 amount = psm.withdraw(address(sDai), 80e18, 0); assertEq(amount, 80e18 - 800); @@ -100,7 +100,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - uint256 amount = psm.withdraw(address(usdc), 100e6); + uint256 amount = psm.withdraw(address(usdc), 100e6, 0); assertEq(amount, 100e6); @@ -120,7 +120,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToAssets(address(sDai), BURN_AMOUNT), 800); vm.prank(user1); - amount = psm.withdraw(address(sDai), 100e18); + amount = psm.withdraw(address(sDai), 100e18, 0); assertEq(amount, 100e18 - 800); @@ -151,7 +151,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - uint256 amount = psm.withdraw(address(usdc), 125e6); + uint256 amount = psm.withdraw(address(usdc), 125e6, 0); assertEq(amount, 100e6); @@ -177,7 +177,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user2); - uint256 amount = psm.withdraw(address(usdc), 225e6); + uint256 amount = psm.withdraw(address(usdc), 225e6, 0); assertEq(amount, 200e6); @@ -225,7 +225,7 @@ contract PSMWithdrawTests is PSMTestBase { = _getExpectedWithdrawnAmount(usdc, user1, withdrawAmount1); vm.prank(user1); - uint256 amount = psm.withdraw(address(usdc), withdrawAmount1); + uint256 amount = psm.withdraw(address(usdc), withdrawAmount1, 0); assertEq(amount, expectedWithdrawnAmount1); @@ -249,7 +249,7 @@ contract PSMWithdrawTests is PSMTestBase { = _getExpectedWithdrawnAmount(usdc, user2, withdrawAmount2); vm.prank(user2); - amount = psm.withdraw(address(usdc), withdrawAmount2); + amount = psm.withdraw(address(usdc), withdrawAmount2, 0); assertEq(amount, expectedWithdrawnAmount2); @@ -281,7 +281,7 @@ contract PSMWithdrawTests is PSMTestBase { = _getExpectedWithdrawnAmount(sDai, user2, withdrawAmount3); vm.prank(user2); - amount = psm.withdraw(address(sDai), withdrawAmount3); + amount = psm.withdraw(address(sDai), withdrawAmount3, 0); assertApproxEqAbs(amount, expectedWithdrawnAmount3, 1); @@ -402,16 +402,16 @@ contract PSMWithdrawTests is PSMTestBase { // // Original full balance reverts // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), 100e18); + // psm.withdraw(address(usdc), 100e18, 0); // // Boundary condition at 90.000001e18 shares // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), maxUsdcShares + 1); + // psm.withdraw(address(usdc), maxUsdcShares + 1, 0); // console2.log("First CTA", psm.convertToAssetValue(100e18)); // // Rounds down here and transfers 100e6 USDC - // psm.withdraw(address(usdc), maxUsdcShares); + // psm.withdraw(address(usdc), maxUsdcShares, 0); // console2.log("\n\n\n"); @@ -424,7 +424,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Second CTA", psm.convertToAssetValue(100e18)); - // psm.withdraw(address(sDai), 100e18 - maxUsdcShares); + // psm.withdraw(address(sDai), 100e18 - maxUsdcShares, 0); // uint256 sDaiUser1Balance = 7.407406790123452675e18; @@ -442,7 +442,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Third CTA", psm.convertToAssetValue(100e18)); // // Withdraw shares originally worth $100 to compare yield with user1 - // psm.withdraw(address(sDai), 125e18); + // psm.withdraw(address(sDai), 125e18, 0); // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); // assertEq(sDai.balanceOf(user2), 100e18 - sDaiUser1Balance - 1); @@ -505,16 +505,16 @@ contract PSMWithdrawTests is PSMTestBase { // // Original full balance reverts // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), 100_000_000e18); + // psm.withdraw(address(usdc), 100_000_000e18, 0); // // Boundary condition at 90.000001e18 shares // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), maxUsdcShares + 1); + // psm.withdraw(address(usdc), maxUsdcShares + 1, 0); // console2.log("First CTA", psm.convertToAssetValue(100_000_000e18)); // // Rounds down here and transfers 100_000_000e6 USDC - // psm.withdraw(address(usdc), maxUsdcShares); + // psm.withdraw(address(usdc), maxUsdcShares, 0); // console2.log("\n\n\n"); @@ -531,7 +531,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Second CTA", psm.convertToAssetValue(100_000_000e18)); - // psm.withdraw(address(sDai), 100_000_000e18 - maxUsdcShares); + // psm.withdraw(address(sDai), 100_000_000e18 - maxUsdcShares, 0); // uint256 sDaiUser1Balance = 7_407_407.407407407407407407e18; @@ -549,7 +549,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Third CTA", psm.convertToAssetValue(100e18)); // // Withdraw shares originally worth $100 to compare yield with user1 - // psm.withdraw(address(sDai), 125_000_000e18); + // psm.withdraw(address(sDai), 125_000_000e18, 0); // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); // assertEq(sDai.balanceOf(user2), 100_000_000e18 - sDaiUser1Balance - 1); @@ -614,16 +614,16 @@ contract PSMWithdrawTests is PSMTestBase { // // Original full balance reverts // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), 100_000_000e18); + // psm.withdraw(address(usdc), 100_000_000e18, 0); // // Boundary condition at 90.000001e18 shares // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), maxUsdcShares + 1); + // psm.withdraw(address(usdc), maxUsdcShares + 1, 0); // console2.log("First CTA", psm.convertToAssetValue(100_000_000e18)); // // Rounds down here and transfers 100_000_000e6 USDC - // psm.withdraw(address(usdc), maxUsdcShares); + // psm.withdraw(address(usdc), maxUsdcShares, 0); // console2.log("\n\n\n"); @@ -638,7 +638,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Second CTA", psm.convertToAssetValue(100_000_000e18)); - // psm.withdraw(address(sDai), 100_000_000e18 - maxUsdcShares); + // psm.withdraw(address(sDai), 100_000_000e18 - maxUsdcShares, 0); // uint256 sDaiUser1Balance = 7_407_407.407406790123456790e18; @@ -656,7 +656,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Third CTA", psm.convertToAssetValue(100e18)); // // Withdraw shares originally worth $100 to compare yield with user1 - // psm.withdraw(address(sDai), 125_000_000e18); + // psm.withdraw(address(sDai), 125_000_000e18, 0); // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); // assertEq(sDai.balanceOf(user2), 100_000_000e18 - sDaiUser1Balance); @@ -723,18 +723,18 @@ contract PSMWithdrawTests is PSMTestBase { // // // Original full balance reverts // // vm.expectRevert("SafeERC20/transfer-failed"); - // // psm.withdraw(address(usdc), 100e18); + // // psm.withdraw(address(usdc), 100e18, 0); // // // Boundary condition at 90.000001e18 shares // // vm.expectRevert("SafeERC20/transfer-failed"); - // // psm.withdraw(address(usdc), maxUsdcShares + 1); + // // psm.withdraw(address(usdc), maxUsdcShares + 1, 0); // console2.log("First CTA", psm.convertToAssetValue(100e18)); // // maxUsdcShares = 89.99999e18; // // Rounds down here and transfers 100e6 USDC - // psm.withdraw(address(usdc), maxUsdcShares); + // psm.withdraw(address(usdc), maxUsdcShares, 0); // console2.log("\n\n\n"); @@ -747,7 +747,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Second CTA", psm.convertToAssetValue(100e18)); - // // psm.withdraw(address(sDai), 100e18 - maxUsdcShares); + // // psm.withdraw(address(sDai), 100e18 - maxUsdcShares, 0); // // uint256 sDaiUser1Balance = 7.407406790123452675e18; @@ -765,7 +765,7 @@ contract PSMWithdrawTests is PSMTestBase { // // console2.log("Third CTA", psm.convertToAssetValue(100e18)); // // // Withdraw shares originally worth $100 to compare yield with user1 - // // psm.withdraw(address(sDai), 100e18); + // // psm.withdraw(address(sDai), 100e18, 0); // // // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); // // // assertEq(sDai.balanceOf(user2), 100e18 - sDaiUser1Balance - 1); From 57c65d6c166a2366ee51c88c2e4f77d221d7d511 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 22 May 2024 16:57:28 -0400 Subject: [PATCH 20/96] fix: update to rm consolegp --- src/PSM.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index a4a5c24..e05a050 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import { console2 } from "forge-std/console2.sol"; - import { IERC20 } from "erc20-helpers/interfaces/IERC20.sol"; import { SafeERC20 } from "erc20-helpers/SafeERC20.sol"; From 416034f222e16a080590b0d490b328dc652c03fe Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 23 May 2024 09:12:51 -0400 Subject: [PATCH 21/96] feat: add events testing --- src/PSM.sol | 3 +- src/interfaces/IPSM.sol | 7 +++ test/Events.t.sol | 128 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 test/Events.t.sol diff --git a/src/PSM.sol b/src/PSM.sol index 3409a83..b9ee35c 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -11,7 +11,6 @@ interface IRateProviderLike { function getConversionRate() external view returns (uint256); } -// TODO: Add events and corresponding tests // TODO: Determine what admin functionality we want (fees?) // TODO: Refactor into inheritance structure // TODO: Prove that we're always rounding against user @@ -102,6 +101,8 @@ contract PSM is IPSM { totalShares += initialBurnAmount; newShares -= initialBurnAmount; + + emit InitialSharesBurned(msg.sender, initialBurnAmount); } shares[msg.sender] += newShares; diff --git a/src/interfaces/IPSM.sol b/src/interfaces/IPSM.sol index 82cc133..3f6c674 100644 --- a/src/interfaces/IPSM.sol +++ b/src/interfaces/IPSM.sol @@ -45,6 +45,13 @@ interface IPSM { uint16 referralCode ); + /** + * @dev Emitted when shares are burned from the first depositor's balance in the PSM. + * @param user Address of the user that burned the shares. + * @param sharesBurned Number of shares burned from the user. + */ + event InitialSharesBurned(address indexed user, uint256 sharesBurned); + /** * @dev Emitted when an asset is withdrawn from the PSM. * @param asset Address of the asset withdrawn. diff --git a/test/Events.t.sol b/test/Events.t.sol new file mode 100644 index 0000000..2a34741 --- /dev/null +++ b/test/Events.t.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import { MockERC20, PSMTestBase } from "test/PSMTestBase.sol"; + +contract PSMEventTests is PSMTestBase { + + event Swap( + address indexed assetIn, + address indexed assetOut, + address sender, + address indexed receiver, + uint256 amountIn, + uint256 amountOut, + uint16 referralCode + ); + + event Deposit( + address indexed asset, + address indexed user, + uint256 assetsDeposited, + uint256 sharesMinted, + uint16 referralCode + ); + + event InitialSharesBurned(address indexed user, uint256 sharesBurned); + + event Withdraw( + address indexed asset, + address indexed user, + uint256 assetsWithdrawn, + uint256 sharesBurned, + uint16 referralCode + ); + + address sender = makeAddr("sender"); + address receiver = makeAddr("receiver"); + + uint256 BURN_AMOUNT = 1000; + + function test_deposit_events() public { + vm.startPrank(sender); + + dai.mint(sender, 100e18); + dai.approve(address(psm), 100e18); + + vm.expectEmit(); + emit InitialSharesBurned(sender, BURN_AMOUNT); + vm.expectEmit(); + emit Deposit(address(dai), sender, 100e18, 100e18 - BURN_AMOUNT, 1); + psm.deposit(address(dai), 100e18, 1); + + usdc.mint(sender, 100e6); + usdc.approve(address(psm), 100e6); + + // Initial shares burned event not emitted + vm.expectEmit(); + emit Deposit(address(usdc), sender, 100e6, 100e18, 2); // Different code + psm.deposit(address(usdc), 100e6, 2); + + sDai.mint(sender, 100e18); + sDai.approve(address(psm), 100e18); + + // Initial shares burned event not emitted + vm.expectEmit(); + emit Deposit(address(sDai), sender, 100e18, 125e18, 3); // Different code + psm.deposit(address(sDai), 100e18, 3); + } + + function test_withdraw_events() public { + _deposit(sender, address(dai), 100e18); + _deposit(sender, address(usdc), 100e6); + _deposit(sender, address(sDai), 100e18); + + vm.startPrank(sender); + + vm.expectEmit(); + emit Withdraw(address(dai), sender, 100e18, 100e18, 1); + psm.withdraw(address(dai), 100e18, 1); + + vm.expectEmit(); + emit Withdraw(address(usdc), sender, 100e6, 100e18, 2); + psm.withdraw(address(usdc), 100e6, 2); + + // Amount of sDAI can't be withdrawn because of first deposit. + uint256 expectedAssetsWithdrawn = 100e18 - BURN_AMOUNT * 80/100; + + // First depositors withdraw is for less than they specify because of burn amount. + // Amount emitted in the event is the resulting withdrawal and burn amounts. + vm.expectEmit(); + emit Withdraw(address(sDai), sender, expectedAssetsWithdrawn, 125e18 - BURN_AMOUNT, 3); + psm.withdraw(address(sDai), expectedAssetsWithdrawn, 3); + } + + function test_swap_events() public { + dai.mint(address(psm), 1000e18); + usdc.mint(address(psm), 1000e6); + sDai.mint(address(psm), 1000e18); + + vm.startPrank(sender); + + _swapEventTest(address(dai), address(usdc), 100e18, 100e6, 1); + _swapEventTest(address(dai), address(sDai), 100e18, 80e18, 2); + + _swapEventTest(address(usdc), address(dai), 100e6, 100e18, 3); + _swapEventTest(address(usdc), address(sDai), 100e6, 80e18, 4); + + _swapEventTest(address(sDai), address(dai), 100e18, 125e18, 5); + _swapEventTest(address(sDai), address(usdc), 100e18, 125e6, 6); + } + + function _swapEventTest( + address assetIn, + address assetOut, + uint256 amountIn, + uint256 expectedAmountOut, + uint16 referralCode + ) internal { + MockERC20(assetIn).mint(sender, amountIn); + MockERC20(assetIn).approve(address(psm), amountIn); + + vm.expectEmit(); + emit Swap(assetIn, assetOut, sender, receiver, amountIn, expectedAmountOut, referralCode); + psm.swap(assetIn, assetOut, amountIn, 0, receiver, referralCode); + } +} From 114f92b701e4af3835d061040d02dd55d3c1c6c9 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 23 May 2024 09:43:28 -0400 Subject: [PATCH 22/96] feat: make precisions internal and add state var natspec --- src/PSM.sol | 46 ++++++++-------- src/interfaces/IPSM.sol | 117 ++++++++++++++++++++++++++++++---------- test/Events.t.sol | 4 +- 3 files changed, 116 insertions(+), 51 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index b9ee35c..2383ede 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -18,6 +18,10 @@ contract PSM is IPSM { using SafeERC20 for IERC20; + uint256 internal immutable _asset0Precision; + uint256 internal immutable _asset1Precision; + uint256 internal immutable _asset2Precision; + // NOTE: Assumption is made that asset2 is the yield-bearing counterpart of asset0 and asset1. // Examples: asset0 = USDC, asset1 = DAI, asset2 = sDAI IERC20 public immutable asset0; @@ -26,9 +30,6 @@ contract PSM is IPSM { address public immutable rateProvider; - uint256 public immutable asset0Precision; - uint256 public immutable asset1Precision; - uint256 public immutable asset2Precision; uint256 public immutable initialBurnAmount; uint256 public totalShares; @@ -53,9 +54,9 @@ contract PSM is IPSM { rateProvider = rateProvider_; - asset0Precision = 10 ** IERC20(asset0_).decimals(); - asset1Precision = 10 ** IERC20(asset1_).decimals(); - asset2Precision = 10 ** IERC20(asset2_).decimals(); + _asset0Precision = 10 ** IERC20(asset0_).decimals(); + _asset1Precision = 10 ** IERC20(asset1_).decimals(); + _asset2Precision = 10 ** IERC20(asset2_).decimals(); initialBurnAmount = initialBurnAmount_; } @@ -170,18 +171,18 @@ contract PSM is IPSM { public view override returns (uint256 amountOut) { if (assetIn == address(asset0)) { - if (assetOut == address(asset1)) return _previewOneToOneSwap(amountIn, asset0Precision, asset1Precision); - else if (assetOut == address(asset2)) return _previewSwapToAsset2(amountIn, asset0Precision); + if (assetOut == address(asset1)) return _previewOneToOneSwap(amountIn, _asset0Precision, _asset1Precision); + else if (assetOut == address(asset2)) return _previewSwapToAsset2(amountIn, _asset0Precision); } else if (assetIn == address(asset1)) { - if (assetOut == address(asset0)) return _previewOneToOneSwap(amountIn, asset1Precision, asset0Precision); - else if (assetOut == address(asset2)) return _previewSwapToAsset2(amountIn, asset1Precision); + if (assetOut == address(asset0)) return _previewOneToOneSwap(amountIn, _asset1Precision, _asset0Precision); + else if (assetOut == address(asset2)) return _previewSwapToAsset2(amountIn, _asset1Precision); } else if (assetIn == address(asset2)) { - if (assetOut == address(asset0)) return _previewSwapFromAsset2(amountIn, asset0Precision); - else if (assetOut == address(asset1)) return _previewSwapFromAsset2(amountIn, asset1Precision); + if (assetOut == address(asset0)) return _previewSwapFromAsset2(amountIn, _asset0Precision); + else if (assetOut == address(asset1)) return _previewSwapFromAsset2(amountIn, _asset1Precision); } revert("PSM/invalid-asset"); @@ -198,13 +199,13 @@ contract PSM is IPSM { uint256 assetValue = convertToAssetValue(numShares); - if (asset == address(asset0)) return assetValue * asset0Precision / 1e18; - else if (asset == address(asset1)) return assetValue * asset1Precision / 1e18; + if (asset == address(asset0)) return assetValue * _asset0Precision / 1e18; + else if (asset == address(asset1)) return assetValue * _asset1Precision / 1e18; // NOTE: Multiplying by 1e27 and dividing by 1e18 cancels to 1e9 in numerator return assetValue * 1e9 - * asset2Precision + * _asset2Precision / IRateProviderLike(rateProvider).getConversionRate(); } @@ -260,16 +261,19 @@ contract PSM is IPSM { } function _getAsset0Value(uint256 amount) internal view returns (uint256) { - return amount * 1e18 / asset0Precision; + return amount * 1e18 / _asset0Precision; } function _getAsset1Value(uint256 amount) internal view returns (uint256) { - return amount * 1e18 / asset1Precision; + return amount * 1e18 / _asset1Precision; } function _getAsset2Value(uint256 amount) internal view returns (uint256) { - // NOTE: Multiplying by 1e18 and dividing by 1e9 cancels to 1e9 in denominator - return amount * IRateProviderLike(rateProvider).getConversionRate() / 1e9 / asset2Precision; + // NOTE: Multiplying by 1e18 and dividing by 1e27 cancels to 1e9 in denominator + return amount + * IRateProviderLike(rateProvider).getConversionRate() + / 1e9 + / _asset2Precision; } function _isValidAsset(address asset) internal view returns (bool) { @@ -281,7 +285,7 @@ contract PSM is IPSM { { return amountIn * 1e27 - * asset2Precision + * _asset2Precision / IRateProviderLike(rateProvider).getConversionRate() / assetInPrecision; } @@ -293,7 +297,7 @@ contract PSM is IPSM { * IRateProviderLike(rateProvider).getConversionRate() * assetInPrecision / 1e27 - / asset2Precision; + / _asset2Precision; } function _previewOneToOneSwap( diff --git a/src/interfaces/IPSM.sol b/src/interfaces/IPSM.sol index 3f6c674..2517265 100644 --- a/src/interfaces/IPSM.sol +++ b/src/interfaces/IPSM.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; +import { IERC20 } from "erc20-helpers/interfaces/IERC20.sol"; + interface IPSM { // TODO: Determine priority for indexing @@ -29,6 +31,13 @@ interface IPSM { uint16 referralCode ); + /** + * @dev Emitted when shares are burned from the first depositor's balance in the PSM. + * @param user Address of the user that burned the shares. + * @param sharesBurned Number of shares burned from the user. + */ + event InitialSharesBurned(address indexed user, uint256 sharesBurned); + /** * @dev Emitted when an asset is deposited into the PSM. * @param asset Address of the asset deposited. @@ -45,13 +54,6 @@ interface IPSM { uint16 referralCode ); - /** - * @dev Emitted when shares are burned from the first depositor's balance in the PSM. - * @param user Address of the user that burned the shares. - * @param sharesBurned Number of shares burned from the user. - */ - event InitialSharesBurned(address indexed user, uint256 sharesBurned); - /** * @dev Emitted when an asset is withdrawn from the PSM. * @param asset Address of the asset withdrawn. @@ -68,6 +70,60 @@ interface IPSM { uint16 referralCode ); + /**********************************************************************************************/ + /*** State variables and immutables ***/ + /**********************************************************************************************/ + + /** + * @dev Returns the IERC20 interface representing asset0. This asset is one of the non-yield + * bearing assets in the PSM (e.g., USDC or DAI). + * @return The IERC20 interface of asset0. + */ + function asset0() external view returns (IERC20); + + /** + * @dev Returns the IERC20 interface representing asset1. This asset is one of the non-yield + * bearing assets in the PSM (e.g., USDC or DAI). + * @return The IERC20 interface of asset1. + */ + function asset1() external view returns (IERC20); + + /** + * @dev Returns the IERC20 interface representing asset2. This asset is the yield + * bearing asset in the PSM (e.g., sDAI). This asset queries its value from the + * rate provider. + * @return The IERC20 interface of asset2. + */ + function asset2() external view returns (IERC20); + + /** + * @dev Returns the address of the rate provider, a contract that provides the conversion + * rate between asset2 and the other two assets in the PSM (e.g., sDAI to USD). + * @return The address of the rate provider. + */ + function rateProvider() external view returns (address); + + /** + * @dev Returns the initial burn amount for shares. This value is set to prevent an inflation + * frontrunning attack on the first depositor in the PSM. Recommended value is 1000. + * @return The initial amount of shares to burn. + */ + function initialBurnAmount() external view returns (uint256); + + /** + * @dev Returns the total number of shares in the PSM. Shares represent ownership of the + * assets in the PSM and can be converted to assets at any time. + * @return The total number of shares. + */ + function totalShares() external view returns (uint256); + + /** + * @dev Returns the number of shares held by a specific user. + * @param user The address of the user. + * @return The number of shares held by the user. + */ + function shares(address user) external view returns (uint256); + /**********************************************************************************************/ /*** Swap functions ***/ /**********************************************************************************************/ @@ -114,7 +170,7 @@ interface IPSM { * Must be one of the supported assets in order to succeed. The amount withdrawn is * the minimum of the balance of the PSM, the max amount, and the max amount of assets * that the user's shares can be converted to. - * @param asset Address of the ERC-20 asset to deposit. + * @param asset Address of the ERC-20 asset to withdraw. * @param maxAssetsToWithdraw Max amount that the user is willing to withdraw. * @param referralCode Referral code for the withdrawal. * @return assetsWithdrawn Resulting amount of the asset withdrawn from the PSM. @@ -127,8 +183,8 @@ interface IPSM { /**********************************************************************************************/ /** - * @dev Returns the exact number of shares that would be minted for a given asset and - * amount to deposit. + * @dev View function that returns the exact number of shares that would be minted for a + * given asset and amount to deposit. * @param asset Address of the ERC-20 asset to deposit. * @param assets Amount of the asset to deposit into the PSM. * @return shares Number of shares to be minted to the user. @@ -136,10 +192,11 @@ interface IPSM { function previewDeposit(address asset, uint256 assets) external view returns (uint256 shares); /** - * @dev Returns the exact number of assets that would be withdrawn and corresponding shares - * that would be burned in a withdrawal for a given asset and max withdraw amount. The - * amount returned is the minimum of the balance of the PSM, the max amount, and the - * max amount of assets that the user's shares can be converted to. + * @dev View function that returns the exact number of assets that would be withdrawn and + * corresponding shares that would be burned in a withdrawal for a given asset and max + * withdraw amount. The amount returned is the minimum of the balance of the PSM, + * the max amount, and the max amount of assets that the user's shares + * can be converted to. * @param asset Address of the ERC-20 asset to withdraw. * @param maxAssetsToWithdraw Max amount that the user is willing to withdraw. * @return sharesToBurn Number of shares that would be burned in the withdrawal. @@ -153,9 +210,9 @@ interface IPSM { /**********************************************************************************************/ /** - * @dev Returns the exact amount of assetOut that would be received for a given amount of - * assetIn in a swap. The amount returned is converted based on the current value - * of the two assets used in the swap. + * @dev View function that returns the exact amount of assetOut that would be received for a + * given amount of assetIn in a swap. The amount returned is converted based on the + * current value of the two assets used in the swap. * @param assetIn Address of the ERC-20 asset to swap in. * @param assetOut Address of the ERC-20 asset to swap out. * @param amountIn Amount of the asset to swap in. @@ -169,32 +226,36 @@ interface IPSM { /**********************************************************************************************/ /** - * @dev Converts an amount of a given shares to the equivalent amount of + * @dev View function that converts an amount of a given shares to the equivalent amount of * assets for a specified asset. - * @param asset Address of the asset to use to convert. - * @param numShares Number of shares to convert to assets. - * @return assets Value of assets in asset-native units. + * @param asset Address of the asset to use to convert. + * @param numShares Number of shares to convert to assets. + * @return assets Value of assets in asset-native units. */ function convertToAssets(address asset, uint256 numShares) external view returns (uint256); /** - * @dev Converts an amount of a given shares to the equivalent amount of assetValue. + * @dev View function that converts an amount of a given shares to the equivalent + * amount of assetValue. * @param numShares Number of shares to convert to assetValue. * @return assetValue Value of assets in asset0 denominated in 18 decimals. */ function convertToAssetValue(uint256 numShares) external view returns (uint256); /** - * @dev Converts an amount of assetValue (18 decimal value denominated in asset0) - * to shares in the PSM based on the current exchange rate. + * @dev View function that converts an amount of assetValue (18 decimal value denominated in + * asset0 and asset1) to shares in the PSM based on the current exchange rate. + * Note that this rounds down on calculation so is intended to be used for quoting the + * current exchange rate. * @param assetValue 18 decimal value denominated in asset0 (e.g., 1e6 USDC = 1e18) * @return shares Number of shares that the assetValue is equivalent to. */ function convertToShares(uint256 assetValue) external view returns (uint256); /** - * @dev Converts an amount of a given asset to shares in the PSM based on the - * current exchange rate. + * @dev View function that converts an amount of a given asset to shares in the PSM based + * on the current exchange rate. Note that this rounds down on calculation so is + * intended to be used for quoting the current exchange rate. * @param asset Address of the ERC-20 asset to convert to shares. * @param assets Amount of assets in asset-native units. * @return shares Number of shares that the assetValue is equivalent to. @@ -206,8 +267,8 @@ interface IPSM { /**********************************************************************************************/ /** - * @dev Returns the total value of the balance of all assets in the PSM converted to - * asset0 denominated in 18 decimal precision. + * @dev View function that returns the total value of the balance of all assets in the PSM + * converted to asset0/asset1 terms denominated in 18 decimal precision. */ function getPsmTotalValue() external view returns (uint256); diff --git a/test/Events.t.sol b/test/Events.t.sol index 2a34741..bb7d8e5 100644 --- a/test/Events.t.sol +++ b/test/Events.t.sol @@ -17,6 +17,8 @@ contract PSMEventTests is PSMTestBase { uint16 referralCode ); + event InitialSharesBurned(address indexed user, uint256 sharesBurned); + event Deposit( address indexed asset, address indexed user, @@ -25,8 +27,6 @@ contract PSMEventTests is PSMTestBase { uint16 referralCode ); - event InitialSharesBurned(address indexed user, uint256 sharesBurned); - event Withdraw( address indexed asset, address indexed user, From 8c2f83b566f922391b56f59c4b0fe57a2c7f0c32 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 23 May 2024 09:44:49 -0400 Subject: [PATCH 23/96] feat: finish natspec --- src/interfaces/IPSM.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interfaces/IPSM.sol b/src/interfaces/IPSM.sol index 2517265..2081ca6 100644 --- a/src/interfaces/IPSM.sol +++ b/src/interfaces/IPSM.sol @@ -89,8 +89,8 @@ interface IPSM { function asset1() external view returns (IERC20); /** - * @dev Returns the IERC20 interface representing asset2. This asset is the yield - * bearing asset in the PSM (e.g., sDAI). This asset queries its value from the + * @dev Returns the IERC20 interface representing asset2. This asset is the yield-bearing + * asset in the PSM (e.g., sDAI). The value of this asset is queried from the * rate provider. * @return The IERC20 interface of asset2. */ From 61443c80834b9aeac4fb1ba95a8985c7c0e6e861 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 23 May 2024 10:02:05 -0400 Subject: [PATCH 24/96] feat: add readme --- README.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 13ab5fe..3159190 100644 --- a/README.md +++ b/README.md @@ -7,21 +7,80 @@ [foundry]: https://getfoundry.sh/ [foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg -PSM contracts to either: -- Convert between a tokenization of an asset (ex. USDC) and a yield-bearing version of the asset (ex. sDAI). -- Convert one to one between directly correlated assets (ex. USDC-DAI). +## Overview -## Usage +This repository contains the implementation of a Peg Stability Module (PSM) contract, which facilitates the swapping, depositing, and withdrawing of three given assets to maintain stability and ensure the peg of involved assets. The PSM supports both yield-bearing and non-yield-bearing assets. -```bash -forge build -``` +This overview provides the essential information needed to understand and interact with the PSM contract. For detailed implementation, refer to the contract code and `IPSM` interface documentation. ## Test ```bash forge test ``` +## Usage + +## Contracts + +### PSM Contract + +The core contract implementing the `IPSM` interface, providing functionality for swapping, depositing, and withdrawing assets. + +### IPSM Interface + +Defines the essential functions and events that the PSM contract implements. + +### IRateProviderLike Interface + +Defines the function to get the conversion rate between yield-bearing and non-yield-bearing assets. + +## PSM Contract Details + +### State Variables and Immutables + +- **`asset0`**: Non-yield-bearing base asset (e.g., USDC). +- **`asset1`**: Another non-yield-bearing base asset that is directly correlated to `asset0` (e.g., DAI). +- **`asset2`**: Yield-bearing version of both `asset0` and `asset1` (e.g., sDAI). +- **`rateProvider`**: Contract that returns a conversion rate between and `asset2` and the base asset (e.g., sDAI to USD) in 1e27 precision. +- **`initialBurnAmount`**: Initial shares burned to prevent an inflation frontrunning attack (more info on this [here](https://mixbytes.io/blog/overview-of-the-inflation-attack)). +- **`totalShares`**: Total shares in the PSM. Shares represent the ownership of the underlying assets in the PSM. +- **`shares`**: Mapping of user addresses to their shares. + +### Functions + +#### Swap Functions + +- **`swap`**: Allows swapping of assets based on current conversion rates. Ensures the output amount meets the minimum required before executing the transfer and emitting the swap event. + +#### Liquidity Provision Functions + +- **`deposit`**: Deposits assets into the PSM, minting new shares. Handles the initial burn amount for the first deposit to prevent inflation frontrunning. +- **`withdraw`**: Withdraws assets from the PSM by burning shares. Ensures the user has sufficient shares for the withdrawal and adjusts the total shares accordingly. + +#### Preview Functions + +- **`previewDeposit`**: Estimates the number of shares minted for a given deposit amount. +- **`previewWithdraw`**: Estimates the number of shares burned and the amount of assets withdrawn for a specified amount. +- **`previewSwap`**: Estimates the amount of one asset received for a given amount of another asset in a swap. + +#### Conversion Functions + +NOTE: These functions do not round in the same way as preview functions, so they are meant to be used for general quoting purposes. + +- **`convertToAssets`**: Converts shares to the equivalent amount of a specified asset. +- **`convertToAssetValue`**: Converts shares to their equivalent value in base asset terms with 18 decimal precision (e.g., USD). +- **`convertToShares`**: Converts asset values to shares based on the current exchange rate. + +#### Asset Value Functions + +- **`getPsmTotalValue`**: Returns the total value of all assets held by the PSM denominated in the base asset with 18 decimal precision. (e.g., USD). + +### Events + +- **`Swap`**: Emitted on asset swaps. +- **`InitialSharesBurned`**: Emitted on the initial burn of shares. +- **`Deposit`**: Emitted on asset deposits. +- **`Withdraw`**: Emitted on asset withdrawals. *** *The IP in this repository was assigned to Mars SPC Limited in respect of the MarsOne SP* From 6ca25a87c5adcc7dbc81dfaebcbafe8b347da93b Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 23 May 2024 10:02:35 -0400 Subject: [PATCH 25/96] feat: add referral code note --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3159190..7d2ea89 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,12 @@ Defines the function to get the conversion rate between yield-bearing and non-yi #### Swap Functions -- **`swap`**: Allows swapping of assets based on current conversion rates. Ensures the output amount meets the minimum required before executing the transfer and emitting the swap event. +- **`swap`**: Allows swapping of assets based on current conversion rates. Ensures the output amount meets the minimum required before executing the transfer and emitting the swap event. Includes a referral code. #### Liquidity Provision Functions -- **`deposit`**: Deposits assets into the PSM, minting new shares. Handles the initial burn amount for the first deposit to prevent inflation frontrunning. -- **`withdraw`**: Withdraws assets from the PSM by burning shares. Ensures the user has sufficient shares for the withdrawal and adjusts the total shares accordingly. +- **`deposit`**: Deposits assets into the PSM, minting new shares. Handles the initial burn amount for the first deposit to prevent inflation frontrunning. Includes a referral code. +- **`withdraw`**: Withdraws assets from the PSM by burning shares. Ensures the user has sufficient shares for the withdrawal and adjusts the total shares accordingly. Includes a referral code. #### Preview Functions From c4f8c782e7fcb2fdf018725166e50a74f34af1fc Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 23 May 2024 10:05:55 -0400 Subject: [PATCH 26/96] fix: update constructor test --- test/Constructor.t.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/Constructor.t.sol b/test/Constructor.t.sol index a2edd82..e9ab5c8 100644 --- a/test/Constructor.t.sol +++ b/test/Constructor.t.sol @@ -38,9 +38,7 @@ contract PSMConstructorTests is PSMTestBase { assertEq(address(psm.asset2()), address(sDai)); assertEq(address(psm.rateProvider()), address(rateProvider)); - assertEq(psm.asset0Precision(), 10 ** dai.decimals()); - assertEq(psm.asset1Precision(), 10 ** usdc.decimals()); - assertEq(psm.asset2Precision(), 10 ** sDai.decimals()); + assertEq(psm.initialBurnAmount(), 1000); } } From b070a44ca9e6d3c5e7dc68f7a142d689180ebac6 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 23 May 2024 10:07:40 -0400 Subject: [PATCH 27/96] fix: update links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7d2ea89..7cb342b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Spark PSM -![Foundry CI](https://github.com/mars-foundation/spark-psm/actions/workflows/ci.yml/badge.svg) +![Foundry CI](https://github.com/marsfoundation/spark-psm/actions/workflows/ci.yml/badge.svg) [![Foundry][foundry-badge]][foundry] -[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://github.com/mars-foundation/spark-psm/blob/master/LICENSE) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://github.com/marsfoundation/spark-psm/blob/master/LICENSE) [foundry]: https://getfoundry.sh/ [foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg From 6947398b4091da5eba85ca73154b528408dc4c6f Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 23 May 2024 10:12:15 -0400 Subject: [PATCH 28/96] fix: reformatting --- README.md | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7cb342b..6b1c0d7 100644 --- a/README.md +++ b/README.md @@ -13,26 +13,11 @@ This repository contains the implementation of a Peg Stability Module (PSM) cont This overview provides the essential information needed to understand and interact with the PSM contract. For detailed implementation, refer to the contract code and `IPSM` interface documentation. -## Test - -```bash -forge test -``` -## Usage - ## Contracts -### PSM Contract - -The core contract implementing the `IPSM` interface, providing functionality for swapping, depositing, and withdrawing assets. - -### IPSM Interface - -Defines the essential functions and events that the PSM contract implements. - -### IRateProviderLike Interface - -Defines the function to get the conversion rate between yield-bearing and non-yield-bearing assets. +- **PSM Contract**: The core contract implementing the `IPSM` interface, providing functionality for swapping, depositing, and withdrawing assets. +- **IPSM Interface**: Defines the essential functions and events that the PSM contract implements. +- **IRateProviderLike Interface**: Defines the function to get the conversion rate between yield-bearing and non-yield-bearing assets. ## PSM Contract Details @@ -82,5 +67,15 @@ NOTE: These functions do not round in the same way as preview functions, so they - **`Deposit`**: Emitted on asset deposits. - **`Withdraw`**: Emitted on asset withdrawals. +## Test + +```bash +forge test +``` + *** *The IP in this repository was assigned to Mars SPC Limited in respect of the MarsOne SP* + +

+ +

From 9bdcfc5da4558d9c0702579c5e182c9c881b1225 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 23 May 2024 10:13:21 -0400 Subject: [PATCH 29/96] fix: update testing section --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b1c0d7..93a7acc 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,9 @@ NOTE: These functions do not round in the same way as preview functions, so they - **`Deposit`**: Emitted on asset deposits. - **`Withdraw`**: Emitted on asset withdrawals. -## Test +## Running Tests + +To run tests in this repo, run: ```bash forge test From d69bc8b6bab880c2a3185f446f5f5be46f4c1f40 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 23 May 2024 10:21:50 -0400 Subject: [PATCH 30/96] fix: improve overview --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 93a7acc..94b08c0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,13 @@ This repository contains the implementation of a Peg Stability Module (PSM) contract, which facilitates the swapping, depositing, and withdrawing of three given assets to maintain stability and ensure the peg of involved assets. The PSM supports both yield-bearing and non-yield-bearing assets. -This overview provides the essential information needed to understand and interact with the PSM contract. For detailed implementation, refer to the contract code and `IPSM` interface documentation. +`asset0` and `asset1` are two ERC20 tokens that are directly correlated and are non-yield-bearing, referred to as "base assets". `asset2` is a yield-bearing version of both `asset0` and `asset1`. The PSM contract allows users to swap between these assets, deposit any of the assets to mint shares, and withdraw any of the assets by burning shares. + +The conversion between a base asset and `asset2` is provided by a rate provider contract. The rate provider returns the conversion rate between `asset2` and the base asset in 1e27 precision. The conversion between the base assets is one to one. + +The conversion rate between assets and shares is based on the total value of assets held by the PSM. The total value is calculated by converting the assets to their equivalent value in the base asset with 18 decimal precision. The shares represent the ownership of the underlying assets in the PSM. Since three assets are used, each with different precisions and values, they are converted to a common base asset-denominated value for share conversions. + +This README provides the essential information needed to understand and interact with the PSM contract. For detailed implementation, refer to the contract code and `IPSM` interface documentation. ## Contracts @@ -76,7 +82,7 @@ forge test ``` *** -*The IP in this repository was assigned to Mars SPC Limited in respect of the MarsOne SP* +*The IP in this repository was assigned to Mars SPC Limited in respect of the MarsOne SP.*

From 04cc6ac7147dca69353f7b205e15854df739905c Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 23 May 2024 10:23:18 -0400 Subject: [PATCH 31/96] feat: add emojis --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 94b08c0..4c9c5c5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Spark PSM +# âš¡ Spark PSM âš¡ ![Foundry CI](https://github.com/marsfoundation/spark-psm/actions/workflows/ci.yml/badge.svg) [![Foundry][foundry-badge]][foundry] From 54a4afefde8a33a64c23612e0f70d6760464f4a6 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 30 May 2024 07:33:54 -0400 Subject: [PATCH 32/96] feat: remove all share burn logic, get all non inflation attack tests to pass --- src/PSM.sol | 12 +----- test/Constructor.t.sol | 8 ++-- test/Deposit.t.sol | 40 ++++++----------- test/Getters.t.sol | 2 +- test/InflationAttack.t.sol | 6 +-- test/PSMTestBase.sol | 2 +- test/Withdraw.t.sol | 81 +++++++++++++---------------------- test/harnesses/PSMHarness.sol | 4 +- 8 files changed, 54 insertions(+), 101 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index e5b363c..f411e4e 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -27,7 +27,6 @@ contract PSM { uint256 public immutable asset0Precision; uint256 public immutable asset1Precision; - uint256 public immutable initialBurnAmount; uint256 public totalShares; @@ -36,8 +35,7 @@ contract PSM { constructor( address asset0_, address asset1_, - address rateProvider_, - uint256 initialBurnAmount_ + address rateProvider_ ) { require(asset0_ != address(0), "PSM/invalid-asset0"); require(asset1_ != address(0), "PSM/invalid-asset1"); @@ -49,7 +47,6 @@ contract PSM { asset0Precision = 10 ** IERC20(asset0_).decimals(); asset1Precision = 10 ** IERC20(asset1_).decimals(); - initialBurnAmount = initialBurnAmount_; } /**********************************************************************************************/ @@ -89,13 +86,6 @@ contract PSM { { newShares = previewDeposit(asset, assetsToDeposit); - if (totalShares == 0 && initialBurnAmount != 0) { - shares[address(0)] += initialBurnAmount; - totalShares += initialBurnAmount; - - newShares -= initialBurnAmount; - } - shares[msg.sender] += newShares; totalShares += newShares; diff --git a/test/Constructor.t.sol b/test/Constructor.t.sol index d505441..cdec19d 100644 --- a/test/Constructor.t.sol +++ b/test/Constructor.t.sol @@ -11,22 +11,22 @@ contract PSMConstructorTests is PSMTestBase { function test_constructor_invalidAsset0() public { vm.expectRevert("PSM/invalid-asset0"); - new PSM(address(0), address(sDai), address(rateProvider), 1000); + new PSM(address(0), address(sDai), address(rateProvider)); } function test_constructor_invalidAsset1() public { vm.expectRevert("PSM/invalid-asset1"); - new PSM(address(usdc), address(0), address(rateProvider), 1000); + new PSM(address(usdc), address(0), address(rateProvider)); } function test_constructor_invalidRateProvider() public { vm.expectRevert("PSM/invalid-rateProvider"); - new PSM(address(sDai), address(usdc), address(0), 1000); + new PSM(address(sDai), address(usdc), address(0)); } function test_constructor() public { // Deploy new PSM to get test coverage - psm = new PSM(address(usdc), address(sDai), address(rateProvider), 1000); + psm = new PSM(address(usdc), address(sDai), address(rateProvider)); assertEq(address(psm.asset0()), address(usdc)); assertEq(address(psm.asset1()), address(sDai)); diff --git a/test/Deposit.t.sol b/test/Deposit.t.sol index f9924f8..24ce76a 100644 --- a/test/Deposit.t.sol +++ b/test/Deposit.t.sol @@ -11,9 +11,6 @@ contract PSMDepositTests is PSMTestBase { address user1 = makeAddr("user1"); address user2 = makeAddr("user2"); - address burn = address(0); - - uint256 BURN_AMOUNT = 1000; function test_deposit_notAsset0OrAsset1() public { vm.expectRevert("PSM/invalid-asset"); @@ -35,7 +32,6 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.totalShares(), 0); assertEq(psm.shares(user1), 0); - assertEq(psm.shares(burn), 0); assertEq(psm.convertToShares(1e18), 1e18); @@ -46,8 +42,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(usdc.balanceOf(address(psm)), 100e6); assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 100e18 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), 100e18); assertEq(psm.convertToShares(1e18), 1e18); } @@ -65,7 +60,6 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.totalShares(), 0); assertEq(psm.shares(user1), 0); - assertEq(psm.shares(burn), 0); assertEq(psm.convertToShares(1e18), 1e18); @@ -76,8 +70,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 100e18); assertEq(psm.totalShares(), 125e18); - assertEq(psm.shares(user1), 125e18 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), 125e18); assertEq(psm.convertToShares(1e18), 1e18); } @@ -101,8 +94,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 0); assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 100e18 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), 100e18); assertEq(psm.convertToShares(1e18), 1e18); @@ -115,16 +107,14 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 100e18); assertEq(psm.totalShares(), 225e18); - assertEq(psm.shares(user1), 225e18 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); // Only burn on first deposit + assertEq(psm.shares(user1), 225e18); assertEq(psm.convertToShares(1e18), 1e18); } function testFuzz_deposit_usdcThenSDai(uint256 usdcAmount, uint256 sDaiAmount) public { - // NOTE: Deposits revert if deposit amount is less than the burn amount - usdcAmount = _bound(usdcAmount, BURN_AMOUNT, USDC_TOKEN_MAX); - sDaiAmount = _bound(sDaiAmount, BURN_AMOUNT, SDAI_TOKEN_MAX); + usdcAmount = _bound(usdcAmount, 0, USDC_TOKEN_MAX); + sDaiAmount = _bound(sDaiAmount, 0, SDAI_TOKEN_MAX); usdc.mint(user1, usdcAmount); @@ -144,8 +134,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 0); assertEq(psm.totalShares(), usdcAmount * 1e12); - assertEq(psm.shares(user1), usdcAmount * 1e12 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), usdcAmount * 1e12); assertEq(psm.convertToShares(1e18), 1e18); @@ -158,8 +147,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), sDaiAmount); assertEq(psm.totalShares(), usdcAmount * 1e12 + sDaiAmount * 125/100); - assertEq(psm.shares(user1), usdcAmount * 1e12 + sDaiAmount * 125/100 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); // Only burn on first deposit + assertEq(psm.shares(user1), usdcAmount * 1e12 + sDaiAmount * 125/100); assertEq(psm.convertToShares(1e18), 1e18); } @@ -187,12 +175,11 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 100e18); assertEq(psm.totalShares(), 225e18); - assertEq(psm.shares(user1), 225e18 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), 225e18); assertEq(psm.convertToShares(1e18), 1e18); - assertEq(psm.convertToAssetValue(psm.shares(user1)), 225e18 - BURN_AMOUNT); + assertEq(psm.convertToAssetValue(psm.shares(user1)), 225e18); rateProvider.__setConversionRate(1.5e27); @@ -212,7 +199,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(user2), 100e18); assertEq(sDai.balanceOf(address(psm)), 100e18); - assertEq(psm.convertToAssetValue(psm.shares(user1)), 250e18 - 1112); // Burn amount conversion + assertEq(psm.convertToAssetValue(psm.shares(user1)), 250e18); assertEq(psm.convertToAssetValue(psm.shares(user2)), 0); assertEq(psm.getPsmTotalValue(), 250e18); @@ -231,12 +218,11 @@ contract PSMDepositTests is PSMTestBase { assertEq(expectedShares, 135e18); assertEq(psm.totalShares(), 360e18); - assertEq(psm.shares(user1), 225e18 - BURN_AMOUNT); + assertEq(psm.shares(user1), 225e18); assertEq(psm.shares(user2), 135e18); - assertEq(psm.shares(burn), BURN_AMOUNT); // User 1 earned $25 on 225, User 2 has earned nothing - assertEq(psm.convertToAssetValue(psm.shares(user1)), 250e18 - 1112); + assertEq(psm.convertToAssetValue(psm.shares(user1)), 250e18); assertEq(psm.convertToAssetValue(psm.shares(user2)), 150e18); assertEq(psm.getPsmTotalValue(), 400e18); diff --git a/test/Getters.t.sol b/test/Getters.t.sol index 895f445..911ff91 100644 --- a/test/Getters.t.sol +++ b/test/Getters.t.sol @@ -13,7 +13,7 @@ contract PSMHarnessTests is PSMTestBase { function setUp() public override { super.setUp(); - psmHarness = new PSMHarness(address(usdc), address(sDai), address(rateProvider), 1000); + psmHarness = new PSMHarness(address(usdc), address(sDai), address(rateProvider)); } function test_getAsset0Value() public view { diff --git a/test/InflationAttack.t.sol b/test/InflationAttack.t.sol index 9f0e189..f0b4c4d 100644 --- a/test/InflationAttack.t.sol +++ b/test/InflationAttack.t.sol @@ -10,7 +10,7 @@ import { PSMTestBase } from "test/PSMTestBase.sol"; contract InflationAttackTests is PSMTestBase { function test_inflationAttack_noInitialBurnAmount() public { - psm = new PSM(address(usdc), address(sDai), address(rateProvider), 0); + psm = new PSM(address(usdc), address(sDai), address(rateProvider)); address firstDepositor = makeAddr("firstDepositor"); address frontRunner = makeAddr("frontRunner"); @@ -55,7 +55,7 @@ contract InflationAttackTests is PSMTestBase { } function test_inflationAttack_useInitialBurnAmount_firstDepositOverflowBoundary() public { - psm = new PSM(address(usdc), address(sDai), address(rateProvider), 1000); + psm = new PSM(address(usdc), address(sDai), address(rateProvider)); address frontRunner = makeAddr("frontRunner"); @@ -71,7 +71,7 @@ contract InflationAttackTests is PSMTestBase { } function test_inflationAttack_useInitialBurnAmount() public { - psm = new PSM(address(usdc), address(sDai), address(rateProvider), 1000); + psm = new PSM(address(usdc), address(sDai), address(rateProvider)); address firstDepositor = makeAddr("firstDepositor"); address frontRunner = makeAddr("frontRunner"); diff --git a/test/PSMTestBase.sol b/test/PSMTestBase.sol index 8675d7c..625b685 100644 --- a/test/PSMTestBase.sol +++ b/test/PSMTestBase.sol @@ -38,7 +38,7 @@ contract PSMTestBase is Test { // NOTE: Using 1.25 for easy two way conversions rateProvider.__setConversionRate(1.25e27); - psm = new PSM(address(usdc), address(sDai), address(rateProvider), 1000); + psm = new PSM(address(usdc), address(sDai), address(rateProvider)); vm.label(address(sDai), "sDAI"); vm.label(address(usdc), "USDC"); diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index fb84371..bcabccb 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -13,9 +13,6 @@ contract PSMWithdrawTests is PSMTestBase { address user1 = makeAddr("user1"); address user2 = makeAddr("user2"); - address burn = address(0); - - uint256 BURN_AMOUNT = 1000; function test_withdraw_notAsset0OrAsset1() public { vm.expectRevert("PSM/invalid-asset"); @@ -31,24 +28,20 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(usdc.balanceOf(address(psm)), 100e6); assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 100e18 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), 100e18); assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); uint256 amount = psm.withdraw(address(usdc), 100e6); - // Burn amount causes shares to round down by one since shares are 99.999... - assertEq(amount, 100e6 - 1); + assertEq(amount, 100e6); - assertEq(usdc.balanceOf(user1), 100e6 - 1); - assertEq(usdc.balanceOf(address(psm)), 1); + assertEq(usdc.balanceOf(user1), 100e6); + assertEq(usdc.balanceOf(address(psm)), 0); - // User still has left over shares from rounding on 1e6 - assertEq(psm.totalShares(), 1e12); - assertEq(psm.shares(user1), 1e12 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.totalShares(), 0); + assertEq(psm.shares(user1), 0); assertEq(psm.convertToShares(1e18), 1e18); } @@ -60,25 +53,20 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 80e18); assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 100e18 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); - - // This is the amount that this user will not be able to withdraw - assertEq(psm.convertToAssets(address(sDai), BURN_AMOUNT), 800); + assertEq(psm.shares(user1), 100e18); assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); uint256 amount = psm.withdraw(address(sDai), 80e18); - assertEq(amount, 80e18 - 800); + assertEq(amount, 80e18); - assertEq(sDai.balanceOf(user1), 80e18 - 800); - assertEq(sDai.balanceOf(address(psm)), 800); + assertEq(sDai.balanceOf(user1), 80e18); + assertEq(sDai.balanceOf(address(psm)), 0); - assertEq(psm.totalShares(), BURN_AMOUNT); + assertEq(psm.totalShares(), 0); assertEq(psm.shares(user1), 0); - assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); } @@ -94,8 +82,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 100e18); assertEq(psm.totalShares(), 225e18); - assertEq(psm.shares(user1), 225e18 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), 225e18); assertEq(psm.convertToShares(1e18), 1e18); @@ -111,28 +98,23 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(sDai.balanceOf(address(psm)), 100e18); assertEq(psm.totalShares(), 125e18); - assertEq(psm.shares(user1), 125e18 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), 125e18); assertEq(psm.convertToShares(1e18), 1e18); - // This is the amount that this user will not be able to withdraw - assertEq(psm.convertToAssets(address(sDai), BURN_AMOUNT), 800); - vm.prank(user1); amount = psm.withdraw(address(sDai), 100e18); - assertEq(amount, 100e18 - 800); + assertEq(amount, 100e18); assertEq(usdc.balanceOf(user1), 100e6); assertEq(usdc.balanceOf(address(psm)), 0); - assertEq(sDai.balanceOf(user1), 100e18 - 800); - assertEq(sDai.balanceOf(address(psm)), 800); + assertEq(sDai.balanceOf(user1), 100e18); + assertEq(sDai.balanceOf(address(psm)), 0); - assertEq(psm.totalShares(), BURN_AMOUNT); + assertEq(psm.totalShares(), 0); assertEq(psm.shares(user1), 0); - assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.convertToShares(1e18), 1e18); } @@ -145,8 +127,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(usdc.balanceOf(address(psm)), 100e6); assertEq(psm.totalShares(), 225e18); - assertEq(psm.shares(user1), 225e18 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), 225e18); assertEq(psm.convertToShares(1e18), 1e18); @@ -159,8 +140,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(usdc.balanceOf(address(psm)), 0); assertEq(psm.totalShares(), 125e18); // Only burns $100 of shares - assertEq(psm.shares(user1), 125e18 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), 125e18); } function test_withdraw_amountHigherThanUserShares() public { @@ -198,10 +178,11 @@ contract PSMWithdrawTests is PSMTestBase { ) public { - // NOTE: Deposits revert if deposit amount is less than the burn amount - depositAmount1 = bound(depositAmount1, BURN_AMOUNT, USDC_TOKEN_MAX); - depositAmount2 = bound(depositAmount2, BURN_AMOUNT, USDC_TOKEN_MAX); - depositAmount3 = bound(depositAmount3, BURN_AMOUNT, SDAI_TOKEN_MAX); + // NOTE: Not covering zero cases, 1e-2 at 1e6 used as min for now so exact values can + // be asserted + depositAmount1 = bound(depositAmount1, 0, USDC_TOKEN_MAX); + depositAmount2 = bound(depositAmount2, 0, USDC_TOKEN_MAX); + depositAmount3 = bound(depositAmount3, 0, SDAI_TOKEN_MAX); withdrawAmount1 = bound(withdrawAmount1, 0, USDC_TOKEN_MAX); withdrawAmount2 = bound(withdrawAmount2, 0, USDC_TOKEN_MAX); @@ -217,8 +198,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(usdc.balanceOf(user1), 0); assertEq(usdc.balanceOf(address(psm)), totalUsdc); - assertEq(psm.shares(user1), depositAmount1 * 1e12 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), depositAmount1 * 1e12); assertEq(psm.totalShares(), totalValue); uint256 expectedWithdrawnAmount1 @@ -240,9 +220,8 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(usdc.balanceOf(user2), 0); assertEq(usdc.balanceOf(address(psm)), totalUsdc - expectedWithdrawnAmount1); - assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12 - BURN_AMOUNT); + assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); assertEq(psm.shares(user2), depositAmount2 * 1e12 + depositAmount3 * 125/100); // Includes sDAI deposit - assertEq(psm.shares(burn), BURN_AMOUNT); assertEq(psm.totalShares(), totalValue - expectedWithdrawnAmount1 * 1e12); uint256 expectedWithdrawnAmount2 @@ -267,8 +246,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(sDai.balanceOf(user2), 0); assertEq(sDai.balanceOf(address(psm)), depositAmount3); - assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); assertEq( psm.shares(user2), @@ -302,8 +280,7 @@ contract PSMWithdrawTests is PSMTestBase { assertApproxEqAbs(sDai.balanceOf(user2), expectedWithdrawnAmount3, 1); assertApproxEqAbs(sDai.balanceOf(address(psm)), depositAmount3 - expectedWithdrawnAmount3, 1); - assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12 - BURN_AMOUNT); - assertEq(psm.shares(burn), BURN_AMOUNT); + assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); assertApproxEqAbs( psm.shares(user2), @@ -333,7 +310,7 @@ contract PSMWithdrawTests is PSMTestBase { // ); } - function _checkPsmInvariant() internal view { + function _checkPsmInvariant() internal { uint256 totalSharesValue = psm.convertToAssetValue(psm.totalShares()); uint256 totalAssetsValue = sDai.balanceOf(address(psm)) * rateProvider.getConversionRate() / 1e27 diff --git a/test/harnesses/PSMHarness.sol b/test/harnesses/PSMHarness.sol index 48dfcf9..6a5eaa7 100644 --- a/test/harnesses/PSMHarness.sol +++ b/test/harnesses/PSMHarness.sol @@ -5,8 +5,8 @@ import { PSM } from "src/PSM.sol"; contract PSMHarness is PSM { - constructor(address asset0_, address asset1_, address rateProvider_, uint256 initialBurnAmount_) - PSM(asset0_, asset1_, rateProvider_, initialBurnAmount_) {} + constructor(address asset0_, address asset1_, address rateProvider_) + PSM(asset0_, asset1_, rateProvider_) {} function getAssetValue(address asset, uint256 amount) external view returns (uint256) { return _getAssetValue(asset, amount); From ce5653d46c2db8f39f0c7e1466ad93fd7a00b14e Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 30 May 2024 07:35:15 -0400 Subject: [PATCH 33/96] fix: cleanup diff --- src/PSM.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index f411e4e..5dcc952 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -45,8 +45,8 @@ contract PSM { asset1 = IERC20(asset1_); rateProvider = rateProvider_; - asset0Precision = 10 ** IERC20(asset0_).decimals(); - asset1Precision = 10 ** IERC20(asset1_).decimals(); + asset0Precision = 10 ** IERC20(asset0_).decimals(); + asset1Precision = 10 ** IERC20(asset1_).decimals(); } /**********************************************************************************************/ From 259cf16092399205e36a12d43ba289c7d02c7cce Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 30 May 2024 07:51:47 -0400 Subject: [PATCH 34/96] fix: update to use initial deposit instead of burn --- src/PSM.sol | 1 + test/InflationAttack.t.sol | 35 +++++++++++------------------------ 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index 5dcc952..679c5c2 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -14,6 +14,7 @@ interface IRateProviderLike { // TODO: Refactor into inheritance structure // TODO: Add interface with natspec and inherit // TODO: Prove that we're always rounding against user +// TODO: Add receiver to deposit/withdraw contract PSM { using SafeERC20 for IERC20; diff --git a/test/InflationAttack.t.sol b/test/InflationAttack.t.sol index f0b4c4d..6c0c047 100644 --- a/test/InflationAttack.t.sol +++ b/test/InflationAttack.t.sol @@ -9,7 +9,9 @@ import { PSMTestBase } from "test/PSMTestBase.sol"; contract InflationAttackTests is PSMTestBase { - function test_inflationAttack_noInitialBurnAmount() public { + // TODO: Add DOS attack test outlined here: https://github.com/marsfoundation/spark-psm/pull/2#pullrequestreview-2085880206 + + function test_inflationAttack_noInitialDeposit() public { psm = new PSM(address(usdc), address(sDai), address(rateProvider)); address firstDepositor = makeAddr("firstDepositor"); @@ -54,32 +56,19 @@ contract InflationAttackTests is PSMTestBase { assertEq(usdc.balanceOf(frontRunner), 15_000_000e6); } - function test_inflationAttack_useInitialBurnAmount_firstDepositOverflowBoundary() public { - psm = new PSM(address(usdc), address(sDai), address(rateProvider)); - - address frontRunner = makeAddr("frontRunner"); - - vm.startPrank(frontRunner); - sDai.mint(frontRunner, 800); - sDai.approve(address(psm), 800); - - vm.expectRevert(stdError.arithmeticError); - psm.deposit(address(sDai), 799); - - // 800 sDAI = 1000 shares - psm.deposit(address(sDai), 800); - } - - function test_inflationAttack_useInitialBurnAmount() public { + function test_inflationAttack_useInitialDeposit() public { psm = new PSM(address(usdc), address(sDai), address(rateProvider)); address firstDepositor = makeAddr("firstDepositor"); address frontRunner = makeAddr("frontRunner"); + address deployer = address(this); // TODO: Update to use non-deployer receiver + + _deposit(address(this), address(sDai), 800); /// 1000 shares // Step 1: Front runner deposits 801 sDAI to get 1 share - // 1000 shares get burned, user is left with 1 - _deposit(frontRunner, address(sDai), 801); + // User tries to do the same attack, depositing one sDAI for 1 share + _deposit(frontRunner, address(sDai), 1); assertEq(psm.shares(frontRunner), 1); @@ -107,14 +96,12 @@ contract InflationAttackTests is PSMTestBase { _withdraw(firstDepositor, address(usdc), type(uint256).max); _withdraw(frontRunner, address(usdc), type(uint256).max); - - // Burnt shares have a claim on these - // TODO: Should this be an admin contract instead of address(0)? - assertEq(usdc.balanceOf(address(psm)), 9_993_337.774818e6); + _withdraw(deployer, address(usdc), type(uint256).max); // Front runner loses 9.99m USDC, first depositor loses 4k USDC assertEq(usdc.balanceOf(firstDepositor), 19_996_668.887408e6); assertEq(usdc.balanceOf(frontRunner), 9_993.337774e6); + assertEq(usdc.balanceOf(deployer), 9_993_337.774818e6); } } From 62ef5c2fdb22a06544a34f54e8552b83541a84f4 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 30 May 2024 08:00:44 -0400 Subject: [PATCH 35/96] feat: add readme section explaining attack --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 13ab5fe..e3336fb 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ PSM contracts to either: - Convert between a tokenization of an asset (ex. USDC) and a yield-bearing version of the asset (ex. sDAI). - Convert one to one between directly correlated assets (ex. USDC-DAI). +## [CRITICAL]: First Depositor Attack Prevention on Deployment + +On the deployment of the PSM, the deployer **MUST make an initial deposit in order to protect the first depositor from getting attacked with a share inflation attack**. This is outlined further [here](https://github.com/marsfoundation/spark-automations/assets/44272939/9472a6d2-0361-48b0-b534-96a0614330d3). 1000 shares minted is determined to be sufficient to prevent this attack. Technical details related to this can be found in `test/InflationAttack.t.sol`. The deployment script [TODO] in this repo contains logic for the deployer to perform this initial deposit, so it is **HIGHLY RECOMMENDED** to use this deployment script when deploying the PSM. Reasoning for the technical implementation approach taken is outlined in more detail [here](https://github.com/marsfoundation/spark-psm/pull/2). + ## Usage ```bash From 07201506a1009dd92de775096ec21da3c01387ac Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 30 May 2024 08:01:48 -0400 Subject: [PATCH 36/96] fix: minimize diff --- src/PSM.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index 679c5c2..d9bd589 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -33,11 +33,7 @@ contract PSM { mapping(address user => uint256 shares) public shares; - constructor( - address asset0_, - address asset1_, - address rateProvider_ - ) { + constructor(address asset0_, address asset1_, address rateProvider_) { require(asset0_ != address(0), "PSM/invalid-asset0"); require(asset1_ != address(0), "PSM/invalid-asset1"); require(rateProvider_ != address(0), "PSM/invalid-rateProvider"); From e6c654db79d6fd477ff84f2d21ba740b5c180827 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 30 May 2024 08:28:15 -0400 Subject: [PATCH 37/96] fix: address bartek comments --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0e761b1..26fdb5e 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,12 @@ The conversion between a base asset and `asset2` is provided by a rate provider The conversion rate between assets and shares is based on the total value of assets held by the PSM. The total value is calculated by converting the assets to their equivalent value in the base asset with 18 decimal precision. The shares represent the ownership of the underlying assets in the PSM. Since three assets are used, each with different precisions and values, they are converted to a common base asset-denominated value for share conversions. -This README provides the essential information needed to understand and interact with the PSM contract. For detailed implementation, refer to the contract code and `IPSM` interface documentation. +For detailed implementation, refer to the contract code and `IPSM` interface documentation. ## Contracts -- **PSM Contract**: The core contract implementing the `IPSM` interface, providing functionality for swapping, depositing, and withdrawing assets. -- **IPSM Interface**: Defines the essential functions and events that the PSM contract implements. -- **IRateProviderLike Interface**: Defines the function to get the conversion rate between yield-bearing and non-yield-bearing assets. +- **`src/PSM.sol`**: The core contract implementing the `IPSM` interface, providing functionality for swapping, depositing, and withdrawing assets. +- **`src/interfaces/IPSM.sol`**: Defines the essential functions and events that the PSM contract implements. ## [CRITICAL]: First Depositor Attack Prevention on Deployment From e540afa3bdb2795e8a1ea950f12a4d065aae85f8 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 30 May 2024 13:41:52 -0400 Subject: [PATCH 38/96] feat: update all tests to work with new interfaces --- src/PSM.sol | 21 +++--- src/interfaces/IPSM.sol | 16 ++++- test/Conversions.t.sol | 54 +++++++------- test/Deposit.t.sol | 134 +++++++++++++++++++++++------------ test/Events.t.sol | 32 +++++---- test/InflationAttack.t.sol | 20 +++--- test/PSMTestBase.sol | 16 +++-- test/Withdraw.t.sol | 140 ++++++++++++++++++++++--------------- 8 files changed, 265 insertions(+), 168 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index 08f87ce..759a5cc 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -14,7 +14,6 @@ interface IRateProviderLike { // TODO: Determine what admin functionality we want (fees?) // TODO: Refactor into inheritance structure // TODO: Prove that we're always rounding against user -// TODO: Add receiver to deposit/withdraw contract PSM is IPSM { using SafeERC20 for IERC20; @@ -83,22 +82,28 @@ contract PSM is IPSM { /*** Liquidity provision functions ***/ /**********************************************************************************************/ - function deposit(address asset, uint256 assetsToDeposit, uint16 referralCode) + function deposit(address asset, address receiver, uint256 assetsToDeposit, uint16 referralCode) external override returns (uint256 newShares) { + require(receiver != address(0), "PSM/invalid-receiver"); + require(assetsToDeposit != 0, "PSM/invalid-amount"); + newShares = previewDeposit(asset, assetsToDeposit); - shares[msg.sender] += newShares; - totalShares += newShares; + shares[receiver] += newShares; + totalShares += newShares; IERC20(asset).safeTransferFrom(msg.sender, address(this), assetsToDeposit); - emit Deposit(asset, msg.sender, assetsToDeposit, newShares, referralCode); + emit Deposit(asset, msg.sender, receiver, assetsToDeposit, newShares, referralCode); } - function withdraw(address asset, uint256 maxAssetsToWithdraw, uint16 referralCode) + function withdraw(address asset, address receiver, uint256 maxAssetsToWithdraw, uint16 referralCode) external override returns (uint256 assetsWithdrawn) { + require(receiver != address(0), "PSM/invalid-receiver"); + require(maxAssetsToWithdraw != 0, "PSM/invalid-amount"); + uint256 sharesToBurn; ( sharesToBurn, assetsWithdrawn ) = previewWithdraw(asset, maxAssetsToWithdraw); @@ -108,9 +113,9 @@ contract PSM is IPSM { totalShares -= sharesToBurn; } - IERC20(asset).safeTransfer(msg.sender, assetsWithdrawn); + IERC20(asset).safeTransfer(receiver, assetsWithdrawn); - emit Withdraw(asset, msg.sender, assetsWithdrawn, sharesToBurn, referralCode); + emit Withdraw(asset, msg.sender, receiver, assetsWithdrawn, sharesToBurn, referralCode); } /**********************************************************************************************/ diff --git a/src/interfaces/IPSM.sol b/src/interfaces/IPSM.sol index 8838d98..5acc9f4 100644 --- a/src/interfaces/IPSM.sol +++ b/src/interfaces/IPSM.sol @@ -35,6 +35,7 @@ interface IPSM { * @dev Emitted when an asset is deposited into the PSM. * @param asset Address of the asset deposited. * @param user Address of the user that deposited the asset. + * @param receiver Address of the receiver of the resulting shares from the deposit. * @param assetsDeposited Amount of the asset deposited. * @param sharesMinted Number of shares minted to the user. * @param referralCode Referral code for the deposit. @@ -42,6 +43,7 @@ interface IPSM { event Deposit( address indexed asset, address indexed user, + address indexed receiver, uint256 assetsDeposited, uint256 sharesMinted, uint16 referralCode @@ -51,6 +53,7 @@ interface IPSM { * @dev Emitted when an asset is withdrawn from the PSM. * @param asset Address of the asset withdrawn. * @param user Address of the user that withdrew the asset. + * @param receiver Address of the receiver of the withdrawn assets. * @param assetsWithdrawn Amount of the asset withdrawn. * @param sharesBurned Number of shares burned from the user. * @param referralCode Referral code for the withdrawal. @@ -58,6 +61,7 @@ interface IPSM { event Withdraw( address indexed asset, address indexed user, + address indexed receiver, uint256 assetsWithdrawn, uint256 sharesBurned, uint16 referralCode @@ -144,11 +148,12 @@ interface IPSM { * assets in order to succeed. The amount deposited is converted to shares based on * the current exchange rate. * @param asset Address of the ERC-20 asset to deposit. + * @param receiver Address of the receiver of the resulting shares from the deposit. * @param assetsToDeposit Amount of the asset to deposit into the PSM. * @param referralCode Referral code for the deposit. * @return newShares Number of shares minted to the user. */ - function deposit(address asset, uint256 assetsToDeposit, uint16 referralCode) + function deposit(address asset, address receiver, uint256 assetsToDeposit, uint16 referralCode) external returns (uint256 newShares); /** @@ -157,12 +162,17 @@ interface IPSM { * the minimum of the balance of the PSM, the max amount, and the max amount of assets * that the user's shares can be converted to. * @param asset Address of the ERC-20 asset to withdraw. + * @param receiver Address of the receiver of the withdrawn assets. * @param maxAssetsToWithdraw Max amount that the user is willing to withdraw. * @param referralCode Referral code for the withdrawal. * @return assetsWithdrawn Resulting amount of the asset withdrawn from the PSM. */ - function withdraw(address asset, uint256 maxAssetsToWithdraw, uint16 referralCode) - external returns (uint256 assetsWithdrawn); + function withdraw( + address asset, + address receiver, + uint256 maxAssetsToWithdraw, + uint16 referralCode + ) external returns (uint256 assetsWithdrawn); /**********************************************************************************************/ /*** Deposit/withdraw preview functions ***/ diff --git a/test/Conversions.t.sol b/test/Conversions.t.sol index f75011a..e884e7c 100644 --- a/test/Conversions.t.sol +++ b/test/Conversions.t.sol @@ -73,9 +73,9 @@ contract PSMConvertToAssetValueTests is PSMTestBase { } function test_convertToAssetValue() public { - _deposit(address(this), address(dai), 100e18); - _deposit(address(this), address(usdc), 100e6); - _deposit(address(this), address(sDai), 80e18); + _deposit(address(dai), address(this), 100e18); + _deposit(address(usdc), address(this), 100e6); + _deposit(address(sDai), address(this), 80e18); assertEq(psm.convertToAssetValue(1e18), 1e18); @@ -103,23 +103,23 @@ contract PSMConvertToSharesTests is PSMTestBase { function test_convertToShares_depositAndWithdrawUsdcAndSDai_noChange() public { _assertOneToOneConversion(); - _deposit(address(this), address(usdc), 100e6); + _deposit(address(usdc), address(this), 100e6); _assertOneToOneConversion(); - _deposit(address(this), address(sDai), 80e18); + _deposit(address(sDai), address(this), 80e18); _assertOneToOneConversion(); - _withdraw(address(this), address(usdc), 100e6); + _withdraw(address(usdc), address(this), 100e6); _assertOneToOneConversion(); - _withdraw(address(this), address(sDai), 80e18); + _withdraw(address(sDai), address(this), 80e18); _assertOneToOneConversion(); } function test_convertToShares_updateSDaiValue() public { // 200 shares minted at 1:1 ratio, $200 of value in pool - _deposit(address(this), address(usdc), 100e6); - _deposit(address(this), address(sDai), 80e18); + _deposit(address(usdc), address(this), 100e6); + _deposit(address(sDai), address(this), 80e18); _assertOneToOneConversion(); @@ -173,23 +173,23 @@ contract PSMConvertToSharesWithDaiTests is PSMTestBase { function test_convertToShares_depositAndWithdrawDaiAndSDai_noChange() public { _assertOneToOneConversionDai(); - _deposit(address(this), address(dai), 100e18); + _deposit(address(dai), address(this), 100e18); _assertOneToOneConversionDai(); - _deposit(address(this), address(sDai), 80e18); + _deposit(address(sDai), address(this), 80e18); _assertOneToOneConversionDai(); - _withdraw(address(this), address(dai), 100e18); + _withdraw(address(dai), address(this), 100e18); _assertOneToOneConversionDai(); - _withdraw(address(this), address(sDai), 80e18); + _withdraw(address(sDai), address(this), 80e18); _assertOneToOneConversionDai(); } function test_convertToShares_updateSDaiValue() public { // 200 shares minted at 1:1 ratio, $200 of value in pool - _deposit(address(this), address(dai), 100e18); - _deposit(address(this), address(sDai), 80e18); + _deposit(address(dai), address(this), 100e18); + _deposit(address(sDai), address(this), 80e18); _assertOneToOneConversionDai(); @@ -234,23 +234,23 @@ contract PSMConvertToSharesWithUsdcTests is PSMTestBase { function test_convertToShares_depositAndWithdrawUsdcAndSDai_noChange() public { _assertOneToOneConversionUsdc(); - _deposit(address(this), address(usdc), 100e6); + _deposit(address(usdc), address(this), 100e6); _assertOneToOneConversionUsdc(); - _deposit(address(this), address(sDai), 80e18); + _deposit(address(sDai), address(this), 80e18); _assertOneToOneConversionUsdc(); - _withdraw(address(this), address(usdc), 100e6); + _withdraw(address(usdc), address(this), 100e6); _assertOneToOneConversionUsdc(); - _withdraw(address(this), address(sDai), 80e18); + _withdraw(address(sDai), address(this), 80e18); _assertOneToOneConversionUsdc(); } function test_convertToShares_updateSDaiValue() public { // 200 shares minted at 1:1 ratio, $200 of value in pool - _deposit(address(this), address(usdc), 100e6); - _deposit(address(this), address(sDai), 80e18); + _deposit(address(usdc), address(this), 100e6); + _deposit(address(sDai), address(this), 80e18); _assertOneToOneConversionUsdc(); @@ -299,23 +299,23 @@ contract PSMConvertToSharesWithSDaiTests is PSMTestBase { function test_convertToShares_depositAndWithdrawUsdcAndSDai_noChange() public { _assertOneToOneConversion(); - _deposit(address(this), address(usdc), 100e6); + _deposit(address(usdc), address(this), 100e6); _assertStartingConversionSDai(); - _deposit(address(this), address(sDai), 80e18); + _deposit(address(sDai), address(this), 80e18); _assertStartingConversionSDai(); - _withdraw(address(this), address(usdc), 100e6); + _withdraw(address(usdc), address(this), 100e6); _assertStartingConversionSDai(); - _withdraw(address(this), address(sDai), 80e18); + _withdraw(address(sDai), address(this), 80e18); _assertStartingConversionSDai(); } function test_convertToShares_updateSDaiValue() public { // 200 shares minted at 1:1 ratio, $200 of value in pool - _deposit(address(this), address(usdc), 100e6); - _deposit(address(this), address(sDai), 80e18); + _deposit(address(usdc), address(this), 100e6); + _deposit(address(sDai), address(this), 80e18); _assertStartingConversionSDai(); diff --git a/test/Deposit.t.sol b/test/Deposit.t.sol index 47464bf..18d7bd2 100644 --- a/test/Deposit.t.sol +++ b/test/Deposit.t.sol @@ -9,15 +9,49 @@ import { PSMTestBase } from "test/PSMTestBase.sol"; contract PSMDepositTests is PSMTestBase { - address user1 = makeAddr("user1"); - address user2 = makeAddr("user2"); + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + address receiver1 = makeAddr("receiver1"); + address receiver2 = makeAddr("receiver2"); function test_deposit_notAsset0OrAsset1() public { vm.expectRevert("PSM/invalid-asset"); - psm.deposit(makeAddr("new-asset"), 100e6, 0); + psm.deposit(makeAddr("new-asset"), user1, 100e6, 0); } // TODO: Add balance/approve failure tests + // TODO: Add assertions for return values + // TODO: Add tests for new requires + + function test_deposit_firstDepositDai() public { + dai.mint(user1, 100e18); + + vm.startPrank(user1); + + dai.approve(address(psm), 100e18); + + assertEq(dai.allowance(user1, address(psm)), 100e18); + assertEq(dai.balanceOf(user1), 100e18); + assertEq(dai.balanceOf(address(psm)), 0); + + assertEq(psm.totalShares(), 0); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver1), 0); + + assertEq(psm.convertToShares(1e18), 1e18); + + psm.deposit(address(dai), receiver1, 100e18, 0); + + assertEq(dai.allowance(user1, address(psm)), 0); + assertEq(dai.balanceOf(user1), 0); + assertEq(dai.balanceOf(address(psm)), 100e18); + + assertEq(psm.totalShares(), 100e18); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver1), 100e18); + + assertEq(psm.convertToShares(1e18), 1e18); + } function test_deposit_firstDepositUsdc() public { usdc.mint(user1, 100e6); @@ -30,19 +64,21 @@ contract PSMDepositTests is PSMTestBase { assertEq(usdc.balanceOf(user1), 100e6); assertEq(usdc.balanceOf(address(psm)), 0); - assertEq(psm.totalShares(), 0); - assertEq(psm.shares(user1), 0); + assertEq(psm.totalShares(), 0); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver1), 0); assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(usdc), 100e6, 0); + psm.deposit(address(usdc), receiver1, 100e6, 0); assertEq(usdc.allowance(user1, address(psm)), 0); assertEq(usdc.balanceOf(user1), 0); assertEq(usdc.balanceOf(address(psm)), 100e6); - assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 100e18); + assertEq(psm.totalShares(), 100e18); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver1), 100e18); assertEq(psm.convertToShares(1e18), 1e18); } @@ -58,19 +94,21 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(user1), 100e18); assertEq(sDai.balanceOf(address(psm)), 0); - assertEq(psm.totalShares(), 0); - assertEq(psm.shares(user1), 0); + assertEq(psm.totalShares(), 0); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver1), 0); assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), 100e18, 0); + psm.deposit(address(sDai), receiver1, 100e18, 0); assertEq(sDai.allowance(user1, address(psm)), 0); assertEq(sDai.balanceOf(user1), 0); assertEq(sDai.balanceOf(address(psm)), 100e18); - assertEq(psm.totalShares(), 125e18); - assertEq(psm.shares(user1), 125e18); + assertEq(psm.totalShares(), 125e18); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver1), 125e18); assertEq(psm.convertToShares(1e18), 1e18); } @@ -82,7 +120,7 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), 100e6); - psm.deposit(address(usdc), 100e6, 0); + psm.deposit(address(usdc), receiver1, 100e6, 0); sDai.mint(user1, 100e18); sDai.approve(address(psm), 100e18); @@ -93,12 +131,13 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(user1), 100e18); assertEq(sDai.balanceOf(address(psm)), 0); - assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 100e18); + assertEq(psm.totalShares(), 100e18); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver1), 100e18); assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), 100e18, 0); + psm.deposit(address(sDai), receiver1, 100e18, 0); assertEq(usdc.balanceOf(address(psm)), 100e6); @@ -106,15 +145,19 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(user1), 0); assertEq(sDai.balanceOf(address(psm)), 100e18); - assertEq(psm.totalShares(), 225e18); - assertEq(psm.shares(user1), 225e18); + assertEq(psm.totalShares(), 225e18); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver1), 225e18); assertEq(psm.convertToShares(1e18), 1e18); } - function testFuzz_deposit_usdcThenSDai(uint256 usdcAmount, uint256 sDaiAmount) public { - usdcAmount = _bound(usdcAmount, 0, USDC_TOKEN_MAX); - sDaiAmount = _bound(sDaiAmount, 0, SDAI_TOKEN_MAX); + function testFuzz_deposit_usdcThenSDai(address receiver, uint256 usdcAmount, uint256 sDaiAmount) public { + vm.assume(receiver != address(0)); + + // Zero amounts revert + usdcAmount = _bound(usdcAmount, 1, USDC_TOKEN_MAX); + sDaiAmount = _bound(sDaiAmount, 1, SDAI_TOKEN_MAX); usdc.mint(user1, usdcAmount); @@ -122,7 +165,7 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), usdcAmount); - psm.deposit(address(usdc), usdcAmount, 0); + psm.deposit(address(usdc), receiver, usdcAmount, 0); sDai.mint(user1, sDaiAmount); sDai.approve(address(psm), sDaiAmount); @@ -133,12 +176,13 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(user1), sDaiAmount); assertEq(sDai.balanceOf(address(psm)), 0); - assertEq(psm.totalShares(), usdcAmount * 1e12); - assertEq(psm.shares(user1), usdcAmount * 1e12); + assertEq(psm.totalShares(), usdcAmount * 1e12); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver), usdcAmount * 1e12); assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), sDaiAmount, 0); + psm.deposit(address(sDai), receiver, sDaiAmount, 0); assertEq(usdc.balanceOf(address(psm)), usdcAmount); @@ -146,8 +190,9 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(user1), 0); assertEq(sDai.balanceOf(address(psm)), sDaiAmount); - assertEq(psm.totalShares(), usdcAmount * 1e12 + sDaiAmount * 125/100); - assertEq(psm.shares(user1), usdcAmount * 1e12 + sDaiAmount * 125/100); + assertEq(psm.totalShares(), usdcAmount * 1e12 + sDaiAmount * 125/100); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver), usdcAmount * 1e12 + sDaiAmount * 125/100); assertEq(psm.convertToShares(1e18), 1e18); } @@ -159,12 +204,12 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), 100e6); - psm.deposit(address(usdc), 100e6, 0); + psm.deposit(address(usdc), receiver1, 100e6, 0); sDai.mint(user1, 100e18); sDai.approve(address(psm), 100e18); - psm.deposit(address(sDai), 100e18, 0); + psm.deposit(address(sDai), receiver1, 100e18, 0); vm.stopPrank(); @@ -174,12 +219,13 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(user1), 0); assertEq(sDai.balanceOf(address(psm)), 100e18); - assertEq(psm.totalShares(), 225e18); - assertEq(psm.shares(user1), 225e18); + assertEq(psm.totalShares(), 225e18); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver1), 225e18); assertEq(psm.convertToShares(1e18), 1e18); - assertEq(psm.convertToAssetValue(psm.shares(user1)), 225e18); + assertEq(psm.convertToAssetValue(psm.shares(receiver1)), 225e18); rateProvider.__setConversionRate(1.5e27); @@ -199,14 +245,12 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(user2), 100e18); assertEq(sDai.balanceOf(address(psm)), 100e18); - assertEq(psm.convertToAssetValue(psm.shares(user1)), 250e18); - assertEq(psm.convertToAssetValue(psm.shares(user2)), 0); + assertEq(psm.convertToAssetValue(psm.shares(receiver1)), 250e18); + assertEq(psm.convertToAssetValue(psm.shares(receiver2)), 0); assertEq(psm.getPsmTotalValue(), 250e18); - psm.deposit(address(sDai), 100e18, 0); - - assertEq(psm.shares(user2), 135e18); + psm.deposit(address(sDai), receiver2, 100e18, 0); assertEq(sDai.allowance(user2, address(psm)), 0); assertEq(sDai.balanceOf(user2), 0); @@ -217,13 +261,15 @@ contract PSMDepositTests is PSMTestBase { assertEq(expectedShares, 135e18); - assertEq(psm.totalShares(), 360e18); - assertEq(psm.shares(user1), 225e18); - assertEq(psm.shares(user2), 135e18); + assertEq(psm.totalShares(), 360e18); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(user2), 0); + assertEq(psm.shares(receiver1), 225e18); + assertEq(psm.shares(receiver2), 135e18); - // User 1 earned $25 on 225, User 2 has earned nothing - assertEq(psm.convertToAssetValue(psm.shares(user1)), 250e18); - assertEq(psm.convertToAssetValue(psm.shares(user2)), 150e18); + // Receiver 1 earned $25 on 225, Receiver 2 has earned nothing + assertEq(psm.convertToAssetValue(psm.shares(receiver1)), 250e18); + assertEq(psm.convertToAssetValue(psm.shares(receiver2)), 150e18); assertEq(psm.getPsmTotalValue(), 400e18); } diff --git a/test/Events.t.sol b/test/Events.t.sol index ac9d768..5b1e815 100644 --- a/test/Events.t.sol +++ b/test/Events.t.sol @@ -20,6 +20,7 @@ contract PSMEventTests is PSMTestBase { event Deposit( address indexed asset, address indexed user, + address indexed receiver, uint256 assetsDeposited, uint256 sharesMinted, uint16 referralCode @@ -28,6 +29,7 @@ contract PSMEventTests is PSMTestBase { event Withdraw( address indexed asset, address indexed user, + address indexed receiver, uint256 assetsWithdrawn, uint256 sharesBurned, uint16 referralCode @@ -43,42 +45,42 @@ contract PSMEventTests is PSMTestBase { dai.approve(address(psm), 100e18); vm.expectEmit(); - emit Deposit(address(dai), sender, 100e18, 100e18, 1); - psm.deposit(address(dai), 100e18, 1); + emit Deposit(address(dai), sender, receiver, 100e18, 100e18, 1); + psm.deposit(address(dai), receiver, 100e18, 1); usdc.mint(sender, 100e6); usdc.approve(address(psm), 100e6); vm.expectEmit(); - emit Deposit(address(usdc), sender, 100e6, 100e18, 2); // Different code - psm.deposit(address(usdc), 100e6, 2); + emit Deposit(address(usdc), sender, receiver, 100e6, 100e18, 2); // Different code + psm.deposit(address(usdc), receiver, 100e6, 2); sDai.mint(sender, 100e18); sDai.approve(address(psm), 100e18); vm.expectEmit(); - emit Deposit(address(sDai), sender, 100e18, 125e18, 3); // Different code - psm.deposit(address(sDai), 100e18, 3); + emit Deposit(address(sDai), sender, receiver, 100e18, 125e18, 3); // Different code + psm.deposit(address(sDai), receiver, 100e18, 3); } function test_withdraw_events() public { - _deposit(sender, address(dai), 100e18); - _deposit(sender, address(usdc), 100e6); - _deposit(sender, address(sDai), 100e18); + _deposit(address(dai), sender, 100e18); + _deposit(address(usdc), sender, 100e6); + _deposit(address(sDai), sender, 100e18); vm.startPrank(sender); vm.expectEmit(); - emit Withdraw(address(dai), sender, 100e18, 100e18, 1); - psm.withdraw(address(dai), 100e18, 1); + emit Withdraw(address(dai), sender, receiver, 100e18, 100e18, 1); + psm.withdraw(address(dai), receiver, 100e18, 1); vm.expectEmit(); - emit Withdraw(address(usdc), sender, 100e6, 100e18, 2); - psm.withdraw(address(usdc), 100e6, 2); + emit Withdraw(address(usdc), sender, receiver, 100e6, 100e18, 2); + psm.withdraw(address(usdc), receiver, 100e6, 2); vm.expectEmit(); - emit Withdraw(address(sDai), sender, 100e18, 125e18, 3); - psm.withdraw(address(sDai), 100e18, 3); + emit Withdraw(address(sDai), sender, receiver, 100e18, 125e18, 3); + psm.withdraw(address(sDai), receiver, 100e18, 3); } function test_swap_events() public { diff --git a/test/InflationAttack.t.sol b/test/InflationAttack.t.sol index 6e4729c..5d4ea9f 100644 --- a/test/InflationAttack.t.sol +++ b/test/InflationAttack.t.sol @@ -21,7 +21,7 @@ contract InflationAttackTests is PSMTestBase { // Step 1: Front runner deposits 1 sDAI to get 1 share // Have to use sDai because 1 USDC mints 1e12 shares - _deposit(frontRunner, address(sDai), 1); + _deposit(address(sDai), frontRunner, 1); assertEq(psm.shares(frontRunner), 1); @@ -38,7 +38,7 @@ contract InflationAttackTests is PSMTestBase { // Step 3: First depositor deposits 20 million USDC, only gets one share because rounding // error gives them 1 instead of 2 shares, worth 15m USDC - _deposit(firstDepositor, address(usdc), 20_000_000e6); + _deposit(address(usdc), firstDepositor, 20_000_000e6); assertEq(psm.shares(firstDepositor), 1); @@ -47,8 +47,8 @@ contract InflationAttackTests is PSMTestBase { // Step 4: Both users withdraw the max amount of funds they can - _withdraw(firstDepositor, address(usdc), type(uint256).max); - _withdraw(frontRunner, address(usdc), type(uint256).max); + _withdraw(address(usdc), firstDepositor, type(uint256).max); + _withdraw(address(usdc), frontRunner, type(uint256).max); assertEq(usdc.balanceOf(address(psm)), 0); @@ -64,12 +64,12 @@ contract InflationAttackTests is PSMTestBase { address frontRunner = makeAddr("frontRunner"); address deployer = address(this); // TODO: Update to use non-deployer receiver - _deposit(address(this), address(sDai), 800); /// 1000 shares + _deposit(address(sDai), address(this), 800); /// 1000 shares // Step 1: Front runner deposits 801 sDAI to get 1 share // User tries to do the same attack, depositing one sDAI for 1 share - _deposit(frontRunner, address(sDai), 1); + _deposit(address(sDai), frontRunner, 1); assertEq(psm.shares(frontRunner), 1); @@ -86,7 +86,7 @@ contract InflationAttackTests is PSMTestBase { // Step 3: First depositor deposits 20 million USDC, only gets one share because rounding // error gives them 1 instead of 2 shares, worth 15m USDC - _deposit(firstDepositor, address(usdc), 20_000_000e6); + _deposit(address(usdc), firstDepositor, 20_000_000e6); assertEq(psm.shares(firstDepositor), 2001); @@ -95,9 +95,9 @@ contract InflationAttackTests is PSMTestBase { // Step 4: Both users withdraw the max amount of funds they can - _withdraw(firstDepositor, address(usdc), type(uint256).max); - _withdraw(frontRunner, address(usdc), type(uint256).max); - _withdraw(deployer, address(usdc), type(uint256).max); + _withdraw(address(usdc), firstDepositor, type(uint256).max); + _withdraw(address(usdc), frontRunner, type(uint256).max); + _withdraw(address(usdc), deployer, type(uint256).max); // Front runner loses 9.99m USDC, first depositor loses 4k USDC assertEq(usdc.balanceOf(firstDepositor), 19_996_668.887408e6); diff --git a/test/PSMTestBase.sol b/test/PSMTestBase.sol index dc02d46..51480b3 100644 --- a/test/PSMTestBase.sol +++ b/test/PSMTestBase.sol @@ -54,17 +54,25 @@ contract PSMTestBase is Test { + dai.balanceOf(address(psm)); } - function _deposit(address user, address asset, uint256 amount) internal { + function _deposit(address asset, address user, uint256 amount) internal { + _deposit(asset, user, user, amount); + } + + function _deposit(address asset, address user, address receiver, uint256 amount) internal { vm.startPrank(user); MockERC20(asset).mint(user, amount); MockERC20(asset).approve(address(psm), amount); - psm.deposit(asset, amount, 0); + psm.deposit(asset, receiver, amount, 0); vm.stopPrank(); } - function _withdraw(address user, address asset, uint256 amount) internal { + function _withdraw(address asset, address user, uint256 amount) internal { + _withdraw(asset, user, user, amount); + } + + function _withdraw(address asset, address user, address receiver, uint256 amount) internal { vm.prank(user); - psm.withdraw(asset, amount, 0); + psm.withdraw(asset, receiver, amount, 0); vm.stopPrank(); } diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index b460f5d..c989aaa 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -11,20 +11,24 @@ import { PSMTestBase } from "test/PSMTestBase.sol"; contract PSMWithdrawTests is PSMTestBase { - address user1 = makeAddr("user1"); - address user2 = makeAddr("user2"); + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + address receiver1 = makeAddr("receiver1"); + address receiver2 = makeAddr("receiver2"); function test_withdraw_notAsset0OrAsset1() public { vm.expectRevert("PSM/invalid-asset"); - psm.withdraw(makeAddr("new-asset"), 100e6, 0); + psm.withdraw(makeAddr("new-asset"), receiver1, 100e6, 0); } // TODO: Add balance/approve failure tests + // TODO: Add tests for new requires function test_withdraw_onlyUsdcInPsm() public { - _deposit(user1, address(usdc), 100e6); + _deposit(address(usdc), user1, 100e6); assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(receiver1), 0); assertEq(usdc.balanceOf(address(psm)), 100e6); assertEq(psm.totalShares(), 100e18); @@ -33,11 +37,12 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - uint256 amount = psm.withdraw(address(usdc), 100e6, 0); + uint256 amount = psm.withdraw(address(usdc), receiver1, 100e6, 0); assertEq(amount, 100e6); - assertEq(usdc.balanceOf(user1), 100e6); + assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(receiver1), 100e6); assertEq(usdc.balanceOf(address(psm)), 0); assertEq(psm.totalShares(), 0); @@ -47,9 +52,10 @@ contract PSMWithdrawTests is PSMTestBase { } function test_withdraw_onlySDaiInPsm() public { - _deposit(user1, address(sDai), 80e18); + _deposit(address(sDai), user1, 80e18); assertEq(sDai.balanceOf(user1), 0); + assertEq(sDai.balanceOf(receiver1), 0); assertEq(sDai.balanceOf(address(psm)), 80e18); assertEq(psm.totalShares(), 100e18); @@ -58,11 +64,12 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - uint256 amount = psm.withdraw(address(sDai), 80e18, 0); + uint256 amount = psm.withdraw(address(sDai), receiver1, 80e18, 0); assertEq(amount, 80e18); - assertEq(sDai.balanceOf(user1), 80e18); + assertEq(sDai.balanceOf(user1), 0); + assertEq(sDai.balanceOf(receiver1), 80e18); assertEq(sDai.balanceOf(address(psm)), 0); assertEq(psm.totalShares(), 0); @@ -72,13 +79,15 @@ contract PSMWithdrawTests is PSMTestBase { } function test_withdraw_usdcThenSDai() public { - _deposit(user1, address(usdc), 100e6); - _deposit(user1, address(sDai), 100e18); + _deposit(address(usdc), user1, 100e6); + _deposit(address(sDai), user1, 100e18); assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(receiver1), 0); assertEq(usdc.balanceOf(address(psm)), 100e6); assertEq(sDai.balanceOf(user1), 0); + assertEq(sDai.balanceOf(receiver1), 0); assertEq(sDai.balanceOf(address(psm)), 100e18); assertEq(psm.totalShares(), 225e18); @@ -87,14 +96,16 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - uint256 amount = psm.withdraw(address(usdc), 100e6, 0); + uint256 amount = psm.withdraw(address(usdc), receiver1, 100e6, 0); assertEq(amount, 100e6); - assertEq(usdc.balanceOf(user1), 100e6); + assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(receiver1), 100e6); assertEq(usdc.balanceOf(address(psm)), 0); assertEq(sDai.balanceOf(user1), 0); + assertEq(sDai.balanceOf(receiver1), 0); assertEq(sDai.balanceOf(address(psm)), 100e18); assertEq(psm.totalShares(), 125e18); @@ -103,14 +114,16 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - amount = psm.withdraw(address(sDai), 100e18, 0); + amount = psm.withdraw(address(sDai), receiver1, 100e18, 0); assertEq(amount, 100e18); - assertEq(usdc.balanceOf(user1), 100e6); + assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(receiver1), 100e6); assertEq(usdc.balanceOf(address(psm)), 0); - assertEq(sDai.balanceOf(user1), 100e18); + assertEq(sDai.balanceOf(user1), 0); + assertEq(sDai.balanceOf(receiver1), 100e18); assertEq(sDai.balanceOf(address(psm)), 0); assertEq(psm.totalShares(), 0); @@ -120,10 +133,11 @@ contract PSMWithdrawTests is PSMTestBase { } function test_withdraw_amountHigherThanBalanceOfAsset() public { - _deposit(user1, address(usdc), 100e6); - _deposit(user1, address(sDai), 100e18); + _deposit(address(usdc), user1, 100e6); + _deposit(address(sDai), user1, 100e18); assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(receiver1), 0); assertEq(usdc.balanceOf(address(psm)), 100e6); assertEq(psm.totalShares(), 225e18); @@ -132,11 +146,12 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - uint256 amount = psm.withdraw(address(usdc), 125e6, 0); + uint256 amount = psm.withdraw(address(usdc), receiver1, 125e6, 0); assertEq(amount, 100e6); - assertEq(usdc.balanceOf(user1), 100e6); + assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(receiver1), 100e6); assertEq(usdc.balanceOf(address(psm)), 0); assertEq(psm.totalShares(), 125e18); // Only burns $100 of shares @@ -144,11 +159,12 @@ contract PSMWithdrawTests is PSMTestBase { } function test_withdraw_amountHigherThanUserShares() public { - _deposit(user1, address(usdc), 100e6); - _deposit(user1, address(sDai), 100e18); - _deposit(user2, address(usdc), 200e6); + _deposit(address(usdc), user1, 100e6); + _deposit(address(sDai), user1, 100e18); + _deposit(address(usdc), user2, 200e6); assertEq(usdc.balanceOf(user2), 0); + assertEq(usdc.balanceOf(receiver2), 0); assertEq(usdc.balanceOf(address(psm)), 300e6); assertEq(psm.totalShares(), 425e18); @@ -157,11 +173,12 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user2); - uint256 amount = psm.withdraw(address(usdc), 225e6, 0); + uint256 amount = psm.withdraw(address(usdc), receiver2, 225e6, 0); assertEq(amount, 200e6); - assertEq(usdc.balanceOf(user2), 200e6); // Gets highest amount possible + assertEq(usdc.balanceOf(user2), 0); + assertEq(usdc.balanceOf(receiver2), 200e6); // Gets highest amount possible assertEq(usdc.balanceOf(address(psm)), 100e6); assertEq(psm.totalShares(), 225e18); @@ -178,24 +195,25 @@ contract PSMWithdrawTests is PSMTestBase { ) public { - // NOTE: Not covering zero cases, 1e-2 at 1e6 used as min for now so exact values can - // be asserted - depositAmount1 = bound(depositAmount1, 0, USDC_TOKEN_MAX); - depositAmount2 = bound(depositAmount2, 0, USDC_TOKEN_MAX); - depositAmount3 = bound(depositAmount3, 0, SDAI_TOKEN_MAX); + // Zero amounts revert + depositAmount1 = bound(depositAmount1, 1, USDC_TOKEN_MAX); + depositAmount2 = bound(depositAmount2, 1, USDC_TOKEN_MAX); + depositAmount3 = bound(depositAmount3, 1, SDAI_TOKEN_MAX); - withdrawAmount1 = bound(withdrawAmount1, 0, USDC_TOKEN_MAX); - withdrawAmount2 = bound(withdrawAmount2, 0, USDC_TOKEN_MAX); - withdrawAmount3 = bound(withdrawAmount3, 0, SDAI_TOKEN_MAX); + // Zero amounts revert + withdrawAmount1 = bound(withdrawAmount1, 1, USDC_TOKEN_MAX); + withdrawAmount2 = bound(withdrawAmount2, 1, USDC_TOKEN_MAX); + withdrawAmount3 = bound(withdrawAmount3, 1, SDAI_TOKEN_MAX); - _deposit(user1, address(usdc), depositAmount1); - _deposit(user2, address(usdc), depositAmount2); - _deposit(user2, address(sDai), depositAmount3); + _deposit(address(usdc), user1, depositAmount1); + _deposit(address(usdc), user2, depositAmount2); + _deposit(address(sDai), user2, depositAmount3); uint256 totalUsdc = depositAmount1 + depositAmount2; uint256 totalValue = totalUsdc * 1e12 + depositAmount3 * 125/100; assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(receiver1), 0); assertEq(usdc.balanceOf(address(psm)), totalUsdc); assertEq(psm.shares(user1), depositAmount1 * 1e12); @@ -205,19 +223,21 @@ contract PSMWithdrawTests is PSMTestBase { = _getExpectedWithdrawnAmount(usdc, user1, withdrawAmount1); vm.prank(user1); - uint256 amount = psm.withdraw(address(usdc), withdrawAmount1, 0); + uint256 amount = psm.withdraw(address(usdc), receiver1, withdrawAmount1, 0); assertEq(amount, expectedWithdrawnAmount1); _checkPsmInvariant(); assertEq( - usdc.balanceOf(user1) * 1e12 + psm.getPsmTotalValue(), + usdc.balanceOf(receiver1) * 1e12 + psm.getPsmTotalValue(), totalValue ); - assertEq(usdc.balanceOf(user1), expectedWithdrawnAmount1); + assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(receiver1), expectedWithdrawnAmount1); assertEq(usdc.balanceOf(user2), 0); + assertEq(usdc.balanceOf(receiver2), 0); assertEq(usdc.balanceOf(address(psm)), totalUsdc - expectedWithdrawnAmount1); assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); @@ -228,22 +248,25 @@ contract PSMWithdrawTests is PSMTestBase { = _getExpectedWithdrawnAmount(usdc, user2, withdrawAmount2); vm.prank(user2); - amount = psm.withdraw(address(usdc), withdrawAmount2, 0); + amount = psm.withdraw(address(usdc), receiver2, withdrawAmount2, 0); assertEq(amount, expectedWithdrawnAmount2); _checkPsmInvariant(); assertEq( - (usdc.balanceOf(user1) + usdc.balanceOf(user2)) * 1e12 + psm.getPsmTotalValue(), + (usdc.balanceOf(receiver1) + usdc.balanceOf(receiver2)) * 1e12 + psm.getPsmTotalValue(), totalValue ); - assertEq(usdc.balanceOf(user1), expectedWithdrawnAmount1); - assertEq(usdc.balanceOf(user2), expectedWithdrawnAmount2); + assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(receiver1), expectedWithdrawnAmount1); + assertEq(usdc.balanceOf(user2), 0); + assertEq(usdc.balanceOf(receiver2), expectedWithdrawnAmount2); assertEq(usdc.balanceOf(address(psm)), totalUsdc - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2)); assertEq(sDai.balanceOf(user2), 0); + assertEq(sDai.balanceOf(receiver2), 0); assertEq(sDai.balanceOf(address(psm)), depositAmount3); assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); @@ -259,25 +282,28 @@ contract PSMWithdrawTests is PSMTestBase { = _getExpectedWithdrawnAmount(sDai, user2, withdrawAmount3); vm.prank(user2); - amount = psm.withdraw(address(sDai), withdrawAmount3, 0); + amount = psm.withdraw(address(sDai), receiver2, withdrawAmount3, 0); assertApproxEqAbs(amount, expectedWithdrawnAmount3, 1); _checkPsmInvariant(); assertApproxEqAbs( - (usdc.balanceOf(user1) + usdc.balanceOf(user2)) * 1e12 - + (sDai.balanceOf(user2) * rateProvider.getConversionRate() / 1e27) + (usdc.balanceOf(receiver1) + usdc.balanceOf(receiver2)) * 1e12 + + (sDai.balanceOf(receiver2) * rateProvider.getConversionRate() / 1e27) + psm.getPsmTotalValue(), totalValue, 1 ); - assertEq(usdc.balanceOf(user1), expectedWithdrawnAmount1); - assertEq(usdc.balanceOf(user2), expectedWithdrawnAmount2); + assertEq(usdc.balanceOf(user1), 0); + assertEq(usdc.balanceOf(receiver1), expectedWithdrawnAmount1); + assertEq(usdc.balanceOf(user2), 0); + assertEq(usdc.balanceOf(receiver2), expectedWithdrawnAmount2); assertEq(usdc.balanceOf(address(psm)), totalUsdc - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2)); - assertApproxEqAbs(sDai.balanceOf(user2), expectedWithdrawnAmount3, 1); + assertApproxEqAbs(sDai.balanceOf(user2), 0, 0); + assertApproxEqAbs(sDai.balanceOf(receiver2), expectedWithdrawnAmount3, 1); assertApproxEqAbs(sDai.balanceOf(address(psm)), depositAmount3 - expectedWithdrawnAmount3, 1); assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); @@ -340,8 +366,8 @@ contract PSMWithdrawTests is PSMTestBase { } // function test_withdraw_changeConversionRate_smallBalances_nonRoundingCode() public { - // _deposit(user1, address(usdc), 100e6); - // _deposit(user2, address(sDai), 100e18); + // _deposit(address(usdc), user1, 100e6); + // _deposit(address(sDai), user2, 100e18); // assertEq(psm.totalShares(), 225e18); // assertEq(psm.shares(user1), 100e18); @@ -443,8 +469,8 @@ contract PSMWithdrawTests is PSMTestBase { // } // function test_withdraw_changeConversionRate_bigBalances_roundingCode() public { - // _deposit(user1, address(usdc), 100_000_000e6); - // _deposit(user2, address(sDai), 100_000_000e18); + // _deposit(address(usdc), user1, 100_000_000e6); + // _deposit(address(sDai), user2, 100_000_000e18); // assertEq(psm.totalShares(), 225_000_000e18); // assertEq(psm.shares(user1), 100_000_000e18); @@ -552,8 +578,8 @@ contract PSMWithdrawTests is PSMTestBase { // } // function test_withdraw_changeConversionRate_bigBalances_nonRoundingCode() public { - // _deposit(user1, address(usdc), 100_000_000e6); - // _deposit(user2, address(sDai), 100_000_000e18); + // _deposit(address(usdc), user1, 100_000_000e6); + // _deposit(address(sDai), user2, 100_000_000e18); // assertEq(psm.totalShares(), 225_000_000e18); // assertEq(psm.shares(user1), 100_000_000e18); @@ -661,8 +687,8 @@ contract PSMWithdrawTests is PSMTestBase { // } // function test_withdraw_2() public { - // _deposit(user1, address(usdc), 100e6); - // _deposit(user2, address(sDai), 100e18); + // _deposit(address(usdc), user1, 100e6); + // _deposit(address(sDai), user2, 100e18); // assertEq(psm.totalShares(), 225e18); // assertEq(psm.shares(user1), 100e18); From c41b010c83d35762d045b47567bddad8c67682d6 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 30 May 2024 15:56:30 -0400 Subject: [PATCH 39/96] feat: add deposit failure mode tests --- test/Deposit.t.sol | 62 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/test/Deposit.t.sol b/test/Deposit.t.sol index 18d7bd2..0e09aa6 100644 --- a/test/Deposit.t.sol +++ b/test/Deposit.t.sol @@ -14,14 +14,50 @@ contract PSMDepositTests is PSMTestBase { address receiver1 = makeAddr("receiver1"); address receiver2 = makeAddr("receiver2"); + function test_deposit_zeroReceiver() public { + vm.expectRevert("PSM/invalid-receiver"); + psm.deposit(address(usdc), address(0), 100e6, 0); + } + + function test_deposit_zeroAmount() public { + vm.expectRevert("PSM/invalid-amount"); + psm.deposit(address(usdc), user1, 0, 0); + } + function test_deposit_notAsset0OrAsset1() public { vm.expectRevert("PSM/invalid-asset"); psm.deposit(makeAddr("new-asset"), user1, 100e6, 0); } - // TODO: Add balance/approve failure tests - // TODO: Add assertions for return values - // TODO: Add tests for new requires + function test_deposit_insufficientApproveBoundary() public { + dai.mint(user1, 100e18); + + vm.startPrank(user1); + + dai.approve(address(psm), 100e18 - 1); + + vm.expectRevert("SafeERC20/transfer-from-failed"); + psm.deposit(address(dai), user1, 100e18, 0); + + dai.approve(address(psm), 100e18); + + psm.deposit(address(dai), user1, 100e18, 0); + } + + function test_deposit_insufficientBalanceBoundary() public { + dai.mint(user1, 100e18 - 1); + + vm.startPrank(user1); + + dai.approve(address(psm), 100e18); + + vm.expectRevert("SafeERC20/transfer-from-failed"); + psm.deposit(address(dai), user1, 100e18, 0); + + dai.mint(user1, 1); + + psm.deposit(address(dai), user1, 100e18, 0); + } function test_deposit_firstDepositDai() public { dai.mint(user1, 100e18); @@ -152,9 +188,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); } - function testFuzz_deposit_usdcThenSDai(address receiver, uint256 usdcAmount, uint256 sDaiAmount) public { - vm.assume(receiver != address(0)); - + function testFuzz_deposit_usdcThenSDai(uint256 usdcAmount, uint256 sDaiAmount) public { // Zero amounts revert usdcAmount = _bound(usdcAmount, 1, USDC_TOKEN_MAX); sDaiAmount = _bound(sDaiAmount, 1, SDAI_TOKEN_MAX); @@ -165,7 +199,7 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), usdcAmount); - psm.deposit(address(usdc), receiver, usdcAmount, 0); + psm.deposit(address(usdc), receiver1, usdcAmount, 0); sDai.mint(user1, sDaiAmount); sDai.approve(address(psm), sDaiAmount); @@ -176,13 +210,13 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(user1), sDaiAmount); assertEq(sDai.balanceOf(address(psm)), 0); - assertEq(psm.totalShares(), usdcAmount * 1e12); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver), usdcAmount * 1e12); + assertEq(psm.totalShares(), usdcAmount * 1e12); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver1), usdcAmount * 1e12); assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), receiver, sDaiAmount, 0); + psm.deposit(address(sDai), receiver1, sDaiAmount, 0); assertEq(usdc.balanceOf(address(psm)), usdcAmount); @@ -190,9 +224,9 @@ contract PSMDepositTests is PSMTestBase { assertEq(sDai.balanceOf(user1), 0); assertEq(sDai.balanceOf(address(psm)), sDaiAmount); - assertEq(psm.totalShares(), usdcAmount * 1e12 + sDaiAmount * 125/100); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver), usdcAmount * 1e12 + sDaiAmount * 125/100); + assertEq(psm.totalShares(), usdcAmount * 1e12 + sDaiAmount * 125/100); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(receiver1), usdcAmount * 1e12 + sDaiAmount * 125/100); assertEq(psm.convertToShares(1e18), 1e18); } From 847a1db1d319d4cd5e2a4be33fa1c2483ed5c857 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 30 May 2024 15:59:58 -0400 Subject: [PATCH 40/96] feat: update to add assertions for return in deposit --- test/Deposit.t.sol | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/test/Deposit.t.sol b/test/Deposit.t.sol index 0e09aa6..4d30980 100644 --- a/test/Deposit.t.sol +++ b/test/Deposit.t.sol @@ -76,7 +76,9 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(dai), receiver1, 100e18, 0); + uint256 newShares = psm.deposit(address(dai), receiver1, 100e18, 0); + + assertEq(newShares, 100e18); assertEq(dai.allowance(user1, address(psm)), 0); assertEq(dai.balanceOf(user1), 0); @@ -106,7 +108,9 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(usdc), receiver1, 100e6, 0); + uint256 newShares = psm.deposit(address(usdc), receiver1, 100e6, 0); + + assertEq(newShares, 100e18); assertEq(usdc.allowance(user1, address(psm)), 0); assertEq(usdc.balanceOf(user1), 0); @@ -136,7 +140,9 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), receiver1, 100e18, 0); + uint256 newShares = psm.deposit(address(sDai), receiver1, 100e18, 0); + + assertEq(newShares, 125e18); assertEq(sDai.allowance(user1, address(psm)), 0); assertEq(sDai.balanceOf(user1), 0); @@ -156,7 +162,9 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), 100e6); - psm.deposit(address(usdc), receiver1, 100e6, 0); + uint256 newShares = psm.deposit(address(usdc), receiver1, 100e6, 0); + + assertEq(newShares, 100e18); sDai.mint(user1, 100e18); sDai.approve(address(psm), 100e18); @@ -173,7 +181,9 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), receiver1, 100e18, 0); + newShares = psm.deposit(address(sDai), receiver1, 100e18, 0); + + assertEq(newShares, 125e18); assertEq(usdc.balanceOf(address(psm)), 100e6); @@ -199,7 +209,9 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), usdcAmount); - psm.deposit(address(usdc), receiver1, usdcAmount, 0); + uint256 newShares = psm.deposit(address(usdc), receiver1, usdcAmount, 0); + + assertEq(newShares, usdcAmount * 1e12); sDai.mint(user1, sDaiAmount); sDai.approve(address(psm), sDaiAmount); @@ -216,7 +228,9 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), receiver1, sDaiAmount, 0); + newShares = psm.deposit(address(sDai), receiver1, sDaiAmount, 0); + + assertEq(newShares, sDaiAmount * 125/100); assertEq(usdc.balanceOf(address(psm)), usdcAmount); @@ -238,12 +252,16 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), 100e6); - psm.deposit(address(usdc), receiver1, 100e6, 0); + uint256 newShares = psm.deposit(address(usdc), receiver1, 100e6, 0); + + assertEq(newShares, 100e18); sDai.mint(user1, 100e18); sDai.approve(address(psm), 100e18); - psm.deposit(address(sDai), receiver1, 100e18, 0); + newShares = psm.deposit(address(sDai), receiver1, 100e18, 0); + + assertEq(newShares, 125e18); vm.stopPrank(); @@ -284,7 +302,9 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.getPsmTotalValue(), 250e18); - psm.deposit(address(sDai), receiver2, 100e18, 0); + newShares = psm.deposit(address(sDai), receiver2, 100e18, 0); + + assertEq(newShares, 135e18); assertEq(sDai.allowance(user2, address(psm)), 0); assertEq(sDai.balanceOf(user2), 0); From ecbf99b843cd48a4a300456d9de32a768e43d649 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 30 May 2024 16:03:29 -0400 Subject: [PATCH 41/96] feat: add withdraw failure tests --- test/Withdraw.t.sol | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index c989aaa..b47f306 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -16,13 +16,51 @@ contract PSMWithdrawTests is PSMTestBase { address receiver1 = makeAddr("receiver1"); address receiver2 = makeAddr("receiver2"); + function test_withdraw_zeroReceiver() public { + _deposit(address(usdc), user1, 100e6); + + vm.expectRevert("PSM/invalid-receiver"); + psm.withdraw(address(usdc), address(0), 100e6, 0); + } + + function test_withdraw_zeroAmount() public { + _deposit(address(usdc), user1, 100e6); + + vm.expectRevert("PSM/invalid-amount"); + psm.withdraw(address(usdc), receiver1, 0, 0); + } + function test_withdraw_notAsset0OrAsset1() public { vm.expectRevert("PSM/invalid-asset"); psm.withdraw(makeAddr("new-asset"), receiver1, 100e6, 0); } - // TODO: Add balance/approve failure tests - // TODO: Add tests for new requires + function test_withdraw_onlyDaiInPsm() public { + _deposit(address(dai), user1, 100e18); + + assertEq(dai.balanceOf(user1), 0); + assertEq(dai.balanceOf(receiver1), 0); + assertEq(dai.balanceOf(address(psm)), 100e18); + + assertEq(psm.totalShares(), 100e18); + assertEq(psm.shares(user1), 100e18); + + assertEq(psm.convertToShares(1e18), 1e18); + + vm.prank(user1); + uint256 amount = psm.withdraw(address(dai), receiver1, 100e18, 0); + + assertEq(amount, 100e18); + + assertEq(dai.balanceOf(user1), 0); + assertEq(dai.balanceOf(receiver1), 100e18); + assertEq(dai.balanceOf(address(psm)), 0); + + assertEq(psm.totalShares(), 0); + assertEq(psm.shares(user1), 0); + + assertEq(psm.convertToShares(1e18), 1e18); + } function test_withdraw_onlyUsdcInPsm() public { _deposit(address(usdc), user1, 100e6); From 44a5d52162acb0acfb749c4c6d0e93b33d300828 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 3 Jun 2024 09:06:46 -0400 Subject: [PATCH 42/96] feat: update to address comments outside sharesToBurn --- src/PSM.sol | 22 +++++++++++----------- test/Withdraw.t.sol | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index d9bd589..72fd270 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -108,11 +108,11 @@ contract PSM { /*** Deposit/withdraw preview functions ***/ /**********************************************************************************************/ - function previewDeposit(address asset, uint256 assets) public view returns (uint256) { + function previewDeposit(address asset, uint256 assetsToDeposit) public view returns (uint256) { require(asset == address(asset0) || asset == address(asset1), "PSM/invalid-asset"); // Convert amount to 1e18 precision denominated in value of asset0 then convert to shares. - return convertToShares(_getAssetValue(asset, assets)); + return convertToShares(_getAssetValue(asset, assetsToDeposit)); } function previewWithdraw(address asset, uint256 maxAssetsToWithdraw) @@ -128,10 +128,10 @@ contract PSM { sharesToBurn = _convertToSharesRoundUp(_getAssetValue(asset, assetsWithdrawn)); - // TODO: Refactor this section to not use convertToAssets because of redundant check - // TODO: Can this cause an underflow in shares? Refactor to use full shares balance? - if (sharesToBurn > shares[msg.sender]) { - assetsWithdrawn = convertToAssets(asset, shares[msg.sender]); + uint256 userShares = shares[msg.sender]; + + if (sharesToBurn > userShares) { + assetsWithdrawn = convertToAssets(asset, userShares); sharesToBurn = _convertToSharesRoundUp(_getAssetValue(asset, assetsWithdrawn)); } } @@ -203,15 +203,15 @@ contract PSM { function _convertToSharesRoundUp(uint256 assetValue) internal view returns (uint256) { uint256 totalValue = getPsmTotalValue(); if (totalValue != 0) { - return _divRoundUp(assetValue * totalShares, totalValue); + return _divUp(assetValue * totalShares, totalValue); } return assetValue; } - function _divRoundUp(uint256 numerator_, uint256 divisor_) - internal pure returns (uint256 result_) - { - result_ = (numerator_ + divisor_ - 1) / divisor_; + function _divUp(uint256 x, uint256 y) internal pure returns (uint256 z) { + unchecked { + z = x != 0 ? ((x - 1) / y) + 1 : 0; + } } function _getAssetValue(address asset, uint256 amount) internal view returns (uint256) { diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index bcabccb..453bb27 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -310,7 +310,7 @@ contract PSMWithdrawTests is PSMTestBase { // ); } - function _checkPsmInvariant() internal { + function _checkPsmInvariant() internal view { uint256 totalSharesValue = psm.convertToAssetValue(psm.totalShares()); uint256 totalAssetsValue = sDai.balanceOf(address(psm)) * rateProvider.getConversionRate() / 1e27 From 9c3958e204ee6b325249441bedd54197f0331647 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 3 Jun 2024 09:18:34 -0400 Subject: [PATCH 43/96] feat: update inflation attack test and readme --- README.md | 2 +- test/InflationAttack.t.sol | 28 ++++++++++++++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e3336fb..ff69c49 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ PSM contracts to either: ## [CRITICAL]: First Depositor Attack Prevention on Deployment -On the deployment of the PSM, the deployer **MUST make an initial deposit in order to protect the first depositor from getting attacked with a share inflation attack**. This is outlined further [here](https://github.com/marsfoundation/spark-automations/assets/44272939/9472a6d2-0361-48b0-b534-96a0614330d3). 1000 shares minted is determined to be sufficient to prevent this attack. Technical details related to this can be found in `test/InflationAttack.t.sol`. The deployment script [TODO] in this repo contains logic for the deployer to perform this initial deposit, so it is **HIGHLY RECOMMENDED** to use this deployment script when deploying the PSM. Reasoning for the technical implementation approach taken is outlined in more detail [here](https://github.com/marsfoundation/spark-psm/pull/2). +On the deployment of the PSM, the deployer **MUST make an initial deposit to get at least 1e18 shares in order to protect the first depositor from getting attacked with a share inflation attack**. This is outlined further [here](https://github.com/marsfoundation/spark-automations/assets/44272939/9472a6d2-0361-48b0-b534-96a0614330d3). 1e18 shares minted is determined to be sufficient to prevent this attack. Technical details related to this can be found in `test/InflationAttack.t.sol`. The deployment script [TODO] in this repo contains logic for the deployer to perform this initial deposit, so it is **HIGHLY RECOMMENDED** to use this deployment script when deploying the PSM. Reasoning for the technical implementation approach taken is outlined in more detail [here](https://github.com/marsfoundation/spark-psm/pull/2). ## Usage diff --git a/test/InflationAttack.t.sol b/test/InflationAttack.t.sol index 6c0c047..8cf773f 100644 --- a/test/InflationAttack.t.sol +++ b/test/InflationAttack.t.sol @@ -28,6 +28,8 @@ contract InflationAttackTests is PSMTestBase { deal(address(usdc), frontRunner, 10_000_000e6); + assertEq(psm.convertToAssetValue(1), 1); + vm.prank(frontRunner); usdc.transfer(address(psm), 10_000_000e6); @@ -63,9 +65,9 @@ contract InflationAttackTests is PSMTestBase { address frontRunner = makeAddr("frontRunner"); address deployer = address(this); // TODO: Update to use non-deployer receiver - _deposit(address(this), address(sDai), 800); /// 1000 shares + _deposit(address(this), address(sDai), 0.8e18); /// 1e18 shares - // Step 1: Front runner deposits 801 sDAI to get 1 share + // Step 1: Front runner deposits sDAI to get 1 share // User tries to do the same attack, depositing one sDAI for 1 share _deposit(frontRunner, address(sDai), 1); @@ -74,23 +76,25 @@ contract InflationAttackTests is PSMTestBase { // Step 2: Front runner transfers 10m USDC to inflate the exchange rate to 1:(10m + 1) + assertEq(psm.convertToAssetValue(1), 1); + deal(address(usdc), frontRunner, 10_000_000e6); vm.prank(frontRunner); usdc.transfer(address(psm), 10_000_000e6); - // Much less inflated exchange rate - assertEq(psm.convertToAssetValue(1), 9990.009990009990009991e18); + // Still inflated, but all value is transferred to existing holder, deployer + assertEq(psm.convertToAssetValue(1), 0.00000000001e18); - // Step 3: First depositor deposits 20 million USDC, only gets one share because rounding - // error gives them 1 instead of 2 shares, worth 15m USDC + // Step 3: First depositor deposits 20 million USDC, this time rounding is not an issue + // so value reflected is much more accurate _deposit(firstDepositor, address(usdc), 20_000_000e6); - assertEq(psm.shares(firstDepositor), 2001); + assertEq(psm.shares(firstDepositor), 1.999999800000020001e18); // Higher amount of initial shares means lower rounding error - assertEq(psm.convertToAssetValue(2001), 19_996_668.887408394403731513e18); + assertEq(psm.convertToAssetValue(1.999999800000020001e18), 19_999_999.999999999996673334e18); // Step 4: Both users withdraw the max amount of funds they can @@ -98,10 +102,10 @@ contract InflationAttackTests is PSMTestBase { _withdraw(frontRunner, address(usdc), type(uint256).max); _withdraw(deployer, address(usdc), type(uint256).max); - // Front runner loses 9.99m USDC, first depositor loses 4k USDC - assertEq(usdc.balanceOf(firstDepositor), 19_996_668.887408e6); - assertEq(usdc.balanceOf(frontRunner), 9_993.337774e6); - assertEq(usdc.balanceOf(deployer), 9_993_337.774818e6); + // Front runner loses full 10m USDC to the deployer that had all shares at the beginning, first depositor loses nothing (1e-6 USDC) + assertEq(usdc.balanceOf(firstDepositor), 19_999_999.999999e6); + assertEq(usdc.balanceOf(frontRunner), 0); + assertEq(usdc.balanceOf(deployer), 10_000_000.000001e6); } } From 4c9cf09e2bdbd87b8a60d7f3b5b9935d2921b81d Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 3 Jun 2024 09:19:31 -0400 Subject: [PATCH 44/96] fix: update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff69c49..f7d0ed8 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ PSM contracts to either: ## [CRITICAL]: First Depositor Attack Prevention on Deployment -On the deployment of the PSM, the deployer **MUST make an initial deposit to get at least 1e18 shares in order to protect the first depositor from getting attacked with a share inflation attack**. This is outlined further [here](https://github.com/marsfoundation/spark-automations/assets/44272939/9472a6d2-0361-48b0-b534-96a0614330d3). 1e18 shares minted is determined to be sufficient to prevent this attack. Technical details related to this can be found in `test/InflationAttack.t.sol`. The deployment script [TODO] in this repo contains logic for the deployer to perform this initial deposit, so it is **HIGHLY RECOMMENDED** to use this deployment script when deploying the PSM. Reasoning for the technical implementation approach taken is outlined in more detail [here](https://github.com/marsfoundation/spark-psm/pull/2). +On the deployment of the PSM, the deployer **MUST make an initial deposit to get AT LEAST 1e18 shares in order to protect the first depositor from getting attacked with a share inflation attack**. This is outlined further [here](https://github.com/marsfoundation/spark-automations/assets/44272939/9472a6d2-0361-48b0-b534-96a0614330d3). Technical details related to this can be found in `test/InflationAttack.t.sol`. The deployment script [TODO] in this repo contains logic for the deployer to perform this initial deposit, so it is **HIGHLY RECOMMENDED** to use this deployment script when deploying the PSM. Reasoning for the technical implementation approach taken is outlined in more detail [here](https://github.com/marsfoundation/spark-psm/pull/2). ## Usage From cb9931ad4d3ce00ccfe22ea55469b183af8f598b Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 3 Jun 2024 09:27:22 -0400 Subject: [PATCH 45/96] feat: update test to constrain deposit/withdraw --- src/PSM.sol | 2 +- test/Withdraw.t.sol | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index 72fd270..d3c13dd 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -132,7 +132,7 @@ contract PSM { if (sharesToBurn > userShares) { assetsWithdrawn = convertToAssets(asset, userShares); - sharesToBurn = _convertToSharesRoundUp(_getAssetValue(asset, assetsWithdrawn)); + sharesToBurn = userShares; } } diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index 453bb27..28b6731 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -168,7 +168,11 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.shares(user2), 0); // Burns the users full amount of shares } - function testFuzz_withdraw_multiUser( + // Adding this test to demonstrate that numbers are exact and correspond to assets deposits/withdrawals when withdrawals + // aren't greater than the user's share balance. The next test doesn't constrain this, but there are rounding errors of + // up to 1e12 for USDC because of the difference in asset precision. Up to 1e12 shares can be burned for 0 USDC in some + // cases, but this is an intentional rounding error against the user. + function testFuzz_withdraw_multiUser_noFullShareBurns( uint256 depositAmount1, uint256 depositAmount2, uint256 depositAmount3, @@ -184,9 +188,9 @@ contract PSMWithdrawTests is PSMTestBase { depositAmount2 = bound(depositAmount2, 0, USDC_TOKEN_MAX); depositAmount3 = bound(depositAmount3, 0, SDAI_TOKEN_MAX); - withdrawAmount1 = bound(withdrawAmount1, 0, USDC_TOKEN_MAX); - withdrawAmount2 = bound(withdrawAmount2, 0, USDC_TOKEN_MAX); - withdrawAmount3 = bound(withdrawAmount3, 0, SDAI_TOKEN_MAX); + withdrawAmount1 = bound(withdrawAmount1, depositAmount1, USDC_TOKEN_MAX); + withdrawAmount2 = bound(withdrawAmount2, depositAmount2, USDC_TOKEN_MAX); + withdrawAmount3 = bound(withdrawAmount3, 0, SDAI_TOKEN_MAX); _deposit(user1, address(usdc), depositAmount1); _deposit(user2, address(usdc), depositAmount2); From e576672c71e6043f7ce806d3adb356f0901f9398 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 3 Jun 2024 09:45:55 -0400 Subject: [PATCH 46/96] feat: update to add both cases --- test/Withdraw.t.sol | 92 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 13 deletions(-) diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index 28b6731..66fe2cb 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -188,10 +188,68 @@ contract PSMWithdrawTests is PSMTestBase { depositAmount2 = bound(depositAmount2, 0, USDC_TOKEN_MAX); depositAmount3 = bound(depositAmount3, 0, SDAI_TOKEN_MAX); - withdrawAmount1 = bound(withdrawAmount1, depositAmount1, USDC_TOKEN_MAX); - withdrawAmount2 = bound(withdrawAmount2, depositAmount2, USDC_TOKEN_MAX); - withdrawAmount3 = bound(withdrawAmount3, 0, SDAI_TOKEN_MAX); + withdrawAmount1 = bound(withdrawAmount1, 0, USDC_TOKEN_MAX); + withdrawAmount2 = bound(withdrawAmount2, 0, depositAmount2); // User can't burn up to 1e12 shares for 0 USDC in this case + withdrawAmount3 = bound(withdrawAmount3, 0, SDAI_TOKEN_MAX); + + // Run with zero share tolerance because the rounding error shouldn't be introduced with the above constraints. + _runWithdrawFuzzTests( + 0, + depositAmount1, + depositAmount2, + depositAmount3, + withdrawAmount1, + withdrawAmount2, + withdrawAmount3 + ); + } + + function testFuzz_withdraw_multiUser_fullShareBurns( + uint256 depositAmount1, + uint256 depositAmount2, + uint256 depositAmount3, + uint256 withdrawAmount1, + uint256 withdrawAmount2, + uint256 withdrawAmount3 + ) + public + { + // NOTE: Not covering zero cases, 1e-2 at 1e6 used as min for now so exact values can + // be asserted + depositAmount1 = bound(depositAmount1, 0, USDC_TOKEN_MAX); + depositAmount2 = bound(depositAmount2, 0, USDC_TOKEN_MAX); + depositAmount3 = bound(depositAmount3, 0, SDAI_TOKEN_MAX); + withdrawAmount1 = bound(withdrawAmount1, 0, USDC_TOKEN_MAX); + withdrawAmount2 = bound(withdrawAmount2, 0, USDC_TOKEN_MAX); + withdrawAmount3 = bound(withdrawAmount3, 0, SDAI_TOKEN_MAX); + + // Run with 1e12 share tolerance because the rounding error will be introduced with the above constraints. + _runWithdrawFuzzTests( + 1e12, + depositAmount1, + depositAmount2, + depositAmount3, + withdrawAmount1, + withdrawAmount2, + withdrawAmount3 + ); + } + + // NOTE: For `assertApproxEqAbs` assertions, a difference calculation is used here instead of comparing + // the two values because this approach inherently asserts that the shares remaining are lower than the + // theoretical value, proving the PSM rounds agains the user. + function _runWithdrawFuzzTests( + uint256 usdcShareTolerance, + uint256 depositAmount1, + uint256 depositAmount2, + uint256 depositAmount3, + uint256 withdrawAmount1, + uint256 withdrawAmount2, + uint256 withdrawAmount3 + ) + internal + { _deposit(user1, address(usdc), depositAmount1); _deposit(user2, address(usdc), depositAmount2); _deposit(user2, address(sDai), depositAmount3); @@ -220,6 +278,9 @@ contract PSMWithdrawTests is PSMTestBase { totalValue ); + // NOTE: User 1 doesn't need a tolerance because their shares are 1e6 precision because they only + // deposited USDC. User 2 has a tolerance because they deposited sDAI which has 1e18 precision + // so there is a chance that the rounding will be off by up to 1e12. assertEq(usdc.balanceOf(user1), expectedWithdrawnAmount1); assertEq(usdc.balanceOf(user2), 0); assertEq(usdc.balanceOf(address(psm)), totalUsdc - expectedWithdrawnAmount1); @@ -252,12 +313,17 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); - assertEq( - psm.shares(user2), - (depositAmount2 * 1e12) + (depositAmount3 * 125/100) - (expectedWithdrawnAmount2 * 1e12) + assertApproxEqAbs( + ((depositAmount2 * 1e12) + (depositAmount3 * 125/100) - (expectedWithdrawnAmount2 * 1e12)) - psm.shares(user2), + 0, + usdcShareTolerance ); - assertEq(psm.totalShares(), totalValue - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2) * 1e12); + assertApproxEqAbs( + (totalValue - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2) * 1e12) - psm.totalShares(), + 0, + usdcShareTolerance + ); uint256 expectedWithdrawnAmount3 = _getExpectedWithdrawnAmount(sDai, user2, withdrawAmount3); @@ -287,15 +353,15 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.shares(user1), (depositAmount1 - expectedWithdrawnAmount1) * 1e12); assertApproxEqAbs( - psm.shares(user2), - (depositAmount2 * 1e12) + (depositAmount3 * 125/100) - (expectedWithdrawnAmount2 * 1e12) - (expectedWithdrawnAmount3 * 125/100), - 1 + ((depositAmount2 * 1e12) + (depositAmount3 * 125/100) - (expectedWithdrawnAmount2 * 1e12) - (expectedWithdrawnAmount3 * 125/100)) - psm.shares(user2), + 0, + usdcShareTolerance + 1 // 1 is added to the tolerance because of rounding error in sDAI calculations ); assertApproxEqAbs( - psm.totalShares(), - totalValue - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2) * 1e12 - (expectedWithdrawnAmount3 * 125/100), - 1 + totalValue - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2) * 1e12 - (expectedWithdrawnAmount3 * 125/100) - psm.totalShares(), + 0, + usdcShareTolerance + 1 // 1 is added to the tolerance because of rounding error in sDAI calculations ); // -- TODO: Get these to work, rounding assertions proving always rounding down From 73d8c8cc17fe7e1197d205235106acba4859be54 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Tue, 4 Jun 2024 10:01:54 -0400 Subject: [PATCH 47/96] feat: update per review --- src/PSM.sol | 8 ++- test/Constructor.t.sol | 17 ++++++- test/Getters.t.sol | 12 ++--- test/Previews.t.sol | 40 +++++++++------ test/Swaps.t.sol | 107 +++++++++++++++++++++++------------------ 5 files changed, 114 insertions(+), 70 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index de9dec1..8736ebd 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -41,6 +41,10 @@ contract PSM { require(asset2_ != address(0), "PSM/invalid-asset2"); require(rateProvider_ != address(0), "PSM/invalid-rateProvider"); + require(asset0_ != asset1_, "PSM/asset0-asset1-same"); + require(asset0_ != asset2_, "PSM/asset0-asset2-same"); + require(asset1_ != asset2_, "PSM/asset1-asset2-same"); + asset0 = IERC20(asset0_); asset1 = IERC20(asset1_); asset2 = IERC20(asset2_); @@ -261,8 +265,8 @@ contract PSM { { return amountIn * 1e27 - * asset2Precision / IRateProviderLike(rateProvider).getConversionRate() + * asset2Precision / assetInPrecision; } @@ -271,8 +275,8 @@ contract PSM { { return amountIn * IRateProviderLike(rateProvider).getConversionRate() - * assetInPrecision / 1e27 + * assetInPrecision / asset2Precision; } diff --git a/test/Constructor.t.sol b/test/Constructor.t.sol index 1a6ddd7..976e7af 100644 --- a/test/Constructor.t.sol +++ b/test/Constructor.t.sol @@ -26,7 +26,22 @@ contract PSMConstructorTests is PSMTestBase { function test_constructor_invalidRateProvider() public { vm.expectRevert("PSM/invalid-rateProvider"); - new PSM(address(dai), address(sDai), address(usdc), address(0)); + new PSM(address(dai), address(usdc), address(sDai), address(0)); + } + + function test_constructor_asset0Asset1Match() public { + vm.expectRevert("PSM/asset0-asset1-same"); + new PSM(address(dai), address(dai), address(sDai), address(rateProvider)); + } + + function test_constructor_asset0Asset2Match() public { + vm.expectRevert("PSM/asset0-asset2-same"); + new PSM(address(dai), address(usdc), address(dai), address(rateProvider)); + } + + function test_constructor_asset1Asset2Match() public { + vm.expectRevert("PSM/asset1-asset2-same"); + new PSM(address(dai), address(usdc), address(usdc), address(rateProvider)); } function test_constructor() public { diff --git a/test/Getters.t.sol b/test/Getters.t.sol index 7187dda..b5138d7 100644 --- a/test/Getters.t.sol +++ b/test/Getters.t.sol @@ -26,13 +26,13 @@ contract PSMHarnessTests is PSMTestBase { assertEq(psmHarness.getAsset0Value(2), 2); assertEq(psmHarness.getAsset0Value(3), 3); - assertEq(psmHarness.getAsset0Value(100e6), 100e6); - assertEq(psmHarness.getAsset0Value(200e6), 200e6); - assertEq(psmHarness.getAsset0Value(300e6), 300e6); + assertEq(psmHarness.getAsset0Value(100e18), 100e18); + assertEq(psmHarness.getAsset0Value(200e18), 200e18); + assertEq(psmHarness.getAsset0Value(300e18), 300e18); - assertEq(psmHarness.getAsset0Value(100_000_000_000e6), 100_000_000_000e6); - assertEq(psmHarness.getAsset0Value(200_000_000_000e6), 200_000_000_000e6); - assertEq(psmHarness.getAsset0Value(300_000_000_000e6), 300_000_000_000e6); + assertEq(psmHarness.getAsset0Value(100_000_000_000e18), 100_000_000_000e18); + assertEq(psmHarness.getAsset0Value(200_000_000_000e18), 200_000_000_000e18); + assertEq(psmHarness.getAsset0Value(300_000_000_000e18), 300_000_000_000e18); } function testFuzz_getAsset0Value(uint256 amount) public view { diff --git a/test/Previews.t.sol b/test/Previews.t.sol index 1cd3cb6..d9beefe 100644 --- a/test/Previews.t.sol +++ b/test/Previews.t.sol @@ -17,10 +17,22 @@ contract PSMPreviewSwapFailureTests is PSMTestBase { psm.previewSwap(address(usdc), makeAddr("other-token"), 1); } -} + function test_previewSwap_bothAsset0() public { + vm.expectRevert("PSM/invalid-asset"); + psm.previewSwap(address(dai), address(dai), 1); + } -// TODO: Determine if 10 billion is too low of an upper bound for sDAI swaps, -// if exchange rate lower bound should be raised (applies to swap tests too). + function test_previewSwap_bothAsset1() public { + vm.expectRevert("PSM/invalid-asset"); + psm.previewSwap(address(usdc), address(usdc), 1); + } + + function test_previewSwap_bothAsset2() public { + vm.expectRevert("PSM/invalid-asset"); + psm.previewSwap(address(sDai), address(sDai), 1); + } + +} contract PSMPreviewSwapDaiAssetInTests is PSMTestBase { @@ -46,8 +58,8 @@ contract PSMPreviewSwapDaiAssetInTests is PSMTestBase { } function testFuzz_previewSwap_daiToSDai(uint256 amountIn, uint256 conversionRate) public { - amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates - conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate rateProvider.__setConversionRate(conversionRate); @@ -78,15 +90,13 @@ contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { assertEq(psm.previewSwap(address(usdc), address(sDai), 3e6), 2.4e18); } - function testFuzz_previewSwap_daiToSDai(uint256 amountIn, uint256 conversionRate) public { - amountIn = _bound(amountIn, 0, USDC_TOKEN_MAX); - - amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates - conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + function testFuzz_previewSwap_usdcToSDai(uint256 amountIn, uint256 conversionRate) public { + amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); + conversionRate = bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * 1e27 * 1e12 / conversionRate; + uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; assertEq(psm.previewSwap(address(usdc), address(sDai), amountIn), amountOut); } @@ -102,8 +112,8 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { } function testFuzz_previewSwap_sDaiToDai(uint256 amountIn, uint256 conversionRate) public { - amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates - conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate rateProvider.__setConversionRate(conversionRate); @@ -119,8 +129,8 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { } function testFuzz_previewSwap_sDaiToUsdc(uint256 amountIn, uint256 conversionRate) public { - amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates - conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate rateProvider.__setConversionRate(conversionRate); diff --git a/test/Swaps.t.sol b/test/Swaps.t.sol index 23ba548..d644002 100644 --- a/test/Swaps.t.sol +++ b/test/Swaps.t.sol @@ -40,6 +40,21 @@ contract PSMSwapFailureTests is PSMTestBase { psm.swap(address(usdc), makeAddr("other-token"), 100e6, 80e18, receiver); } + function test_swap_bothAsset0() public { + vm.expectRevert("PSM/invalid-asset"); + psm.swap(address(dai), address(dai), 100e6, 80e18, receiver); + } + + function test_swap_bothAsset1() public { + vm.expectRevert("PSM/invalid-asset"); + psm.swap(address(usdc), address(usdc), 100e6, 80e18, receiver); + } + + function test_swap_bothAsset2() public { + vm.expectRevert("PSM/invalid-asset"); + psm.swap(address(sDai), address(sDai), 100e6, 80e18, receiver); + } + function test_swap_minAmountOutBoundary() public { usdc.mint(swapper, 100e6); @@ -88,32 +103,40 @@ contract PSMSwapFailureTests is PSMTestBase { } function test_swap_insufficientPsmBalanceBoundary() public { - usdc.mint(swapper, 125e6 + 1); + // NOTE: Using 2 instead of 1 here because 1/1.25 rounds to 0, 2/1.25 rounds to 1 + // this is because the conversion rate is divided out before the precision conversion + // is done. + usdc.mint(swapper, 125e6 + 2); vm.startPrank(swapper); - usdc.approve(address(psm), 125e6 + 1); + usdc.approve(address(psm), 125e6 + 2); - uint256 expectedAmountOut = psm.previewSwap(address(usdc), address(sDai), 125e6 + 1); + uint256 expectedAmountOut = psm.previewSwap(address(usdc), address(sDai), 125e6 + 2); - assertEq(expectedAmountOut, 100.0000008e18); // More than balance of sDAI + assertEq(expectedAmountOut, 100.000001e18); // More than balance of sDAI vm.expectRevert("SafeERC20/transfer-failed"); - psm.swap(address(usdc), address(sDai), 125e6 + 1, 100e18, receiver); + psm.swap(address(usdc), address(sDai), 125e6 + 2, 100e18, receiver); psm.swap(address(usdc), address(sDai), 125e6, 100e18, receiver); } } -contract PSMSuccessTestsBase is PSMTestBase { +contract PSMSwapSuccessTestsBase is PSMTestBase { + + address public swapper = makeAddr("swapper"); + address public receiver = makeAddr("receiver"); function setUp() public override { super.setUp(); - dai.mint(address(psm), DAI_TOKEN_MAX); - usdc.mint(address(psm), USDC_TOKEN_MAX); - sDai.mint(address(psm), SDAI_TOKEN_MAX); + // Mint 100x higher than max amount for each token (max conversion rate) + // Covers both lower and upper bounds of conversion rate (1% to 10,000% are both 100x) + dai.mint(address(psm), DAI_TOKEN_MAX * 100); + usdc.mint(address(psm), USDC_TOKEN_MAX * 100); + sDai.mint(address(psm), SDAI_TOKEN_MAX * 100); } function _swapTest( @@ -121,48 +144,41 @@ contract PSMSuccessTestsBase is PSMTestBase { MockERC20 assetOut, uint256 amountIn, uint256 amountOut, - address swapper, - address receiver + address swapper_, + address receiver_ ) internal { - // 1 trillion of each token corresponds to MAX values - uint256 psmAssetInBalance = 1_000_000_000_000 * 10 ** assetIn.decimals(); - uint256 psmAssetOutBalance = 1_000_000_000_000 * 10 ** assetOut.decimals(); + // 100 trillion of each token corresponds to original mint amount + uint256 psmAssetInBalance = 100_000_000_000_000 * 10 ** assetIn.decimals(); + uint256 psmAssetOutBalance = 100_000_000_000_000 * 10 ** assetOut.decimals(); - assetIn.mint(swapper, amountIn); + assetIn.mint(swapper_, amountIn); - vm.startPrank(swapper); + vm.startPrank(swapper_); assetIn.approve(address(psm), amountIn); - assertEq(assetIn.allowance(swapper, address(psm)), amountIn); + assertEq(assetIn.allowance(swapper_, address(psm)), amountIn); - assertEq(assetIn.balanceOf(swapper), amountIn); + assertEq(assetIn.balanceOf(swapper_), amountIn); assertEq(assetIn.balanceOf(address(psm)), psmAssetInBalance); - assertEq(assetOut.balanceOf(receiver), 0); + assertEq(assetOut.balanceOf(receiver_), 0); assertEq(assetOut.balanceOf(address(psm)), psmAssetOutBalance); - psm.swap(address(assetIn), address(assetOut), amountIn, amountOut, receiver); + psm.swap(address(assetIn), address(assetOut), amountIn, amountOut, receiver_); - assertEq(assetIn.allowance(swapper, address(psm)), 0); + assertEq(assetIn.allowance(swapper_, address(psm)), 0); - assertEq(assetIn.balanceOf(swapper), 0); + assertEq(assetIn.balanceOf(swapper_), 0); assertEq(assetIn.balanceOf(address(psm)), psmAssetInBalance + amountIn); - assertEq(assetOut.balanceOf(receiver), amountOut); + assertEq(assetOut.balanceOf(receiver_), amountOut); assertEq(assetOut.balanceOf(address(psm)), psmAssetOutBalance - amountOut); } } -contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { - - address public swapper = makeAddr("swapper"); - address public receiver = makeAddr("receiver"); - - /**********************************************************************************************/ - /*** DAI assetIn tests ***/ - /**********************************************************************************************/ +contract PSMSwapDaiAssetInTests is PSMSwapSuccessTestsBase { function test_swap_daiToUsdc_sameReceiver() public assertAtomicPsmValueDoesNotChange { _swapTest(dai, usdc, 100e18, 100e6, swapper, swapper); @@ -204,9 +220,8 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, 10_000_000_000e18); // Using 10 billion for conversion rates - conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate - + amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate rateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate; @@ -214,9 +229,9 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { _swapTest(dai, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } - /**********************************************************************************************/ - /*** USDC assetIn tests ***/ - /**********************************************************************************************/ +} + +contract PSMSwapUsdcAssetInTests is PSMSwapSuccessTestsBase { function test_swap_usdcToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { _swapTest(usdc, dai, 100e6, 100e18, swapper, swapper); @@ -258,8 +273,8 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates - conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate rateProvider.__setConversionRate(conversionRate); @@ -268,9 +283,9 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { _swapTest(usdc, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } - /**********************************************************************************************/ - /*** sDAI assetIn tests ***/ - /**********************************************************************************************/ +} + +contract PSMSwapSDaiAssetInTests is PSMSwapSuccessTestsBase { function test_swap_sDaiToDai_sameReceiver() public assertAtomicPsmValueDoesNotChange { _swapTest(sDai, dai, 100e18, 125e18, swapper, swapper); @@ -298,8 +313,8 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates - conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate rateProvider.__setConversionRate(conversionRate); @@ -318,8 +333,8 @@ contract PSMSwapDaiAssetInTests is PSMSuccessTestsBase { vm.assume(fuzzReceiver != address(psm)); vm.assume(fuzzReceiver != address(0)); - amountIn = _bound(amountIn, 1, 10_000_000_000e6); // Using 10 billion for conversion rates - conversionRate = _bound(conversionRate, 0.01e27, 1000e27); // 1% to 100,000% conversion rate + amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate rateProvider.__setConversionRate(conversionRate); From 0f182b285258646aae160049fa6214fec68033d3 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 5 Jun 2024 09:11:34 -0400 Subject: [PATCH 48/96] feat: update to use underscore bound, fix test --- test/Conversions.t.sol | 4 +-- test/Previews.t.sol | 4 +-- test/Swaps.t.sol | 2 +- test/Withdraw.t.sol | 62 +++++++++++++++++++++--------------------- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/test/Conversions.t.sol b/test/Conversions.t.sol index f75011a..28c050d 100644 --- a/test/Conversions.t.sol +++ b/test/Conversions.t.sol @@ -56,8 +56,8 @@ contract PSMConvertToAssetsTests is PSMTestBase { function testFuzz_convertToAssets_asset2(uint256 conversionRate, uint256 amount) public { // NOTE: 0.0001e27 considered lower bound for overflow considerations - conversionRate = bound(conversionRate, 0.0001e27, 1000e27); - amount = bound(amount, 0, SDAI_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); + amount = _bound(amount, 0, SDAI_TOKEN_MAX); rateProvider.__setConversionRate(conversionRate); diff --git a/test/Previews.t.sol b/test/Previews.t.sol index d9beefe..005b3a5 100644 --- a/test/Previews.t.sol +++ b/test/Previews.t.sol @@ -91,8 +91,8 @@ contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { } function testFuzz_previewSwap_usdcToSDai(uint256 amountIn, uint256 conversionRate) public { - amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); - conversionRate = bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate + amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); + conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate rateProvider.__setConversionRate(conversionRate); diff --git a/test/Swaps.t.sol b/test/Swaps.t.sol index d644002..1cf28fa 100644 --- a/test/Swaps.t.sol +++ b/test/Swaps.t.sol @@ -278,7 +278,7 @@ contract PSMSwapUsdcAssetInTests is PSMSwapSuccessTestsBase { rateProvider.__setConversionRate(conversionRate); - uint256 amountOut = amountIn * 1e27 * 1e12 / conversionRate; + uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; _swapTest(usdc, sDai, amountIn, amountOut, fuzzSwapper, fuzzReceiver); } diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index 7b32b4e..0c6c575 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -168,7 +168,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.shares(user2), 0); // Burns the users full amount of shares } - // Adding this test to demonstrate that numbers are exact and correspond to assets deposits/withdrawals when withdrawals + // Adding this test to demonstrate that numbers are exact and correspond to assets deposits/withdrawals when withdrawals // aren't greater than the user's share balance. The next test doesn't constrain this, but there are rounding errors of // up to 1e12 for USDC because of the difference in asset precision. Up to 1e12 shares can be burned for 0 USDC in some // cases, but this is an intentional rounding error against the user. @@ -184,22 +184,22 @@ contract PSMWithdrawTests is PSMTestBase { { // NOTE: Not covering zero cases, 1e-2 at 1e6 used as min for now so exact values can // be asserted - depositAmount1 = bound(depositAmount1, 0, USDC_TOKEN_MAX); - depositAmount2 = bound(depositAmount2, 0, USDC_TOKEN_MAX); - depositAmount3 = bound(depositAmount3, 0, SDAI_TOKEN_MAX); + depositAmount1 = _bound(depositAmount1, 0, USDC_TOKEN_MAX); + depositAmount2 = _bound(depositAmount2, 0, USDC_TOKEN_MAX); + depositAmount3 = _bound(depositAmount3, 0, SDAI_TOKEN_MAX); - withdrawAmount1 = bound(withdrawAmount1, 0, USDC_TOKEN_MAX); - withdrawAmount2 = bound(withdrawAmount2, 0, depositAmount2); // User can't burn up to 1e12 shares for 0 USDC in this case - withdrawAmount3 = bound(withdrawAmount3, 0, SDAI_TOKEN_MAX); + withdrawAmount1 = _bound(withdrawAmount1, 0, USDC_TOKEN_MAX); + withdrawAmount2 = _bound(withdrawAmount2, 0, depositAmount2); // User can't burn up to 1e12 shares for 0 USDC in this case + withdrawAmount3 = _bound(withdrawAmount3, 0, SDAI_TOKEN_MAX); // Run with zero share tolerance because the rounding error shouldn't be introduced with the above constraints. _runWithdrawFuzzTests( - 0, - depositAmount1, - depositAmount2, - depositAmount3, - withdrawAmount1, - withdrawAmount2, + 0, + depositAmount1, + depositAmount2, + depositAmount3, + withdrawAmount1, + withdrawAmount2, withdrawAmount3 ); } @@ -216,39 +216,39 @@ contract PSMWithdrawTests is PSMTestBase { { // NOTE: Not covering zero cases, 1e-2 at 1e6 used as min for now so exact values can // be asserted - depositAmount1 = bound(depositAmount1, 0, USDC_TOKEN_MAX); - depositAmount2 = bound(depositAmount2, 0, USDC_TOKEN_MAX); - depositAmount3 = bound(depositAmount3, 0, SDAI_TOKEN_MAX); + depositAmount1 = _bound(depositAmount1, 0, USDC_TOKEN_MAX); + depositAmount2 = _bound(depositAmount2, 0, USDC_TOKEN_MAX); + depositAmount3 = _bound(depositAmount3, 0, SDAI_TOKEN_MAX); - withdrawAmount1 = bound(withdrawAmount1, 0, USDC_TOKEN_MAX); - withdrawAmount2 = bound(withdrawAmount2, 0, USDC_TOKEN_MAX); - withdrawAmount3 = bound(withdrawAmount3, 0, SDAI_TOKEN_MAX); + withdrawAmount1 = _bound(withdrawAmount1, 0, USDC_TOKEN_MAX); + withdrawAmount2 = _bound(withdrawAmount2, 0, USDC_TOKEN_MAX); + withdrawAmount3 = _bound(withdrawAmount3, 0, SDAI_TOKEN_MAX); // Run with 1e12 share tolerance because the rounding error will be introduced with the above constraints. _runWithdrawFuzzTests( - 1e12, - depositAmount1, - depositAmount2, - depositAmount3, - withdrawAmount1, - withdrawAmount2, + 1e12, + depositAmount1, + depositAmount2, + depositAmount3, + withdrawAmount1, + withdrawAmount2, withdrawAmount3 ); } - // NOTE: For `assertApproxEqAbs` assertions, a difference calculation is used here instead of comparing - // the two values because this approach inherently asserts that the shares remaining are lower than the + // NOTE: For `assertApproxEqAbs` assertions, a difference calculation is used here instead of comparing + // the two values because this approach inherently asserts that the shares remaining are lower than the // theoretical value, proving the PSM rounds agains the user. function _runWithdrawFuzzTests( - uint256 usdcShareTolerance, + uint256 usdcShareTolerance, uint256 depositAmount1, uint256 depositAmount2, uint256 depositAmount3, uint256 withdrawAmount1, uint256 withdrawAmount2, uint256 withdrawAmount3 - ) - internal + ) + internal { _deposit(user1, address(usdc), depositAmount1); _deposit(user2, address(usdc), depositAmount2); @@ -320,7 +320,7 @@ contract PSMWithdrawTests is PSMTestBase { ); assertApproxEqAbs( - (totalValue - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2) * 1e12) - psm.totalShares(), + (totalValue - (expectedWithdrawnAmount1 + expectedWithdrawnAmount2) * 1e12) - psm.totalShares(), 0, usdcShareTolerance ); From ba440868ed07955cc56e79c43ce52cbeb2662655 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 5 Jun 2024 09:14:10 -0400 Subject: [PATCH 49/96] fix: typo --- test/Withdraw.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index 7d1bb88..72474e8 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -293,7 +293,7 @@ contract PSMWithdrawTests is PSMTestBase { // NOTE: For `assertApproxEqAbs` assertions, a difference calculation is used here instead of comparing // the two values because this approach inherently asserts that the shares remaining are lower than the - // theoretical value, proving the PSM rounds agains the user. + // theoretical value, proving the PSM rounds against the user. function _runWithdrawFuzzTests( uint256 usdcShareTolerance, uint256 depositAmount1, From 9a243f941974c00517b750d88ec8a54d6861104a Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 10 Jun 2024 08:40:34 -0400 Subject: [PATCH 50/96] feat: add overrides, remove referrals, update referral type --- src/PSM.sol | 18 +++++++------ src/interfaces/IPSM.sol | 18 +++++-------- test/Deposit.t.sol | 20 +++++++------- test/Events.t.sol | 32 +++++++++++----------- test/PSMTestBase.sol | 4 +-- test/Withdraw.t.sol | 60 ++++++++++++++++++++--------------------- 6 files changed, 73 insertions(+), 79 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index 12031b8..81d641c 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -66,9 +66,9 @@ contract PSM is IPSM { uint256 amountIn, uint256 minAmountOut, address receiver, - uint16 referralCode + uint256 referralCode ) - external + external override { require(amountIn != 0, "PSM/invalid-amountIn"); require(receiver != address(0), "PSM/invalid-receiver"); @@ -87,7 +87,7 @@ contract PSM is IPSM { /*** Liquidity provision functions ***/ /**********************************************************************************************/ - function deposit(address asset, uint256 assetsToDeposit, uint16 referralCode) + function deposit(address asset, uint256 assetsToDeposit) external override returns (uint256 newShares) { newShares = previewDeposit(asset, assetsToDeposit); @@ -97,10 +97,10 @@ contract PSM is IPSM { IERC20(asset).safeTransferFrom(msg.sender, address(this), assetsToDeposit); - emit Deposit(asset, msg.sender, assetsToDeposit, newShares, referralCode); + emit Deposit(asset, msg.sender, assetsToDeposit, newShares); } - function withdraw(address asset, uint256 maxAssetsToWithdraw, uint16 referralCode) + function withdraw(address asset, uint256 maxAssetsToWithdraw) external override returns (uint256 assetsWithdrawn) { uint256 sharesToBurn; @@ -114,14 +114,16 @@ contract PSM is IPSM { IERC20(asset).safeTransfer(msg.sender, assetsWithdrawn); - emit Withdraw(asset, msg.sender, assetsWithdrawn, sharesToBurn, referralCode); + emit Withdraw(asset, msg.sender, assetsWithdrawn, sharesToBurn); } /**********************************************************************************************/ /*** Deposit/withdraw preview functions ***/ /**********************************************************************************************/ - function previewDeposit(address asset, uint256 assetsToDeposit) public view returns (uint256) { + function previewDeposit(address asset, uint256 assetsToDeposit) + public view override returns (uint256) + { require(_isValidAsset(asset), "PSM/invalid-asset"); // Convert amount to 1e18 precision denominated in value of asset0 then convert to shares. @@ -175,7 +177,7 @@ contract PSM is IPSM { } /**********************************************************************************************/ - /*** Swap preview functions ***/ + /*** Conversion functions ***/ /**********************************************************************************************/ function convertToAssets(address asset, uint256 numShares) diff --git a/src/interfaces/IPSM.sol b/src/interfaces/IPSM.sol index 8838d98..f16bc32 100644 --- a/src/interfaces/IPSM.sol +++ b/src/interfaces/IPSM.sol @@ -28,7 +28,7 @@ interface IPSM { address indexed receiver, uint256 amountIn, uint256 amountOut, - uint16 referralCode + uint256 referralCode ); /** @@ -37,14 +37,12 @@ interface IPSM { * @param user Address of the user that deposited the asset. * @param assetsDeposited Amount of the asset deposited. * @param sharesMinted Number of shares minted to the user. - * @param referralCode Referral code for the deposit. */ event Deposit( address indexed asset, address indexed user, uint256 assetsDeposited, - uint256 sharesMinted, - uint16 referralCode + uint256 sharesMinted ); /** @@ -53,14 +51,12 @@ interface IPSM { * @param user Address of the user that withdrew the asset. * @param assetsWithdrawn Amount of the asset withdrawn. * @param sharesBurned Number of shares burned from the user. - * @param referralCode Referral code for the withdrawal. */ event Withdraw( address indexed asset, address indexed user, uint256 assetsWithdrawn, - uint256 sharesBurned, - uint16 referralCode + uint256 sharesBurned ); /**********************************************************************************************/ @@ -132,7 +128,7 @@ interface IPSM { uint256 amountIn, uint256 minAmountOut, address receiver, - uint16 referralCode + uint256 referralCode ) external; /**********************************************************************************************/ @@ -145,10 +141,9 @@ interface IPSM { * the current exchange rate. * @param asset Address of the ERC-20 asset to deposit. * @param assetsToDeposit Amount of the asset to deposit into the PSM. - * @param referralCode Referral code for the deposit. * @return newShares Number of shares minted to the user. */ - function deposit(address asset, uint256 assetsToDeposit, uint16 referralCode) + function deposit(address asset, uint256 assetsToDeposit) external returns (uint256 newShares); /** @@ -158,10 +153,9 @@ interface IPSM { * that the user's shares can be converted to. * @param asset Address of the ERC-20 asset to withdraw. * @param maxAssetsToWithdraw Max amount that the user is willing to withdraw. - * @param referralCode Referral code for the withdrawal. * @return assetsWithdrawn Resulting amount of the asset withdrawn from the PSM. */ - function withdraw(address asset, uint256 maxAssetsToWithdraw, uint16 referralCode) + function withdraw(address asset, uint256 maxAssetsToWithdraw) external returns (uint256 assetsWithdrawn); /**********************************************************************************************/ diff --git a/test/Deposit.t.sol b/test/Deposit.t.sol index 47464bf..9830c1c 100644 --- a/test/Deposit.t.sol +++ b/test/Deposit.t.sol @@ -14,7 +14,7 @@ contract PSMDepositTests is PSMTestBase { function test_deposit_notAsset0OrAsset1() public { vm.expectRevert("PSM/invalid-asset"); - psm.deposit(makeAddr("new-asset"), 100e6, 0); + psm.deposit(makeAddr("new-asset"), 100e6); } // TODO: Add balance/approve failure tests @@ -35,7 +35,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(usdc), 100e6, 0); + psm.deposit(address(usdc), 100e6); assertEq(usdc.allowance(user1, address(psm)), 0); assertEq(usdc.balanceOf(user1), 0); @@ -63,7 +63,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), 100e18, 0); + psm.deposit(address(sDai), 100e18); assertEq(sDai.allowance(user1, address(psm)), 0); assertEq(sDai.balanceOf(user1), 0); @@ -82,7 +82,7 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), 100e6); - psm.deposit(address(usdc), 100e6, 0); + psm.deposit(address(usdc), 100e6); sDai.mint(user1, 100e18); sDai.approve(address(psm), 100e18); @@ -98,7 +98,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), 100e18, 0); + psm.deposit(address(sDai), 100e18); assertEq(usdc.balanceOf(address(psm)), 100e6); @@ -122,7 +122,7 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), usdcAmount); - psm.deposit(address(usdc), usdcAmount, 0); + psm.deposit(address(usdc), usdcAmount); sDai.mint(user1, sDaiAmount); sDai.approve(address(psm), sDaiAmount); @@ -138,7 +138,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - psm.deposit(address(sDai), sDaiAmount, 0); + psm.deposit(address(sDai), sDaiAmount); assertEq(usdc.balanceOf(address(psm)), usdcAmount); @@ -159,12 +159,12 @@ contract PSMDepositTests is PSMTestBase { usdc.approve(address(psm), 100e6); - psm.deposit(address(usdc), 100e6, 0); + psm.deposit(address(usdc), 100e6); sDai.mint(user1, 100e18); sDai.approve(address(psm), 100e18); - psm.deposit(address(sDai), 100e18, 0); + psm.deposit(address(sDai), 100e18); vm.stopPrank(); @@ -204,7 +204,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.getPsmTotalValue(), 250e18); - psm.deposit(address(sDai), 100e18, 0); + psm.deposit(address(sDai), 100e18); assertEq(psm.shares(user2), 135e18); diff --git a/test/Events.t.sol b/test/Events.t.sol index ac9d768..0ac7ba6 100644 --- a/test/Events.t.sol +++ b/test/Events.t.sol @@ -14,23 +14,21 @@ contract PSMEventTests is PSMTestBase { address indexed receiver, uint256 amountIn, uint256 amountOut, - uint16 referralCode + uint256 referralCode ); event Deposit( address indexed asset, address indexed user, uint256 assetsDeposited, - uint256 sharesMinted, - uint16 referralCode + uint256 sharesMinted ); event Withdraw( address indexed asset, address indexed user, uint256 assetsWithdrawn, - uint256 sharesBurned, - uint16 referralCode + uint256 sharesBurned ); address sender = makeAddr("sender"); @@ -43,22 +41,22 @@ contract PSMEventTests is PSMTestBase { dai.approve(address(psm), 100e18); vm.expectEmit(); - emit Deposit(address(dai), sender, 100e18, 100e18, 1); - psm.deposit(address(dai), 100e18, 1); + emit Deposit(address(dai), sender, 100e18, 100e18); + psm.deposit(address(dai), 100e18); usdc.mint(sender, 100e6); usdc.approve(address(psm), 100e6); vm.expectEmit(); - emit Deposit(address(usdc), sender, 100e6, 100e18, 2); // Different code - psm.deposit(address(usdc), 100e6, 2); + emit Deposit(address(usdc), sender, 100e6, 100e18); // Different code + psm.deposit(address(usdc), 100e6); sDai.mint(sender, 100e18); sDai.approve(address(psm), 100e18); vm.expectEmit(); - emit Deposit(address(sDai), sender, 100e18, 125e18, 3); // Different code - psm.deposit(address(sDai), 100e18, 3); + emit Deposit(address(sDai), sender, 100e18, 125e18); // Different code + psm.deposit(address(sDai), 100e18); } function test_withdraw_events() public { @@ -69,16 +67,16 @@ contract PSMEventTests is PSMTestBase { vm.startPrank(sender); vm.expectEmit(); - emit Withdraw(address(dai), sender, 100e18, 100e18, 1); - psm.withdraw(address(dai), 100e18, 1); + emit Withdraw(address(dai), sender, 100e18, 100e18); + psm.withdraw(address(dai), 100e18); vm.expectEmit(); - emit Withdraw(address(usdc), sender, 100e6, 100e18, 2); - psm.withdraw(address(usdc), 100e6, 2); + emit Withdraw(address(usdc), sender, 100e6, 100e18); + psm.withdraw(address(usdc), 100e6); vm.expectEmit(); - emit Withdraw(address(sDai), sender, 100e18, 125e18, 3); - psm.withdraw(address(sDai), 100e18, 3); + emit Withdraw(address(sDai), sender, 100e18, 125e18); + psm.withdraw(address(sDai), 100e18); } function test_swap_events() public { diff --git a/test/PSMTestBase.sol b/test/PSMTestBase.sol index dc02d46..8c55f31 100644 --- a/test/PSMTestBase.sol +++ b/test/PSMTestBase.sol @@ -58,13 +58,13 @@ contract PSMTestBase is Test { vm.startPrank(user); MockERC20(asset).mint(user, amount); MockERC20(asset).approve(address(psm), amount); - psm.deposit(asset, amount, 0); + psm.deposit(asset, amount); vm.stopPrank(); } function _withdraw(address user, address asset, uint256 amount) internal { vm.prank(user); - psm.withdraw(asset, amount, 0); + psm.withdraw(asset, amount); vm.stopPrank(); } diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index 595cf7f..0c6c575 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -16,7 +16,7 @@ contract PSMWithdrawTests is PSMTestBase { function test_withdraw_notAsset0OrAsset1() public { vm.expectRevert("PSM/invalid-asset"); - psm.withdraw(makeAddr("new-asset"), 100e6, 0); + psm.withdraw(makeAddr("new-asset"), 100e6); } // TODO: Add balance/approve failure tests @@ -33,7 +33,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - uint256 amount = psm.withdraw(address(usdc), 100e6, 0); + uint256 amount = psm.withdraw(address(usdc), 100e6); assertEq(amount, 100e6); @@ -58,7 +58,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - uint256 amount = psm.withdraw(address(sDai), 80e18, 0); + uint256 amount = psm.withdraw(address(sDai), 80e18); assertEq(amount, 80e18); @@ -87,7 +87,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - uint256 amount = psm.withdraw(address(usdc), 100e6, 0); + uint256 amount = psm.withdraw(address(usdc), 100e6); assertEq(amount, 100e6); @@ -103,7 +103,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - amount = psm.withdraw(address(sDai), 100e18, 0); + amount = psm.withdraw(address(sDai), 100e18); assertEq(amount, 100e18); @@ -132,7 +132,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user1); - uint256 amount = psm.withdraw(address(usdc), 125e6, 0); + uint256 amount = psm.withdraw(address(usdc), 125e6); assertEq(amount, 100e6); @@ -157,7 +157,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); vm.prank(user2); - uint256 amount = psm.withdraw(address(usdc), 225e6, 0); + uint256 amount = psm.withdraw(address(usdc), 225e6); assertEq(amount, 200e6); @@ -267,7 +267,7 @@ contract PSMWithdrawTests is PSMTestBase { = _getExpectedWithdrawnAmount(usdc, user1, withdrawAmount1); vm.prank(user1); - uint256 amount = psm.withdraw(address(usdc), withdrawAmount1, 0); + uint256 amount = psm.withdraw(address(usdc), withdrawAmount1); assertEq(amount, expectedWithdrawnAmount1); @@ -293,7 +293,7 @@ contract PSMWithdrawTests is PSMTestBase { = _getExpectedWithdrawnAmount(usdc, user2, withdrawAmount2); vm.prank(user2); - amount = psm.withdraw(address(usdc), withdrawAmount2, 0); + amount = psm.withdraw(address(usdc), withdrawAmount2); assertEq(amount, expectedWithdrawnAmount2); @@ -329,7 +329,7 @@ contract PSMWithdrawTests is PSMTestBase { = _getExpectedWithdrawnAmount(sDai, user2, withdrawAmount3); vm.prank(user2); - amount = psm.withdraw(address(sDai), withdrawAmount3, 0); + amount = psm.withdraw(address(sDai), withdrawAmount3); assertApproxEqAbs(amount, expectedWithdrawnAmount3, 1); @@ -449,16 +449,16 @@ contract PSMWithdrawTests is PSMTestBase { // // Original full balance reverts // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), 100e18, 0); + // psm.withdraw(address(usdc), 100e18); // // Boundary condition at 90.000001e18 shares // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), maxUsdcShares + 1, 0); + // psm.withdraw(address(usdc), maxUsdcShares + 1); // console2.log("First CTA", psm.convertToAssetValue(100e18)); // // Rounds down here and transfers 100e6 USDC - // psm.withdraw(address(usdc), maxUsdcShares, 0); + // psm.withdraw(address(usdc), maxUsdcShares); // console2.log("\n\n\n"); @@ -471,7 +471,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Second CTA", psm.convertToAssetValue(100e18)); - // psm.withdraw(address(sDai), 100e18 - maxUsdcShares, 0); + // psm.withdraw(address(sDai), 100e18 - maxUsdcShares); // uint256 sDaiUser1Balance = 7.407406790123452675e18; @@ -489,7 +489,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Third CTA", psm.convertToAssetValue(100e18)); // // Withdraw shares originally worth $100 to compare yield with user1 - // psm.withdraw(address(sDai), 125e18, 0); + // psm.withdraw(address(sDai), 125e18); // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); // assertEq(sDai.balanceOf(user2), 100e18 - sDaiUser1Balance - 1); @@ -552,16 +552,16 @@ contract PSMWithdrawTests is PSMTestBase { // // Original full balance reverts // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), 100_000_000e18, 0); + // psm.withdraw(address(usdc), 100_000_000e18); // // Boundary condition at 90.000001e18 shares // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), maxUsdcShares + 1, 0); + // psm.withdraw(address(usdc), maxUsdcShares + 1); // console2.log("First CTA", psm.convertToAssetValue(100_000_000e18)); // // Rounds down here and transfers 100_000_000e6 USDC - // psm.withdraw(address(usdc), maxUsdcShares, 0); + // psm.withdraw(address(usdc), maxUsdcShares); // console2.log("\n\n\n"); @@ -578,7 +578,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Second CTA", psm.convertToAssetValue(100_000_000e18)); - // psm.withdraw(address(sDai), 100_000_000e18 - maxUsdcShares, 0); + // psm.withdraw(address(sDai), 100_000_000e18 - maxUsdcShares); // uint256 sDaiUser1Balance = 7_407_407.407407407407407407e18; @@ -596,7 +596,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Third CTA", psm.convertToAssetValue(100e18)); // // Withdraw shares originally worth $100 to compare yield with user1 - // psm.withdraw(address(sDai), 125_000_000e18, 0); + // psm.withdraw(address(sDai), 125_000_000e18); // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); // assertEq(sDai.balanceOf(user2), 100_000_000e18 - sDaiUser1Balance - 1); @@ -661,16 +661,16 @@ contract PSMWithdrawTests is PSMTestBase { // // Original full balance reverts // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), 100_000_000e18, 0); + // psm.withdraw(address(usdc), 100_000_000e18); // // Boundary condition at 90.000001e18 shares // vm.expectRevert("SafeERC20/transfer-failed"); - // psm.withdraw(address(usdc), maxUsdcShares + 1, 0); + // psm.withdraw(address(usdc), maxUsdcShares + 1); // console2.log("First CTA", psm.convertToAssetValue(100_000_000e18)); // // Rounds down here and transfers 100_000_000e6 USDC - // psm.withdraw(address(usdc), maxUsdcShares, 0); + // psm.withdraw(address(usdc), maxUsdcShares); // console2.log("\n\n\n"); @@ -685,7 +685,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Second CTA", psm.convertToAssetValue(100_000_000e18)); - // psm.withdraw(address(sDai), 100_000_000e18 - maxUsdcShares, 0); + // psm.withdraw(address(sDai), 100_000_000e18 - maxUsdcShares); // uint256 sDaiUser1Balance = 7_407_407.407406790123456790e18; @@ -703,7 +703,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Third CTA", psm.convertToAssetValue(100e18)); // // Withdraw shares originally worth $100 to compare yield with user1 - // psm.withdraw(address(sDai), 125_000_000e18, 0); + // psm.withdraw(address(sDai), 125_000_000e18); // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); // assertEq(sDai.balanceOf(user2), 100_000_000e18 - sDaiUser1Balance); @@ -770,18 +770,18 @@ contract PSMWithdrawTests is PSMTestBase { // // // Original full balance reverts // // vm.expectRevert("SafeERC20/transfer-failed"); - // // psm.withdraw(address(usdc), 100e18, 0); + // // psm.withdraw(address(usdc), 100e18); // // // Boundary condition at 90.000001e18 shares // // vm.expectRevert("SafeERC20/transfer-failed"); - // // psm.withdraw(address(usdc), maxUsdcShares + 1, 0); + // // psm.withdraw(address(usdc), maxUsdcShares + 1); // console2.log("First CTA", psm.convertToAssetValue(100e18)); // // maxUsdcShares = 89.99999e18; // // Rounds down here and transfers 100e6 USDC - // psm.withdraw(address(usdc), maxUsdcShares, 0); + // psm.withdraw(address(usdc), maxUsdcShares); // console2.log("\n\n\n"); @@ -794,7 +794,7 @@ contract PSMWithdrawTests is PSMTestBase { // console2.log("Second CTA", psm.convertToAssetValue(100e18)); - // // psm.withdraw(address(sDai), 100e18 - maxUsdcShares, 0); + // // psm.withdraw(address(sDai), 100e18 - maxUsdcShares); // // uint256 sDaiUser1Balance = 7.407406790123452675e18; @@ -812,7 +812,7 @@ contract PSMWithdrawTests is PSMTestBase { // // console2.log("Third CTA", psm.convertToAssetValue(100e18)); // // // Withdraw shares originally worth $100 to compare yield with user1 - // // psm.withdraw(address(sDai), 100e18, 0); + // // psm.withdraw(address(sDai), 100e18); // // // assertEq(sDai.balanceOf(user1), sDaiUser1Balance); // // // assertEq(sDai.balanceOf(user2), 100e18 - sDaiUser1Balance - 1); From cc68ed39509404c90562c0178021a305caa1edc3 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 10 Jun 2024 08:41:27 -0400 Subject: [PATCH 51/96] fix: update expect emit --- test/Events.t.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/Events.t.sol b/test/Events.t.sol index 0ac7ba6..91507fe 100644 --- a/test/Events.t.sol +++ b/test/Events.t.sol @@ -40,21 +40,21 @@ contract PSMEventTests is PSMTestBase { dai.mint(sender, 100e18); dai.approve(address(psm), 100e18); - vm.expectEmit(); + vm.expectEmit(address(psm)); emit Deposit(address(dai), sender, 100e18, 100e18); psm.deposit(address(dai), 100e18); usdc.mint(sender, 100e6); usdc.approve(address(psm), 100e6); - vm.expectEmit(); + vm.expectEmit(address(psm)); emit Deposit(address(usdc), sender, 100e6, 100e18); // Different code psm.deposit(address(usdc), 100e6); sDai.mint(sender, 100e18); sDai.approve(address(psm), 100e18); - vm.expectEmit(); + vm.expectEmit(address(psm)); emit Deposit(address(sDai), sender, 100e18, 125e18); // Different code psm.deposit(address(sDai), 100e18); } @@ -66,15 +66,15 @@ contract PSMEventTests is PSMTestBase { vm.startPrank(sender); - vm.expectEmit(); + vm.expectEmit(address(psm)); emit Withdraw(address(dai), sender, 100e18, 100e18); psm.withdraw(address(dai), 100e18); - vm.expectEmit(); + vm.expectEmit(address(psm)); emit Withdraw(address(usdc), sender, 100e6, 100e18); psm.withdraw(address(usdc), 100e6); - vm.expectEmit(); + vm.expectEmit(address(psm)); emit Withdraw(address(sDai), sender, 100e18, 125e18); psm.withdraw(address(sDai), 100e18); } @@ -106,7 +106,7 @@ contract PSMEventTests is PSMTestBase { MockERC20(assetIn).mint(sender, amountIn); MockERC20(assetIn).approve(address(psm), amountIn); - vm.expectEmit(); + vm.expectEmit(address(psm)); emit Swap(assetIn, assetOut, sender, receiver, amountIn, expectedAmountOut, referralCode); psm.swap(assetIn, assetOut, amountIn, 0, receiver, referralCode); } From 9862a532b06cdab3fa5ed006dac87fd5ee82716b Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 10 Jun 2024 15:33:14 -0400 Subject: [PATCH 52/96] feat: update name and remove todos --- src/PSM.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PSM.sol b/src/PSM.sol index e48efb9..1dc706b 100644 --- a/src/PSM.sol +++ b/src/PSM.sol @@ -11,10 +11,8 @@ interface IRateProviderLike { function getConversionRate() external view returns (uint256); } -// TODO: Determine what admin functionality we want (fees?) -// TODO: Refactor into inheritance structure // TODO: Prove that we're always rounding against user -contract PSM is IPSM { +contract PSM3 is IPSM { using SafeERC20 for IERC20; From 376fa83578abe6b0bb89d0110ac1a778a726e922 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 10 Jun 2024 15:42:15 -0400 Subject: [PATCH 53/96] feat: move files and set up structure --- src/{PSM.sol => PSM3.sol} | 0 test/PSMTestBase.sol | 4 ++-- test/invariant/handlers/Invariants.t.sol | 19 +++++++++++++++++++ test/{ => unit}/Constructor.t.sol | 2 +- test/{ => unit}/Conversions.t.sol | 2 +- test/{ => unit}/Deposit.t.sol | 2 +- test/{ => unit}/Events.t.sol | 0 test/{ => unit}/Getters.t.sol | 2 +- test/{ => unit}/InflationAttack.t.sol | 0 test/{ => unit}/Previews.t.sol | 0 test/{ => unit}/Swaps.t.sol | 2 +- test/{ => unit}/Withdraw.t.sol | 2 +- test/{ => unit}/harnesses/PSMHarness.sol | 0 13 files changed, 27 insertions(+), 8 deletions(-) rename src/{PSM.sol => PSM3.sol} (100%) create mode 100644 test/invariant/handlers/Invariants.t.sol rename test/{ => unit}/Constructor.t.sol (98%) rename test/{ => unit}/Conversions.t.sol (99%) rename test/{ => unit}/Deposit.t.sol (99%) rename test/{ => unit}/Events.t.sol (100%) rename test/{ => unit}/Getters.t.sol (99%) rename test/{ => unit}/InflationAttack.t.sol (100%) rename test/{ => unit}/Previews.t.sol (100%) rename test/{ => unit}/Swaps.t.sol (99%) rename test/{ => unit}/Withdraw.t.sol (99%) rename test/{ => unit}/harnesses/PSMHarness.sol (100%) diff --git a/src/PSM.sol b/src/PSM3.sol similarity index 100% rename from src/PSM.sol rename to src/PSM3.sol diff --git a/test/PSMTestBase.sol b/test/PSMTestBase.sol index 51480b3..1e8c29e 100644 --- a/test/PSMTestBase.sol +++ b/test/PSMTestBase.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "src/PSM.sol"; +import { PSM3 } from "src/PSM3.sol"; import { MockERC20 } from "erc20-helpers/MockERC20.sol"; @@ -11,7 +11,7 @@ import { MockRateProvider } from "test/mocks/MockRateProvider.sol"; contract PSMTestBase is Test { - PSM public psm; + PSM3 public psm; // NOTE: Using DAI, sDAI and USDC as example assets MockERC20 public dai; diff --git a/test/invariant/handlers/Invariants.t.sol b/test/invariant/handlers/Invariants.t.sol new file mode 100644 index 0000000..6d8ec5c --- /dev/null +++ b/test/invariant/handlers/Invariants.t.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import { PSM } from "src/PSM.sol"; + +import { PSMTestBase } from "test/PSMTestBase.sol"; + +contract PSMInvariantTests is PSMTestBase { + + function setUp() public override { + super.setUp(); + } + + function invariant_A() public { + assertEq(true, true); + } +} diff --git a/test/Constructor.t.sol b/test/unit/Constructor.t.sol similarity index 98% rename from test/Constructor.t.sol rename to test/unit/Constructor.t.sol index 848385e..d131bbf 100644 --- a/test/Constructor.t.sol +++ b/test/unit/Constructor.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "../src/PSM.sol"; +import { PSM } from "src/PSM.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; diff --git a/test/Conversions.t.sol b/test/unit/Conversions.t.sol similarity index 99% rename from test/Conversions.t.sol rename to test/unit/Conversions.t.sol index 35a9374..6309670 100644 --- a/test/Conversions.t.sol +++ b/test/unit/Conversions.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "../src/PSM.sol"; +import { PSM } from "src/PSM.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; diff --git a/test/Deposit.t.sol b/test/unit/Deposit.t.sol similarity index 99% rename from test/Deposit.t.sol rename to test/unit/Deposit.t.sol index 4d30980..4c9e5db 100644 --- a/test/Deposit.t.sol +++ b/test/unit/Deposit.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "../src/PSM.sol"; +import { PSM } from "src/PSM.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; diff --git a/test/Events.t.sol b/test/unit/Events.t.sol similarity index 100% rename from test/Events.t.sol rename to test/unit/Events.t.sol diff --git a/test/Getters.t.sol b/test/unit/Getters.t.sol similarity index 99% rename from test/Getters.t.sol rename to test/unit/Getters.t.sol index b5138d7..50069c1 100644 --- a/test/Getters.t.sol +++ b/test/unit/Getters.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -import { PSMHarness } from "test/harnesses/PSMHarness.sol"; +import { PSMHarness } from "test/unit/harnesses/PSMHarness.sol"; contract PSMHarnessTests is PSMTestBase { diff --git a/test/InflationAttack.t.sol b/test/unit/InflationAttack.t.sol similarity index 100% rename from test/InflationAttack.t.sol rename to test/unit/InflationAttack.t.sol diff --git a/test/Previews.t.sol b/test/unit/Previews.t.sol similarity index 100% rename from test/Previews.t.sol rename to test/unit/Previews.t.sol diff --git a/test/Swaps.t.sol b/test/unit/Swaps.t.sol similarity index 99% rename from test/Swaps.t.sol rename to test/unit/Swaps.t.sol index f8151d9..0fe3e4c 100644 --- a/test/Swaps.t.sol +++ b/test/unit/Swaps.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "../src/PSM.sol"; +import { PSM } from "src/PSM.sol"; import { MockERC20, PSMTestBase } from "test/PSMTestBase.sol"; diff --git a/test/Withdraw.t.sol b/test/unit/Withdraw.t.sol similarity index 99% rename from test/Withdraw.t.sol rename to test/unit/Withdraw.t.sol index 72474e8..62c9000 100644 --- a/test/Withdraw.t.sol +++ b/test/unit/Withdraw.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "../src/PSM.sol"; +import { PSM } from "src/PSM.sol"; import { MockERC20 } from "erc20-helpers/MockERC20.sol"; diff --git a/test/harnesses/PSMHarness.sol b/test/unit/harnesses/PSMHarness.sol similarity index 100% rename from test/harnesses/PSMHarness.sol rename to test/unit/harnesses/PSMHarness.sol From 73cb854a2e13aafa9cb60d9362ffb9edefaa4234 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 10 Jun 2024 15:50:38 -0400 Subject: [PATCH 54/96] feat: update to rename files, contracts, and errors --- README.md | 4 +- src/{PSM.sol => PSM3.sol} | 44 +++++++++---------- src/interfaces/{IPSM.sol => IPSM3.sol} | 6 +-- test/Constructor.t.sol | 32 +++++++------- test/Conversions.t.sol | 6 +-- test/Deposit.t.sol | 8 ++-- test/Getters.t.sol | 8 ++-- test/InflationAttack.t.sol | 6 +-- test/PSMTestBase.sol | 6 +-- test/Previews.t.sol | 10 ++--- test/Swaps.t.sol | 18 ++++---- test/Withdraw.t.sol | 8 ++-- .../{PSMHarness.sol => PSM3Harness.sol} | 6 +-- 13 files changed, 81 insertions(+), 81 deletions(-) rename src/{PSM.sol => PSM3.sol} (89%) rename src/interfaces/{IPSM.sol => IPSM3.sol} (99%) rename test/harnesses/{PSMHarness.sol => PSM3Harness.sol} (84%) diff --git a/README.md b/README.md index c296759..00b94ae 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ For detailed implementation, refer to the contract code and `IPSM` interface doc ## Contracts -- **`src/PSM.sol`**: The core contract implementing the `IPSM` interface, providing functionality for swapping, depositing, and withdrawing assets. -- **`src/interfaces/IPSM.sol`**: Defines the essential functions and events that the PSM contract implements. +- **`src/PSM3.sol`**: The core contract implementing the `IPSM3` interface, providing functionality for swapping, depositing, and withdrawing assets. +- **`src/interfaces/IPSM3.sol`**: Defines the essential functions and events that the PSM contract implements. ## [CRITICAL]: First Depositor Attack Prevention on Deployment diff --git a/src/PSM.sol b/src/PSM3.sol similarity index 89% rename from src/PSM.sol rename to src/PSM3.sol index 1dc706b..d33bdd8 100644 --- a/src/PSM.sol +++ b/src/PSM3.sol @@ -5,14 +5,14 @@ import { IERC20 } from "erc20-helpers/interfaces/IERC20.sol"; import { SafeERC20 } from "erc20-helpers/SafeERC20.sol"; -import { IPSM } from "src/interfaces/IPSM.sol"; +import { IPSM3 } from "src/interfaces/IPSM3.sol"; interface IRateProviderLike { function getConversionRate() external view returns (uint256); } // TODO: Prove that we're always rounding against user -contract PSM3 is IPSM { +contract PSM3 is IPSM3 { using SafeERC20 for IERC20; @@ -33,14 +33,14 @@ contract PSM3 is IPSM { mapping(address user => uint256 shares) public shares; constructor(address asset0_, address asset1_, address asset2_, address rateProvider_) { - require(asset0_ != address(0), "PSM/invalid-asset0"); - require(asset1_ != address(0), "PSM/invalid-asset1"); - require(asset2_ != address(0), "PSM/invalid-asset2"); - require(rateProvider_ != address(0), "PSM/invalid-rateProvider"); + require(asset0_ != address(0), "PSM3/invalid-asset0"); + require(asset1_ != address(0), "PSM3/invalid-asset1"); + require(asset2_ != address(0), "PSM3/invalid-asset2"); + require(rateProvider_ != address(0), "PSM3/invalid-rateProvider"); - require(asset0_ != asset1_, "PSM/asset0-asset1-same"); - require(asset0_ != asset2_, "PSM/asset0-asset2-same"); - require(asset1_ != asset2_, "PSM/asset1-asset2-same"); + require(asset0_ != asset1_, "PSM3/asset0-asset1-same"); + require(asset0_ != asset2_, "PSM3/asset0-asset2-same"); + require(asset1_ != asset2_, "PSM3/asset1-asset2-same"); asset0 = IERC20(asset0_); asset1 = IERC20(asset1_); @@ -67,12 +67,12 @@ contract PSM3 is IPSM { ) external { - require(amountIn != 0, "PSM/invalid-amountIn"); - require(receiver != address(0), "PSM/invalid-receiver"); + require(amountIn != 0, "PSM3/invalid-amountIn"); + require(receiver != address(0), "PSM3/invalid-receiver"); uint256 amountOut = previewSwap(assetIn, assetOut, amountIn); - require(amountOut >= minAmountOut, "PSM/amountOut-too-low"); + require(amountOut >= minAmountOut, "PSM3/amountOut-too-low"); IERC20(assetIn).safeTransferFrom(msg.sender, address(this), amountIn); IERC20(assetOut).safeTransfer(receiver, amountOut); @@ -87,8 +87,8 @@ contract PSM3 is IPSM { function deposit(address asset, address receiver, uint256 assetsToDeposit, uint16 referralCode) external override returns (uint256 newShares) { - require(receiver != address(0), "PSM/invalid-receiver"); - require(assetsToDeposit != 0, "PSM/invalid-amount"); + require(receiver != address(0), "PSM3/invalid-receiver"); + require(assetsToDeposit != 0, "PSM3/invalid-amount"); newShares = previewDeposit(asset, assetsToDeposit); @@ -103,8 +103,8 @@ contract PSM3 is IPSM { function withdraw(address asset, address receiver, uint256 maxAssetsToWithdraw, uint16 referralCode) external override returns (uint256 assetsWithdrawn) { - require(receiver != address(0), "PSM/invalid-receiver"); - require(maxAssetsToWithdraw != 0, "PSM/invalid-amount"); + require(receiver != address(0), "PSM3/invalid-receiver"); + require(maxAssetsToWithdraw != 0, "PSM3/invalid-amount"); uint256 sharesToBurn; @@ -125,7 +125,7 @@ contract PSM3 is IPSM { /**********************************************************************************************/ function previewDeposit(address asset, uint256 assetsToDeposit) public view returns (uint256) { - require(_isValidAsset(asset), "PSM/invalid-asset"); + require(_isValidAsset(asset), "PSM3/invalid-asset"); // Convert amount to 1e18 precision denominated in value of asset0 then convert to shares. return convertToShares(_getAssetValue(asset, assetsToDeposit)); @@ -134,7 +134,7 @@ contract PSM3 is IPSM { function previewWithdraw(address asset, uint256 maxAssetsToWithdraw) public view override returns (uint256 sharesToBurn, uint256 assetsWithdrawn) { - require(_isValidAsset(asset), "PSM/invalid-asset"); + require(_isValidAsset(asset), "PSM3/invalid-asset"); uint256 assetBalance = IERC20(asset).balanceOf(address(this)); @@ -174,7 +174,7 @@ contract PSM3 is IPSM { else if (assetOut == address(asset1)) return _previewSwapFromAsset2(amountIn, _asset1Precision); } - revert("PSM/invalid-asset"); + revert("PSM3/invalid-asset"); } /**********************************************************************************************/ @@ -184,7 +184,7 @@ contract PSM3 is IPSM { function convertToAssets(address asset, uint256 numShares) public view override returns (uint256) { - require(_isValidAsset(asset), "PSM/invalid-asset"); + require(_isValidAsset(asset), "PSM3/invalid-asset"); uint256 assetValue = convertToAssetValue(numShares); @@ -216,7 +216,7 @@ contract PSM3 is IPSM { } function convertToShares(address asset, uint256 assets) public view override returns (uint256) { - require(_isValidAsset(asset), "PSM/invalid-asset"); + require(_isValidAsset(asset), "PSM3/invalid-asset"); return convertToShares(_getAssetValue(asset, assets)); } @@ -252,7 +252,7 @@ contract PSM3 is IPSM { if (asset == address(asset0)) return _getAsset0Value(amount); else if (asset == address(asset1)) return _getAsset1Value(amount); else if (asset == address(asset2)) return _getAsset2Value(amount); - else revert("PSM/invalid-asset"); + else revert("PSM3/invalid-asset"); } function _getAsset0Value(uint256 amount) internal view returns (uint256) { diff --git a/src/interfaces/IPSM.sol b/src/interfaces/IPSM3.sol similarity index 99% rename from src/interfaces/IPSM.sol rename to src/interfaces/IPSM3.sol index 5acc9f4..33e0d45 100644 --- a/src/interfaces/IPSM.sol +++ b/src/interfaces/IPSM3.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import { IERC20 } from "erc20-helpers/interfaces/IERC20.sol"; -interface IPSM { +interface IPSM3 { // TODO: Determine priority for indexing @@ -168,9 +168,9 @@ interface IPSM { * @return assetsWithdrawn Resulting amount of the asset withdrawn from the PSM. */ function withdraw( - address asset, + address asset, address receiver, - uint256 maxAssetsToWithdraw, + uint256 maxAssetsToWithdraw, uint16 referralCode ) external returns (uint256 assetsWithdrawn); diff --git a/test/Constructor.t.sol b/test/Constructor.t.sol index 848385e..1b3e22a 100644 --- a/test/Constructor.t.sol +++ b/test/Constructor.t.sol @@ -3,50 +3,50 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "../src/PSM.sol"; +import { PSM3 } from "../src/PSM3.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; contract PSMConstructorTests is PSMTestBase { function test_constructor_invalidAsset0() public { - vm.expectRevert("PSM/invalid-asset0"); - new PSM(address(0), address(usdc), address(sDai), address(rateProvider)); + vm.expectRevert("PSM3/invalid-asset0"); + new PSM3(address(0), address(usdc), address(sDai), address(rateProvider)); } function test_constructor_invalidAsset1() public { - vm.expectRevert("PSM/invalid-asset1"); - new PSM(address(dai), address(0), address(sDai), address(rateProvider)); + vm.expectRevert("PSM3/invalid-asset1"); + new PSM3(address(dai), address(0), address(sDai), address(rateProvider)); } function test_constructor_invalidAsset2() public { - vm.expectRevert("PSM/invalid-asset2"); - new PSM(address(dai), address(usdc), address(0), address(rateProvider)); + vm.expectRevert("PSM3/invalid-asset2"); + new PSM3(address(dai), address(usdc), address(0), address(rateProvider)); } function test_constructor_invalidRateProvider() public { - vm.expectRevert("PSM/invalid-rateProvider"); - new PSM(address(dai), address(usdc), address(sDai), address(0)); + vm.expectRevert("PSM3/invalid-rateProvider"); + new PSM3(address(dai), address(usdc), address(sDai), address(0)); } function test_constructor_asset0Asset1Match() public { - vm.expectRevert("PSM/asset0-asset1-same"); - new PSM(address(dai), address(dai), address(sDai), address(rateProvider)); + vm.expectRevert("PSM3/asset0-asset1-same"); + new PSM3(address(dai), address(dai), address(sDai), address(rateProvider)); } function test_constructor_asset0Asset2Match() public { - vm.expectRevert("PSM/asset0-asset2-same"); - new PSM(address(dai), address(usdc), address(dai), address(rateProvider)); + vm.expectRevert("PSM3/asset0-asset2-same"); + new PSM3(address(dai), address(usdc), address(dai), address(rateProvider)); } function test_constructor_asset1Asset2Match() public { - vm.expectRevert("PSM/asset1-asset2-same"); - new PSM(address(dai), address(usdc), address(usdc), address(rateProvider)); + vm.expectRevert("PSM3/asset1-asset2-same"); + new PSM3(address(dai), address(usdc), address(usdc), address(rateProvider)); } function test_constructor() public { // Deploy new PSM to get test coverage - psm = new PSM(address(dai), address(usdc), address(sDai), address(rateProvider)); + psm = new PSM3(address(dai), address(usdc), address(sDai), address(rateProvider)); assertEq(address(psm.asset0()), address(dai)); assertEq(address(psm.asset1()), address(usdc)); diff --git a/test/Conversions.t.sol b/test/Conversions.t.sol index 35a9374..90f2e89 100644 --- a/test/Conversions.t.sol +++ b/test/Conversions.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "../src/PSM.sol"; +import { PSM3 } from "../src/PSM3.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; @@ -12,7 +12,7 @@ import { PSMTestBase } from "test/PSMTestBase.sol"; contract PSMConvertToAssetsTests is PSMTestBase { function test_convertToAssets_invalidAsset() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.convertToAssets(makeAddr("new-asset"), 100); } @@ -153,7 +153,7 @@ contract PSMConvertToSharesTests is PSMTestBase { contract PSMConvertToSharesFailureTests is PSMTestBase { function test_convertToShares_invalidAsset() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.convertToShares(makeAddr("new-asset"), 100); } diff --git a/test/Deposit.t.sol b/test/Deposit.t.sol index 4d30980..87748b1 100644 --- a/test/Deposit.t.sol +++ b/test/Deposit.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "../src/PSM.sol"; +import { PSM3 } from "../src/PSM3.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; @@ -15,17 +15,17 @@ contract PSMDepositTests is PSMTestBase { address receiver2 = makeAddr("receiver2"); function test_deposit_zeroReceiver() public { - vm.expectRevert("PSM/invalid-receiver"); + vm.expectRevert("PSM3/invalid-receiver"); psm.deposit(address(usdc), address(0), 100e6, 0); } function test_deposit_zeroAmount() public { - vm.expectRevert("PSM/invalid-amount"); + vm.expectRevert("PSM3/invalid-amount"); psm.deposit(address(usdc), user1, 0, 0); } function test_deposit_notAsset0OrAsset1() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.deposit(makeAddr("new-asset"), user1, 100e6, 0); } diff --git a/test/Getters.t.sol b/test/Getters.t.sol index b5138d7..2146bea 100644 --- a/test/Getters.t.sol +++ b/test/Getters.t.sol @@ -5,15 +5,15 @@ import "forge-std/Test.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -import { PSMHarness } from "test/harnesses/PSMHarness.sol"; +import { PSM3Harness } from "test/harnesses/PSM3Harness.sol"; contract PSMHarnessTests is PSMTestBase { - PSMHarness psmHarness; + PSM3Harness psmHarness; function setUp() public override { super.setUp(); - psmHarness = new PSMHarness( + psmHarness = new PSM3Harness( address(dai), address(usdc), address(sDai), @@ -141,7 +141,7 @@ contract PSMHarnessTests is PSMTestBase { } function test_getAssetValue_zeroAddress() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psmHarness.getAssetValue(address(0), 1); } diff --git a/test/InflationAttack.t.sol b/test/InflationAttack.t.sol index 3e4735d..924200e 100644 --- a/test/InflationAttack.t.sol +++ b/test/InflationAttack.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "src/PSM.sol"; +import { PSM3 } from "src/PSM3.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; @@ -13,7 +13,7 @@ contract InflationAttackTests is PSMTestBase { // TODO: Decide if DAI test is needed function test_inflationAttack_noInitialDeposit() public { - psm = new PSM(address(dai), address(usdc), address(sDai), address(rateProvider)); + psm = new PSM3(address(dai), address(usdc), address(sDai), address(rateProvider)); address firstDepositor = makeAddr("firstDepositor"); address frontRunner = makeAddr("frontRunner"); @@ -60,7 +60,7 @@ contract InflationAttackTests is PSMTestBase { } function test_inflationAttack_useInitialDeposit() public { - psm = new PSM(address(dai), address(usdc), address(sDai), address(rateProvider)); + psm = new PSM3(address(dai), address(usdc), address(sDai), address(rateProvider)); address firstDepositor = makeAddr("firstDepositor"); address frontRunner = makeAddr("frontRunner"); diff --git a/test/PSMTestBase.sol b/test/PSMTestBase.sol index 51480b3..a13f47b 100644 --- a/test/PSMTestBase.sol +++ b/test/PSMTestBase.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "src/PSM.sol"; +import { PSM3 } from "src/PSM3.sol"; import { MockERC20 } from "erc20-helpers/MockERC20.sol"; @@ -11,7 +11,7 @@ import { MockRateProvider } from "test/mocks/MockRateProvider.sol"; contract PSMTestBase is Test { - PSM public psm; + PSM3 public psm; // NOTE: Using DAI, sDAI and USDC as example assets MockERC20 public dai; @@ -41,7 +41,7 @@ contract PSMTestBase is Test { // NOTE: Using 1.25 for easy two way conversions rateProvider.__setConversionRate(1.25e27); - psm = new PSM(address(dai), address(usdc), address(sDai), address(rateProvider)); + psm = new PSM3(address(dai), address(usdc), address(sDai), address(rateProvider)); vm.label(address(dai), "DAI"); vm.label(address(usdc), "USDC"); diff --git a/test/Previews.t.sol b/test/Previews.t.sol index 005b3a5..13d2557 100644 --- a/test/Previews.t.sol +++ b/test/Previews.t.sol @@ -8,27 +8,27 @@ import { PSMTestBase } from "test/PSMTestBase.sol"; contract PSMPreviewSwapFailureTests is PSMTestBase { function test_previewSwap_invalidAssetIn() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.previewSwap(makeAddr("other-token"), address(usdc), 1); } function test_previewSwap_invalidAssetOut() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.previewSwap(address(usdc), makeAddr("other-token"), 1); } function test_previewSwap_bothAsset0() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.previewSwap(address(dai), address(dai), 1); } function test_previewSwap_bothAsset1() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.previewSwap(address(usdc), address(usdc), 1); } function test_previewSwap_bothAsset2() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.previewSwap(address(sDai), address(sDai), 1); } diff --git a/test/Swaps.t.sol b/test/Swaps.t.sol index f8151d9..5db9680 100644 --- a/test/Swaps.t.sol +++ b/test/Swaps.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "../src/PSM.sol"; +import { PSM3 } from "../src/PSM3.sol"; import { MockERC20, PSMTestBase } from "test/PSMTestBase.sol"; @@ -21,37 +21,37 @@ contract PSMSwapFailureTests is PSMTestBase { } function test_swap_amountZero() public { - vm.expectRevert("PSM/invalid-amountIn"); + vm.expectRevert("PSM3/invalid-amountIn"); psm.swap(address(usdc), address(sDai), 0, 0, receiver, 0); } function test_swap_receiverZero() public { - vm.expectRevert("PSM/invalid-receiver"); + vm.expectRevert("PSM3/invalid-receiver"); psm.swap(address(usdc), address(sDai), 100e6, 80e18, address(0), 0); } function test_swap_invalid_assetIn() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.swap(makeAddr("other-token"), address(sDai), 100e6, 80e18, receiver, 0); } function test_swap_invalid_assetOut() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.swap(address(usdc), makeAddr("other-token"), 100e6, 80e18, receiver, 0); } function test_swap_bothAsset0() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.swap(address(dai), address(dai), 100e6, 80e18, receiver, 0); } function test_swap_bothAsset1() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.swap(address(usdc), address(usdc), 100e6, 80e18, receiver, 0); } function test_swap_bothAsset2() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.swap(address(sDai), address(sDai), 100e6, 80e18, receiver, 0); } @@ -66,7 +66,7 @@ contract PSMSwapFailureTests is PSMTestBase { assertEq(expectedAmountOut, 80e18); - vm.expectRevert("PSM/amountOut-too-low"); + vm.expectRevert("PSM3/amountOut-too-low"); psm.swap(address(usdc), address(sDai), 100e6, 80e18 + 1, receiver, 0); psm.swap(address(usdc), address(sDai), 100e6, 80e18, receiver, 0); diff --git a/test/Withdraw.t.sol b/test/Withdraw.t.sol index 72474e8..7185470 100644 --- a/test/Withdraw.t.sol +++ b/test/Withdraw.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM } from "../src/PSM.sol"; +import { PSM3 } from "../src/PSM3.sol"; import { MockERC20 } from "erc20-helpers/MockERC20.sol"; @@ -19,19 +19,19 @@ contract PSMWithdrawTests is PSMTestBase { function test_withdraw_zeroReceiver() public { _deposit(address(usdc), user1, 100e6); - vm.expectRevert("PSM/invalid-receiver"); + vm.expectRevert("PSM3/invalid-receiver"); psm.withdraw(address(usdc), address(0), 100e6, 0); } function test_withdraw_zeroAmount() public { _deposit(address(usdc), user1, 100e6); - vm.expectRevert("PSM/invalid-amount"); + vm.expectRevert("PSM3/invalid-amount"); psm.withdraw(address(usdc), receiver1, 0, 0); } function test_withdraw_notAsset0OrAsset1() public { - vm.expectRevert("PSM/invalid-asset"); + vm.expectRevert("PSM3/invalid-asset"); psm.withdraw(makeAddr("new-asset"), receiver1, 100e6, 0); } diff --git a/test/harnesses/PSMHarness.sol b/test/harnesses/PSM3Harness.sol similarity index 84% rename from test/harnesses/PSMHarness.sol rename to test/harnesses/PSM3Harness.sol index 74f3328..7e1a60b 100644 --- a/test/harnesses/PSMHarness.sol +++ b/test/harnesses/PSM3Harness.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import { PSM } from "src/PSM.sol"; +import { PSM3 } from "src/PSM3.sol"; -contract PSMHarness is PSM { +contract PSM3Harness is PSM3 { constructor(address asset0_, address asset1_, address asset2_, address rateProvider_) - PSM(asset0_, asset1_, asset2_, rateProvider_) {} + PSM3(asset0_, asset1_, asset2_, rateProvider_) {} function getAssetValue(address asset, uint256 amount) external view returns (uint256) { return _getAssetValue(asset, amount); From 81d2e3742d598f52968853856d134dd824171139 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 10 Jun 2024 16:07:09 -0400 Subject: [PATCH 55/96] fix: rm dup file, update toml --- foundry.toml | 4 ++++ test/unit/harnesses/PSMHarness.sol | 27 --------------------------- 2 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 test/unit/harnesses/PSMHarness.sol diff --git a/foundry.toml b/foundry.toml index 4963041..f6239f6 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,6 +9,10 @@ optimizer_runs = 200 [fuzz] runs = 1000 +[invariant] +runs = 10 +depth = 20 + # See more config options https://github.com/foundry-rs/foundry/tree/master/config remappings = [ diff --git a/test/unit/harnesses/PSMHarness.sol b/test/unit/harnesses/PSMHarness.sol deleted file mode 100644 index 7e1a60b..0000000 --- a/test/unit/harnesses/PSMHarness.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.13; - -import { PSM3 } from "src/PSM3.sol"; - -contract PSM3Harness is PSM3 { - - constructor(address asset0_, address asset1_, address asset2_, address rateProvider_) - PSM3(asset0_, asset1_, asset2_, rateProvider_) {} - - function getAssetValue(address asset, uint256 amount) external view returns (uint256) { - return _getAssetValue(asset, amount); - } - - function getAsset0Value(uint256 amount) external view returns (uint256) { - return _getAsset0Value(amount); - } - - function getAsset1Value(uint256 amount) external view returns (uint256) { - return _getAsset1Value(amount); - } - - function getAsset2Value(uint256 amount) external view returns (uint256) { - return _getAsset2Value(amount); - } - -} From fbff8d3452d8a835b4ba564b09209566fa94a53e Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 10 Jun 2024 16:27:13 -0400 Subject: [PATCH 56/96] feat: get deposits working --- .../invariant/{handlers => }/Invariants.t.sol | 9 +++ test/invariant/handlers/HandlerBase.sol | 66 +++++++++++++++++++ test/invariant/handlers/LPHandler.sol | 25 +++++++ 3 files changed, 100 insertions(+) rename test/invariant/{handlers => }/Invariants.t.sol (59%) create mode 100644 test/invariant/handlers/HandlerBase.sol create mode 100644 test/invariant/handlers/LPHandler.sol diff --git a/test/invariant/handlers/Invariants.t.sol b/test/invariant/Invariants.t.sol similarity index 59% rename from test/invariant/handlers/Invariants.t.sol rename to test/invariant/Invariants.t.sol index 5d758ed..be8ec5c 100644 --- a/test/invariant/handlers/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -7,13 +7,22 @@ import { PSM3 } from "src/PSM3.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; +import { LPHandler } from "test/invariant/handlers/HandlerBase.sol"; + contract PSMInvariantTests is PSMTestBase { + LPHandler public lpHandler; + function setUp() public override { super.setUp(); + + lpHandler = new LPHandler(psm, dai, usdc, sDai, 3); + + targetContract(address(lpHandler)); } function invariant_A() public { assertEq(true, true); + console.log("count", lpHandler.count()); } } diff --git a/test/invariant/handlers/HandlerBase.sol b/test/invariant/handlers/HandlerBase.sol new file mode 100644 index 0000000..d6e9e27 --- /dev/null +++ b/test/invariant/handlers/HandlerBase.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import { MockERC20 } from "erc20-helpers/MockERC20.sol"; + +import { CommonBase } from "forge-std/Base.sol"; +import { StdCheatsSafe } from "forge-std/StdCheats.sol"; +import { StdUtils } from "forge-std/StdUtils.sol"; + +import { PSM3 } from "src/PSM3.sol"; + +contract LPHandler is CommonBase, StdCheatsSafe, StdUtils { + + MockERC20[3] public assets; + + address[] public lps; + + PSM3 public psm; + + uint256 public count; + + constructor( + PSM3 psm_, + MockERC20 asset0, + MockERC20 asset1, + MockERC20 asset2, + uint256 lpCount + ) { + psm = psm_; + + assets[0] = asset0; + assets[1] = asset1; + assets[2] = asset2; + + for (uint256 i = 0; i < lpCount; i++) { + lps.push(makeAddr(string(abi.encodePacked("LP", i)))); + } + } + + function _getAsset(uint256 indexSeed) internal view returns (MockERC20) { + return assets[_bound(indexSeed, 0, 2)]; + } + + function _getLP(uint256 indexSeed) internal view returns (address) { + return lps[_bound(indexSeed, 0, lps.length - 1)]; + } + + function deposit(uint256 indexSeed, address user, uint256 amount) public { + MockERC20 asset = _getAsset(indexSeed); + address lp = _getLP(indexSeed); + + console.log("asset", address(asset)); + + amount = _bound(amount, 1, 1e18); // TODO: Change this to something dynamic + + vm.startPrank(user); + asset.mint(user, amount); + asset.approve(address(psm), amount); + psm.deposit(address(asset), lp, amount); + vm.stopPrank(); + + count++; + } +} diff --git a/test/invariant/handlers/LPHandler.sol b/test/invariant/handlers/LPHandler.sol new file mode 100644 index 0000000..0c077f3 --- /dev/null +++ b/test/invariant/handlers/LPHandler.sol @@ -0,0 +1,25 @@ +// // SPDX-License-Identifier: AGPL-3.0-or-later +// pragma solidity ^0.8.13; + +// import { MockERC20 } from "erc20-helpers/MockERC20.sol"; + +// contract LPHandler { + +// MockERC20 public asset0; +// MockERC20 public asset1; +// MockERC20 public asset2; + +// constructor(MockERC20 asset0_, MockERC20 asset1_, MockERC20 asset2_) { +// asset0 = asset0_; +// asset1 = asset1_; +// asset2 = asset2_; +// } + +// function deposit(address asset, address user, address receiver, uint256 amount) public { +// vm.startPrank(user); +// MockERC20(asset).mint(user, amount); +// MockERC20(asset).approve(address(psm), amount); +// psm.deposit(asset, receiver, amount); +// vm.stopPrank(); +// } +// } From 3e5a3eb24c0069e4a0a3fe9faec609de7d03939d Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 10 Jun 2024 16:32:43 -0400 Subject: [PATCH 57/96] chore: refactor into proper inheritance structure --- foundry.toml | 4 +- test/invariant/Invariants.t.sol | 5 +- test/invariant/handlers/HandlerBase.sol | 35 ++----------- test/invariant/handlers/LPHandler.sol | 70 ++++++++++++++++--------- 4 files changed, 55 insertions(+), 59 deletions(-) diff --git a/foundry.toml b/foundry.toml index f6239f6..b3a667c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,8 +10,8 @@ optimizer_runs = 200 runs = 1000 [invariant] -runs = 10 -depth = 20 +runs = 1 +depth = 10000 # See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index be8ec5c..76fa821 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -7,7 +7,7 @@ import { PSM3 } from "src/PSM3.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -import { LPHandler } from "test/invariant/handlers/HandlerBase.sol"; +import { LPHandler } from "test/invariant/handlers/LpHandler.sol"; contract PSMInvariantTests is PSMTestBase { @@ -24,5 +24,8 @@ contract PSMInvariantTests is PSMTestBase { function invariant_A() public { assertEq(true, true); console.log("count", lpHandler.count()); + console.log("lp1Shares", psm.shares(address(lpHandler.lps(0)))); + console.log("lp2Shares", psm.shares(address(lpHandler.lps(1)))); + console.log("lp3Shares", psm.shares(address(lpHandler.lps(2)))); } } diff --git a/test/invariant/handlers/HandlerBase.sol b/test/invariant/handlers/HandlerBase.sol index d6e9e27..f9fce36 100644 --- a/test/invariant/handlers/HandlerBase.sol +++ b/test/invariant/handlers/HandlerBase.sol @@ -11,56 +11,29 @@ import { StdUtils } from "forge-std/StdUtils.sol"; import { PSM3 } from "src/PSM3.sol"; -contract LPHandler is CommonBase, StdCheatsSafe, StdUtils { - - MockERC20[3] public assets; - - address[] public lps; +contract HandlerBase is CommonBase, StdCheatsSafe, StdUtils { PSM3 public psm; + MockERC20[3] public assets; + uint256 public count; constructor( PSM3 psm_, MockERC20 asset0, MockERC20 asset1, - MockERC20 asset2, - uint256 lpCount + MockERC20 asset2 ) { psm = psm_; assets[0] = asset0; assets[1] = asset1; assets[2] = asset2; - - for (uint256 i = 0; i < lpCount; i++) { - lps.push(makeAddr(string(abi.encodePacked("LP", i)))); - } } function _getAsset(uint256 indexSeed) internal view returns (MockERC20) { return assets[_bound(indexSeed, 0, 2)]; } - function _getLP(uint256 indexSeed) internal view returns (address) { - return lps[_bound(indexSeed, 0, lps.length - 1)]; - } - - function deposit(uint256 indexSeed, address user, uint256 amount) public { - MockERC20 asset = _getAsset(indexSeed); - address lp = _getLP(indexSeed); - - console.log("asset", address(asset)); - - amount = _bound(amount, 1, 1e18); // TODO: Change this to something dynamic - - vm.startPrank(user); - asset.mint(user, amount); - asset.approve(address(psm), amount); - psm.deposit(address(asset), lp, amount); - vm.stopPrank(); - - count++; - } } diff --git a/test/invariant/handlers/LPHandler.sol b/test/invariant/handlers/LPHandler.sol index 0c077f3..2507401 100644 --- a/test/invariant/handlers/LPHandler.sol +++ b/test/invariant/handlers/LPHandler.sol @@ -1,25 +1,45 @@ -// // SPDX-License-Identifier: AGPL-3.0-or-later -// pragma solidity ^0.8.13; - -// import { MockERC20 } from "erc20-helpers/MockERC20.sol"; - -// contract LPHandler { - -// MockERC20 public asset0; -// MockERC20 public asset1; -// MockERC20 public asset2; - -// constructor(MockERC20 asset0_, MockERC20 asset1_, MockERC20 asset2_) { -// asset0 = asset0_; -// asset1 = asset1_; -// asset2 = asset2_; -// } - -// function deposit(address asset, address user, address receiver, uint256 amount) public { -// vm.startPrank(user); -// MockERC20(asset).mint(user, amount); -// MockERC20(asset).approve(address(psm), amount); -// psm.deposit(asset, receiver, amount); -// vm.stopPrank(); -// } -// } +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { MockERC20 } from "erc20-helpers/MockERC20.sol"; + +import { HandlerBase } from "test/invariant/handlers/HandlerBase.sol"; + +import { PSM3 } from "src/PSM3.sol"; + +contract LPHandler is HandlerBase { + + address[] public lps; + + constructor( + PSM3 psm_, + MockERC20 asset0, + MockERC20 asset1, + MockERC20 asset2, + uint256 lpCount + ) HandlerBase(psm_, asset0, asset1, asset2) { + for (uint256 i = 0; i < lpCount; i++) { + lps.push(makeAddr(string(abi.encodePacked("LP", i)))); + } + } + + function _getLP(uint256 indexSeed) internal view returns (address) { + return lps[_bound(indexSeed, 0, lps.length - 1)]; + } + + function deposit(uint256 indexSeed, address user, uint256 amount) public { + MockERC20 asset = _getAsset(indexSeed); + address lp = _getLP(indexSeed); + + amount = _bound(amount, 1, 1e18); // TODO: Change this to something dynamic + + vm.startPrank(user); + asset.mint(user, amount); + asset.approve(address(psm), amount); + psm.deposit(address(asset), lp, amount); + vm.stopPrank(); + + count++; + } + +} From 2e6059378b3a04e13678bd741b421d8bb43d2293 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 10 Jun 2024 16:56:08 -0400 Subject: [PATCH 58/96] feat: get all functions working with reverts --- test/invariant/Invariants.t.sol | 25 ++++++++-- test/invariant/handlers/HandlerBase.sol | 4 ++ test/invariant/handlers/LPHandler.sol | 33 ++++++++++---- test/invariant/handlers/SwapperHandler.sol | 53 ++++++++++++++++++++++ 4 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 test/invariant/handlers/SwapperHandler.sol diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 76fa821..c569b4b 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -7,23 +7,38 @@ import { PSM3 } from "src/PSM3.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -import { LPHandler } from "test/invariant/handlers/LpHandler.sol"; +import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; +import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; contract PSMInvariantTests is PSMTestBase { - LPHandler public lpHandler; + LpHandler public lpHandler; + SwapperHandler public swapperHandler; function setUp() public override { super.setUp(); - lpHandler = new LPHandler(psm, dai, usdc, sDai, 3); + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); targetContract(address(lpHandler)); + targetContract(address(swapperHandler)); } function invariant_A() public { - assertEq(true, true); - console.log("count", lpHandler.count()); + assertEq( + psm.shares(address(lpHandler.lps(0))) + + psm.shares(address(lpHandler.lps(1))) + + psm.shares(address(lpHandler.lps(2))), + psm.totalShares() + ); + } + + function invariant_logs() public { + console.log("count1", lpHandler.count()); + console.log("count2", lpHandler.withdrawCount()); + console.log("count3", swapperHandler.count()); + console.log("lp1Shares", psm.shares(address(lpHandler.lps(0)))); console.log("lp2Shares", psm.shares(address(lpHandler.lps(1)))); console.log("lp3Shares", psm.shares(address(lpHandler.lps(2)))); diff --git a/test/invariant/handlers/HandlerBase.sol b/test/invariant/handlers/HandlerBase.sol index f9fce36..4e624e3 100644 --- a/test/invariant/handlers/HandlerBase.sol +++ b/test/invariant/handlers/HandlerBase.sol @@ -36,4 +36,8 @@ contract HandlerBase is CommonBase, StdCheatsSafe, StdUtils { return assets[_bound(indexSeed, 0, 2)]; } + function _hash(uint256 number_, string memory salt) internal pure returns (uint256 hash_) { + hash_ = uint256(keccak256(abi.encode(number_, salt))); + } + } diff --git a/test/invariant/handlers/LPHandler.sol b/test/invariant/handlers/LPHandler.sol index 2507401..6552dc8 100644 --- a/test/invariant/handlers/LPHandler.sol +++ b/test/invariant/handlers/LPHandler.sol @@ -7,10 +7,12 @@ import { HandlerBase } from "test/invariant/handlers/HandlerBase.sol"; import { PSM3 } from "src/PSM3.sol"; -contract LPHandler is HandlerBase { +contract LpHandler is HandlerBase { address[] public lps; + uint256 public withdrawCount; + constructor( PSM3 psm_, MockERC20 asset0, @@ -19,22 +21,22 @@ contract LPHandler is HandlerBase { uint256 lpCount ) HandlerBase(psm_, asset0, asset1, asset2) { for (uint256 i = 0; i < lpCount; i++) { - lps.push(makeAddr(string(abi.encodePacked("LP", i)))); + lps.push(makeAddr(string(abi.encodePacked("lp-", i)))); } } - function _getLP(uint256 indexSeed) internal view returns (address) { - return lps[_bound(indexSeed, 0, lps.length - 1)]; + function _getLP(uint256 lpSeed) internal view returns (address) { + return lps[_bound(lpSeed, 0, lps.length - 1)]; } - function deposit(uint256 indexSeed, address user, uint256 amount) public { - MockERC20 asset = _getAsset(indexSeed); - address lp = _getLP(indexSeed); + function deposit(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { + MockERC20 asset = _getAsset(assetSeed); + address lp = _getLP(lpSeed); amount = _bound(amount, 1, 1e18); // TODO: Change this to something dynamic - vm.startPrank(user); - asset.mint(user, amount); + vm.startPrank(lp); + asset.mint(lp, amount); asset.approve(address(psm), amount); psm.deposit(address(asset), lp, amount); vm.stopPrank(); @@ -42,4 +44,17 @@ contract LPHandler is HandlerBase { count++; } + function withdraw(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { + MockERC20 asset = _getAsset(assetSeed); + address lp = _getLP(lpSeed); + + amount = _bound(amount, 1, 1e18); // TODO: Change this to something dynamic + + vm.prank(lp); + psm.withdraw(address(asset), lp, amount); + vm.stopPrank(); + + withdrawCount++; + } + } diff --git a/test/invariant/handlers/SwapperHandler.sol b/test/invariant/handlers/SwapperHandler.sol new file mode 100644 index 0000000..e366cb8 --- /dev/null +++ b/test/invariant/handlers/SwapperHandler.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { MockERC20 } from "erc20-helpers/MockERC20.sol"; + +import { HandlerBase } from "test/invariant/handlers/HandlerBase.sol"; + +import { PSM3 } from "src/PSM3.sol"; + +contract SwapperHandler is HandlerBase { + + address[] public swappers; + + constructor( + PSM3 psm_, + MockERC20 asset0, + MockERC20 asset1, + MockERC20 asset2, + uint256 lpCount + ) HandlerBase(psm_, asset0, asset1, asset2) { + for (uint256 i = 0; i < lpCount; i++) { + swappers.push(makeAddr(string(abi.encodePacked("swapper-", i)))); + } + } + + function _getSwapper(uint256 indexSeed) internal view returns (address) { + return swappers[_bound(indexSeed, 0, swappers.length - 1)]; + } + + function swap( + uint256 assetInSeed, + uint256 assetOutSeed, + uint256 swapperSeed, + uint256 amount + ) + public + { + MockERC20 assetIn = _getAsset(assetInSeed); + MockERC20 assetOut = _getAsset(assetOutSeed); + address swapper = _getSwapper(swapperSeed); + + amount = _bound(amount, 1, 1); // TODO: Change this to calculate max amount out + + vm.startPrank(swapper); + assetIn.mint(swapper, amount); + assetIn.approve(address(psm), amount); + psm.swap(address(assetIn), address(assetOut), amount, 0, swapper, 0); // TODO: Update amountOut + vm.stopPrank(); + + count++; + } + +} From defc84ea8d270bad0bafbc8d9031eaa656c6513b Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 15:19:06 -0400 Subject: [PATCH 59/96] feat: update conversion --- test/unit/Conversions.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Conversions.t.sol b/test/unit/Conversions.t.sol index 67c9192..1b1c484 100644 --- a/test/unit/Conversions.t.sol +++ b/test/unit/Conversions.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM3 } from "../../src/PSM3.sol"; +import { PSM3 } from "src/PSM3.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; From bbc3e8e6666480f96f246c2124fe5abedddec9e3 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 15:57:50 -0400 Subject: [PATCH 60/96] feat: get swaps working without reverts --- foundry.toml | 2 +- test/invariant/Invariants.t.sol | 1 + test/invariant/handlers/LPHandler.sol | 18 +++++----- test/invariant/handlers/SwapperHandler.sol | 42 +++++++++++++++++++--- 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/foundry.toml b/foundry.toml index b3a667c..04b0e7f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -11,7 +11,7 @@ runs = 1000 [invariant] runs = 1 -depth = 10000 +depth = 1000 # See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index c569b4b..b653170 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -38,6 +38,7 @@ contract PSMInvariantTests is PSMTestBase { console.log("count1", lpHandler.count()); console.log("count2", lpHandler.withdrawCount()); console.log("count3", swapperHandler.count()); + console.log("count4", swapperHandler.zeroBalanceCount()); console.log("lp1Shares", psm.shares(address(lpHandler.lps(0)))); console.log("lp2Shares", psm.shares(address(lpHandler.lps(1)))); diff --git a/test/invariant/handlers/LPHandler.sol b/test/invariant/handlers/LPHandler.sol index 6552dc8..ed3e0d2 100644 --- a/test/invariant/handlers/LPHandler.sol +++ b/test/invariant/handlers/LPHandler.sol @@ -44,17 +44,17 @@ contract LpHandler is HandlerBase { count++; } - function withdraw(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { - MockERC20 asset = _getAsset(assetSeed); - address lp = _getLP(lpSeed); + // function withdraw(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { + // MockERC20 asset = _getAsset(assetSeed); + // address lp = _getLP(lpSeed); - amount = _bound(amount, 1, 1e18); // TODO: Change this to something dynamic + // amount = _bound(amount, 1, 1e18); // TODO: Change this to something dynamic - vm.prank(lp); - psm.withdraw(address(asset), lp, amount); - vm.stopPrank(); + // vm.prank(lp); + // psm.withdraw(address(asset), lp, amount); + // vm.stopPrank(); - withdrawCount++; - } + // withdrawCount++; + // } } diff --git a/test/invariant/handlers/SwapperHandler.sol b/test/invariant/handlers/SwapperHandler.sol index e366cb8..42c16cf 100644 --- a/test/invariant/handlers/SwapperHandler.sol +++ b/test/invariant/handlers/SwapperHandler.sol @@ -11,6 +11,8 @@ contract SwapperHandler is HandlerBase { address[] public swappers; + uint256 public zeroBalanceCount; + constructor( PSM3 psm_, MockERC20 asset0, @@ -31,20 +33,50 @@ contract SwapperHandler is HandlerBase { uint256 assetInSeed, uint256 assetOutSeed, uint256 swapperSeed, - uint256 amount + uint256 amountIn, + uint256 minAmountOut ) public { + // Prevent overflow in if statement below + assetOutSeed = _bound(assetOutSeed, 0, type(uint256).max - 2); + MockERC20 assetIn = _getAsset(assetInSeed); MockERC20 assetOut = _getAsset(assetOutSeed); address swapper = _getSwapper(swapperSeed); - amount = _bound(amount, 1, 1); // TODO: Change this to calculate max amount out + // Handle case where randomly selected assets match + if (assetIn == assetOut) { + assetOut = _getAsset(assetOutSeed + 2); + } + + // By calculating the amount of assetIn we can get from the max asset out, we can + // determine the max amount of assetIn we can swap since its the same both ways. + uint256 maxAmountIn = psm.previewSwap( + address(assetOut), + address(assetIn), + assetOut.balanceOf(address(psm) + )); + + // If there's zero balance a swap can't be performed + if (maxAmountIn == 0) { + zeroBalanceCount++; + return; + } + + amountIn = _bound(amountIn, 1, maxAmountIn); + + // Fuzz between zero and the expected amount out from the swap + minAmountOut = _bound( + minAmountOut, + 0, + psm.previewSwap(address(assetIn), address(assetOut), amountIn) + ); vm.startPrank(swapper); - assetIn.mint(swapper, amount); - assetIn.approve(address(psm), amount); - psm.swap(address(assetIn), address(assetOut), amount, 0, swapper, 0); // TODO: Update amountOut + assetIn.mint(swapper, amountIn); + assetIn.approve(address(psm), amountIn); + psm.swap(address(assetIn), address(assetOut), amountIn, minAmountOut, swapper, 0); vm.stopPrank(); count++; From 4c04863624c9c15670463d37f7152d4c16569250 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 16:12:10 -0400 Subject: [PATCH 61/96] feat: add fully working deposit/withdraw/swaps, invariant_B failing --- test/invariant/Invariants.t.sol | 43 +++++++++++++++++----- test/invariant/handlers/HandlerBase.sol | 2 - test/invariant/handlers/LPHandler.sol | 25 +++++++------ test/invariant/handlers/SwapperHandler.sol | 3 +- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index b653170..f6dca97 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -21,11 +21,14 @@ contract PSMInvariantTests is PSMTestBase { lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + // TODO: Add rate updates + rateProvider.__setConversionRate(1.25e27); + targetContract(address(lpHandler)); targetContract(address(swapperHandler)); } - function invariant_A() public { + function invariant_A() public view { assertEq( psm.shares(address(lpHandler.lps(0))) + psm.shares(address(lpHandler.lps(1))) + @@ -34,14 +37,36 @@ contract PSMInvariantTests is PSMTestBase { ); } - function invariant_logs() public { - console.log("count1", lpHandler.count()); - console.log("count2", lpHandler.withdrawCount()); - console.log("count3", swapperHandler.count()); - console.log("count4", swapperHandler.zeroBalanceCount()); + function invariant_B() public view { + // Assumes exchange rate above 1 for sDAI + assertGe( + psm.getPsmTotalValue(), + psm.totalShares() + ); + } - console.log("lp1Shares", psm.shares(address(lpHandler.lps(0)))); - console.log("lp2Shares", psm.shares(address(lpHandler.lps(1)))); - console.log("lp3Shares", psm.shares(address(lpHandler.lps(2)))); + function invariant_C() public view { + assertApproxEqAbs( + psm.convertToAssetValue(psm.shares(address(lpHandler.lps(0)))) + + psm.convertToAssetValue(psm.shares(address(lpHandler.lps(1)))) + + psm.convertToAssetValue(psm.shares(address(lpHandler.lps(2)))), + psm.getPsmTotalValue(), + 3 + ); } + + function invariant_logs() public view { + console.log("depositCount ", lpHandler.depositCount()); + console.log("withdrawCount ", lpHandler.withdrawCount()); + console.log("swapCount ", swapperHandler.swapCount()); + console.log("zeroBalanceCount", swapperHandler.zeroBalanceCount()); + console.log( + "sum ", + lpHandler.depositCount() + + lpHandler.withdrawCount() + + swapperHandler.swapCount() + + swapperHandler.zeroBalanceCount() + ); + } + } diff --git a/test/invariant/handlers/HandlerBase.sol b/test/invariant/handlers/HandlerBase.sol index 4e624e3..28c6bec 100644 --- a/test/invariant/handlers/HandlerBase.sol +++ b/test/invariant/handlers/HandlerBase.sol @@ -17,8 +17,6 @@ contract HandlerBase is CommonBase, StdCheatsSafe, StdUtils { MockERC20[3] public assets; - uint256 public count; - constructor( PSM3 psm_, MockERC20 asset0, diff --git a/test/invariant/handlers/LPHandler.sol b/test/invariant/handlers/LPHandler.sol index ed3e0d2..95ef3d8 100644 --- a/test/invariant/handlers/LPHandler.sol +++ b/test/invariant/handlers/LPHandler.sol @@ -11,8 +11,11 @@ contract LpHandler is HandlerBase { address[] public lps; + uint256 public depositCount; uint256 public withdrawCount; + uint256 public constant TRILLION = 1e10; + constructor( PSM3 psm_, MockERC20 asset0, @@ -33,7 +36,7 @@ contract LpHandler is HandlerBase { MockERC20 asset = _getAsset(assetSeed); address lp = _getLP(lpSeed); - amount = _bound(amount, 1, 1e18); // TODO: Change this to something dynamic + amount = _bound(amount, 1, TRILLION * 10 ** asset.decimals()); vm.startPrank(lp); asset.mint(lp, amount); @@ -41,20 +44,20 @@ contract LpHandler is HandlerBase { psm.deposit(address(asset), lp, amount); vm.stopPrank(); - count++; + depositCount++; } - // function withdraw(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { - // MockERC20 asset = _getAsset(assetSeed); - // address lp = _getLP(lpSeed); + function withdraw(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { + MockERC20 asset = _getAsset(assetSeed); + address lp = _getLP(lpSeed); - // amount = _bound(amount, 1, 1e18); // TODO: Change this to something dynamic + amount = _bound(amount, 1, TRILLION * 10 ** asset.decimals()); - // vm.prank(lp); - // psm.withdraw(address(asset), lp, amount); - // vm.stopPrank(); + vm.prank(lp); + psm.withdraw(address(asset), lp, amount); + vm.stopPrank(); - // withdrawCount++; - // } + withdrawCount++; + } } diff --git a/test/invariant/handlers/SwapperHandler.sol b/test/invariant/handlers/SwapperHandler.sol index 42c16cf..17b70ff 100644 --- a/test/invariant/handlers/SwapperHandler.sol +++ b/test/invariant/handlers/SwapperHandler.sol @@ -11,6 +11,7 @@ contract SwapperHandler is HandlerBase { address[] public swappers; + uint256 public swapCount; uint256 public zeroBalanceCount; constructor( @@ -79,7 +80,7 @@ contract SwapperHandler is HandlerBase { psm.swap(address(assetIn), address(assetOut), amountIn, minAmountOut, swapper, 0); vm.stopPrank(); - count++; + swapCount++; } } From d23bfbed936a129ce433f18cea80c5b67bab5745 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 16:19:47 -0400 Subject: [PATCH 62/96] ci: update for ci --- .github/workflows/{ci.yml => master.yml} | 5 +- .github/workflows/pr.yml | 87 ++++++++++++++++++++++++ foundry.toml | 8 ++- test/invariant/Invariants.t.sol | 9 +-- 4 files changed, 99 insertions(+), 10 deletions(-) rename .github/workflows/{ci.yml => master.yml} (98%) create mode 100644 .github/workflows/pr.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/master.yml similarity index 98% rename from .github/workflows/ci.yml rename to .github/workflows/master.yml index 579beed..7e28a84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/master.yml @@ -1,11 +1,8 @@ name: CI on: - workflow_dispatch: - pull_request: push: - branches: - - master + branches: [master] env: FOUNDRY_PROFILE: ci diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..3e1e73c --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,87 @@ +name: CI + +on: [pull_request] + +env: + FOUNDRY_PROFILE: ci + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Build contracts + run: | + forge --version + forge build --sizes + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Run tests + env: + MAINNET_RPC_URL: ${{secrets.MAINNET_RPC_URL}} + OPTIMISM_RPC_URL: ${{secrets.OPTIMISM_RPC_URL}} + ARBITRUM_ONE_RPC_URL: ${{secrets.ARBITRUM_ONE_RPC_URL}} + ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}} + GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}} + BASE_RPC_URL: ${{secrets.BASE_RPC_URL}} + run: forge test + + # coverage: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + + # - name: Install Foundry + # uses: foundry-rs/foundry-toolchain@v1 + + # - name: Run coverage + # env: + # MAINNET_RPC_URL: ${{secrets.MAINNET_RPC_URL}} + # OPTIMISM_RPC_URL: ${{secrets.OPTIMISM_RPC_URL}} + # ARBITRUM_ONE_RPC_URL: ${{secrets.ARBITRUM_ONE_RPC_URL}} + # ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}} + # GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}} + # BASE_RPC_URL: ${{secrets.BASE_RPC_URL}} + # run: forge coverage --report summary --report lcov + + # # To ignore coverage for certain directories modify the paths in this step as needed. The + # # below default ignores coverage results for the test and script directories. Alternatively, + # # to include coverage in all directories, comment out this step. Note that because this + # # filtering applies to the lcov file, the summary table generated in the previous step will + # # still include all files and directories. + # # The `--rc lcov_branch_coverage=1` part keeps branch info in the filtered report, since lcov + # # defaults to removing branch info. + # - name: Filter directories + # run: | + # sudo apt update && sudo apt install -y lcov + # lcov --remove lcov.info 'test/*' 'script/*' --output-file lcov.info --rc lcov_branch_coverage=1 + + # # This step posts a detailed coverage report as a comment and deletes previous comments on + # # each push. The below step is used to fail coverage if the specified coverage threshold is + # # not met. The below step can post a comment (when it's `github-token` is specified) but it's + # # not as useful, and this action cannot fail CI based on a minimum coverage threshold, which + # # is why we use both in this way. + # - name: Post coverage report + # if: github.event_name == 'pull_request' # This action fails when ran outside of a pull request. + # uses: romeovs/lcov-reporter-action@v0.3.1 + # with: + # delete-old-comments: true + # lcov-file: ./lcov.info + # github-token: ${{ secrets.GITHUB_TOKEN }} # Adds a coverage summary comment to the PR. + + # - name: Verify minimum coverage + # uses: zgosalvez/github-actions-report-lcov@v2 + # with: + # coverage-files: ./lcov.info + # minimum-coverage: 90 # Set coverage threshold. diff --git a/foundry.toml b/foundry.toml index 04b0e7f..bbcf9d3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,8 +10,12 @@ optimizer_runs = 200 runs = 1000 [invariant] -runs = 1 -depth = 1000 +runs = 20 +depth = 100 + +[ci.invariant] +runs = 200 +depth = 10000 # See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index f6dca97..912a5f3 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -39,10 +39,11 @@ contract PSMInvariantTests is PSMTestBase { function invariant_B() public view { // Assumes exchange rate above 1 for sDAI - assertGe( - psm.getPsmTotalValue(), - psm.totalShares() - ); + // Commenting out temporarily to avoid "Reason: invariant_B replay failure" in foundry + // assertGe( + // psm.getPsmTotalValue(), + // psm.totalShares() + // ); } function invariant_C() public view { From 7ba3e16b96d337f5eec43bba1b369ab9f98869d5 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 16:21:10 -0400 Subject: [PATCH 63/96] fix: update name --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index bbcf9d3..3dc0f89 100644 --- a/foundry.toml +++ b/foundry.toml @@ -13,7 +13,7 @@ runs = 1000 runs = 20 depth = 100 -[ci.invariant] +[profile.ci.invariant] runs = 200 depth = 10000 From 499afaed3352454f60f69bce30f18d859955eed1 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 16:23:28 -0400 Subject: [PATCH 64/96] chore: rm basly cased file --- test/invariant/handlers/LPHandler.sol | 63 --------------------------- 1 file changed, 63 deletions(-) delete mode 100644 test/invariant/handlers/LPHandler.sol diff --git a/test/invariant/handlers/LPHandler.sol b/test/invariant/handlers/LPHandler.sol deleted file mode 100644 index 95ef3d8..0000000 --- a/test/invariant/handlers/LPHandler.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.13; - -import { MockERC20 } from "erc20-helpers/MockERC20.sol"; - -import { HandlerBase } from "test/invariant/handlers/HandlerBase.sol"; - -import { PSM3 } from "src/PSM3.sol"; - -contract LpHandler is HandlerBase { - - address[] public lps; - - uint256 public depositCount; - uint256 public withdrawCount; - - uint256 public constant TRILLION = 1e10; - - constructor( - PSM3 psm_, - MockERC20 asset0, - MockERC20 asset1, - MockERC20 asset2, - uint256 lpCount - ) HandlerBase(psm_, asset0, asset1, asset2) { - for (uint256 i = 0; i < lpCount; i++) { - lps.push(makeAddr(string(abi.encodePacked("lp-", i)))); - } - } - - function _getLP(uint256 lpSeed) internal view returns (address) { - return lps[_bound(lpSeed, 0, lps.length - 1)]; - } - - function deposit(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { - MockERC20 asset = _getAsset(assetSeed); - address lp = _getLP(lpSeed); - - amount = _bound(amount, 1, TRILLION * 10 ** asset.decimals()); - - vm.startPrank(lp); - asset.mint(lp, amount); - asset.approve(address(psm), amount); - psm.deposit(address(asset), lp, amount); - vm.stopPrank(); - - depositCount++; - } - - function withdraw(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { - MockERC20 asset = _getAsset(assetSeed); - address lp = _getLP(lpSeed); - - amount = _bound(amount, 1, TRILLION * 10 ** asset.decimals()); - - vm.prank(lp); - psm.withdraw(address(asset), lp, amount); - vm.stopPrank(); - - withdrawCount++; - } - -} From d1daf4bb4d8911d835ee254be95093cafc6a3f81 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 16:23:45 -0400 Subject: [PATCH 65/96] chore: re add --- test/invariant/handlers/LpHandler.sol | 63 +++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 test/invariant/handlers/LpHandler.sol diff --git a/test/invariant/handlers/LpHandler.sol b/test/invariant/handlers/LpHandler.sol new file mode 100644 index 0000000..95ef3d8 --- /dev/null +++ b/test/invariant/handlers/LpHandler.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { MockERC20 } from "erc20-helpers/MockERC20.sol"; + +import { HandlerBase } from "test/invariant/handlers/HandlerBase.sol"; + +import { PSM3 } from "src/PSM3.sol"; + +contract LpHandler is HandlerBase { + + address[] public lps; + + uint256 public depositCount; + uint256 public withdrawCount; + + uint256 public constant TRILLION = 1e10; + + constructor( + PSM3 psm_, + MockERC20 asset0, + MockERC20 asset1, + MockERC20 asset2, + uint256 lpCount + ) HandlerBase(psm_, asset0, asset1, asset2) { + for (uint256 i = 0; i < lpCount; i++) { + lps.push(makeAddr(string(abi.encodePacked("lp-", i)))); + } + } + + function _getLP(uint256 lpSeed) internal view returns (address) { + return lps[_bound(lpSeed, 0, lps.length - 1)]; + } + + function deposit(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { + MockERC20 asset = _getAsset(assetSeed); + address lp = _getLP(lpSeed); + + amount = _bound(amount, 1, TRILLION * 10 ** asset.decimals()); + + vm.startPrank(lp); + asset.mint(lp, amount); + asset.approve(address(psm), amount); + psm.deposit(address(asset), lp, amount); + vm.stopPrank(); + + depositCount++; + } + + function withdraw(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { + MockERC20 asset = _getAsset(assetSeed); + address lp = _getLP(lpSeed); + + amount = _bound(amount, 1, TRILLION * 10 ** asset.decimals()); + + vm.prank(lp); + psm.withdraw(address(asset), lp, amount); + vm.stopPrank(); + + withdrawCount++; + } + +} From 84c378746c7564bf88f9072e7bee9628fe8a770d Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 16:25:14 -0400 Subject: [PATCH 66/96] fix: re add invariant --- test/invariant/Invariants.t.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 912a5f3..f6dca97 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -39,11 +39,10 @@ contract PSMInvariantTests is PSMTestBase { function invariant_B() public view { // Assumes exchange rate above 1 for sDAI - // Commenting out temporarily to avoid "Reason: invariant_B replay failure" in foundry - // assertGe( - // psm.getPsmTotalValue(), - // psm.totalShares() - // ); + assertGe( + psm.getPsmTotalValue(), + psm.totalShares() + ); } function invariant_C() public view { From 3814067f2a22946aacdf39412a30d7766c22eff4 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 16:27:44 -0400 Subject: [PATCH 67/96] ci: experiment with 2 million total calls --- .github/workflows/pr.yml | 2 +- foundry.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 3e1e73c..cede3a6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -35,7 +35,7 @@ jobs: ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}} GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}} BASE_RPC_URL: ${{secrets.BASE_RPC_URL}} - run: forge test + run: FOUNDRY_PROFILE=ci forge test # coverage: # runs-on: ubuntu-latest diff --git a/foundry.toml b/foundry.toml index 3dc0f89..a5b20cd 100644 --- a/foundry.toml +++ b/foundry.toml @@ -15,7 +15,7 @@ depth = 100 [profile.ci.invariant] runs = 200 -depth = 10000 +depth = 10_000 # See more config options https://github.com/foundry-rs/foundry/tree/master/config From 9215a296b96343b8900ff4fe82b40e513ed4fbb4 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 16:40:36 -0400 Subject: [PATCH 68/96] ci: add show progress flag --- .github/workflows/pr.yml | 2 +- foundry.toml | 2 +- test/invariant/Invariants.t.sol | 2 +- test/invariant/{handlers => }/LpHandler.sol | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename test/invariant/{handlers => }/LpHandler.sol (100%) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index cede3a6..6e7c20b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -35,7 +35,7 @@ jobs: ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}} GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}} BASE_RPC_URL: ${{secrets.BASE_RPC_URL}} - run: FOUNDRY_PROFILE=ci forge test + run: FOUNDRY_PROFILE=ci forge test --show-progress # coverage: # runs-on: ubuntu-latest diff --git a/foundry.toml b/foundry.toml index a5b20cd..6bcc537 100644 --- a/foundry.toml +++ b/foundry.toml @@ -11,7 +11,7 @@ runs = 1000 [invariant] runs = 20 -depth = 100 +depth = 1000 [profile.ci.invariant] runs = 200 diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index f6dca97..cba6d87 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -40,7 +40,7 @@ contract PSMInvariantTests is PSMTestBase { function invariant_B() public view { // Assumes exchange rate above 1 for sDAI assertGe( - psm.getPsmTotalValue(), + psm.getPsmTotalValue() + 1, // Make this adjustment to allow a negative tolerance of 1 psm.totalShares() ); } diff --git a/test/invariant/handlers/LpHandler.sol b/test/invariant/LpHandler.sol similarity index 100% rename from test/invariant/handlers/LpHandler.sol rename to test/invariant/LpHandler.sol From 9caf3c697464ce24d8a4ebe64a7c046710b8ee42 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 16:41:49 -0400 Subject: [PATCH 69/96] fix: move file back --- test/invariant/{ => handlers}/LpHandler.sol | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/invariant/{ => handlers}/LpHandler.sol (100%) diff --git a/test/invariant/LpHandler.sol b/test/invariant/handlers/LpHandler.sol similarity index 100% rename from test/invariant/LpHandler.sol rename to test/invariant/handlers/LpHandler.sol From 094d61359a9a5a74c4482fad8ae180d92ba3d286 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 16:43:14 -0400 Subject: [PATCH 70/96] ci: update verbosity --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 6e7c20b..9eff9d8 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -35,7 +35,7 @@ jobs: ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}} GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}} BASE_RPC_URL: ${{secrets.BASE_RPC_URL}} - run: FOUNDRY_PROFILE=ci forge test --show-progress + run: FOUNDRY_PROFILE=ci forge test -vv --show-progress # coverage: # runs-on: ubuntu-latest From 20853606b87c71f76c4d6db25e7ecf7532e34ca8 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 16:49:46 -0400 Subject: [PATCH 71/96] ci: add PR profile --- .github/workflows/master.yml | 2 +- .github/workflows/pr.yml | 2 +- foundry.toml | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 7e28a84..726353e 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -37,7 +37,7 @@ jobs: ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}} GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}} BASE_RPC_URL: ${{secrets.BASE_RPC_URL}} - run: FOUNDRY_PROFILE=ci forge test + run: FOUNDRY_PROFILE=master forge test -vv --show-progress # coverage: # runs-on: ubuntu-latest diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9eff9d8..5614607 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -35,7 +35,7 @@ jobs: ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}} GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}} BASE_RPC_URL: ${{secrets.BASE_RPC_URL}} - run: FOUNDRY_PROFILE=ci forge test -vv --show-progress + run: FOUNDRY_PROFILE=pr forge test -vv --show-progress # coverage: # runs-on: ubuntu-latest diff --git a/foundry.toml b/foundry.toml index 6bcc537..da4ae9b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -13,7 +13,11 @@ runs = 1000 runs = 20 depth = 1000 -[profile.ci.invariant] +[profile.pr.invariant] +runs = 200 +depth = 1000 + +[profile.master.invariant] runs = 200 depth = 10_000 From 06055865c0f44e6a1cb0b7225a4233b65178154b Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 13 Jun 2024 16:50:11 -0400 Subject: [PATCH 72/96] fix: rm redundant files --- test/Constructor.t.sol | 57 ------- test/Deposit.t.sol | 333 ----------------------------------------- test/Events.t.sol | 115 -------------- 3 files changed, 505 deletions(-) delete mode 100644 test/Constructor.t.sol delete mode 100644 test/Deposit.t.sol delete mode 100644 test/Events.t.sol diff --git a/test/Constructor.t.sol b/test/Constructor.t.sol deleted file mode 100644 index 1b3e22a..0000000 --- a/test/Constructor.t.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; - -import { PSM3 } from "../src/PSM3.sol"; - -import { PSMTestBase } from "test/PSMTestBase.sol"; - -contract PSMConstructorTests is PSMTestBase { - - function test_constructor_invalidAsset0() public { - vm.expectRevert("PSM3/invalid-asset0"); - new PSM3(address(0), address(usdc), address(sDai), address(rateProvider)); - } - - function test_constructor_invalidAsset1() public { - vm.expectRevert("PSM3/invalid-asset1"); - new PSM3(address(dai), address(0), address(sDai), address(rateProvider)); - } - - function test_constructor_invalidAsset2() public { - vm.expectRevert("PSM3/invalid-asset2"); - new PSM3(address(dai), address(usdc), address(0), address(rateProvider)); - } - - function test_constructor_invalidRateProvider() public { - vm.expectRevert("PSM3/invalid-rateProvider"); - new PSM3(address(dai), address(usdc), address(sDai), address(0)); - } - - function test_constructor_asset0Asset1Match() public { - vm.expectRevert("PSM3/asset0-asset1-same"); - new PSM3(address(dai), address(dai), address(sDai), address(rateProvider)); - } - - function test_constructor_asset0Asset2Match() public { - vm.expectRevert("PSM3/asset0-asset2-same"); - new PSM3(address(dai), address(usdc), address(dai), address(rateProvider)); - } - - function test_constructor_asset1Asset2Match() public { - vm.expectRevert("PSM3/asset1-asset2-same"); - new PSM3(address(dai), address(usdc), address(usdc), address(rateProvider)); - } - - function test_constructor() public { - // Deploy new PSM to get test coverage - psm = new PSM3(address(dai), address(usdc), address(sDai), address(rateProvider)); - - assertEq(address(psm.asset0()), address(dai)); - assertEq(address(psm.asset1()), address(usdc)); - assertEq(address(psm.asset2()), address(sDai)); - assertEq(address(psm.rateProvider()), address(rateProvider)); - } - -} diff --git a/test/Deposit.t.sol b/test/Deposit.t.sol deleted file mode 100644 index 0876ced..0000000 --- a/test/Deposit.t.sol +++ /dev/null @@ -1,333 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; - -import { PSM3 } from "../src/PSM3.sol"; - -import { PSMTestBase } from "test/PSMTestBase.sol"; - -contract PSMDepositTests is PSMTestBase { - - address user1 = makeAddr("user1"); - address user2 = makeAddr("user2"); - address receiver1 = makeAddr("receiver1"); - address receiver2 = makeAddr("receiver2"); - - function test_deposit_zeroReceiver() public { - vm.expectRevert("PSM3/invalid-receiver"); - psm.deposit(address(usdc), address(0), 100e6); - } - - function test_deposit_zeroAmount() public { - vm.expectRevert("PSM3/invalid-amount"); - psm.deposit(address(usdc), user1, 0); - } - - function test_deposit_notAsset0OrAsset1() public { - vm.expectRevert("PSM3/invalid-asset"); - psm.deposit(makeAddr("new-asset"), user1, 100e6); - } - - function test_deposit_insufficientApproveBoundary() public { - dai.mint(user1, 100e18); - - vm.startPrank(user1); - - dai.approve(address(psm), 100e18 - 1); - - vm.expectRevert("SafeERC20/transfer-from-failed"); - psm.deposit(address(dai), user1, 100e18); - - dai.approve(address(psm), 100e18); - - psm.deposit(address(dai), user1, 100e18); - } - - function test_deposit_insufficientBalanceBoundary() public { - dai.mint(user1, 100e18 - 1); - - vm.startPrank(user1); - - dai.approve(address(psm), 100e18); - - vm.expectRevert("SafeERC20/transfer-from-failed"); - psm.deposit(address(dai), user1, 100e18); - - dai.mint(user1, 1); - - psm.deposit(address(dai), user1, 100e18); - } - - function test_deposit_firstDepositDai() public { - dai.mint(user1, 100e18); - - vm.startPrank(user1); - - dai.approve(address(psm), 100e18); - - assertEq(dai.allowance(user1, address(psm)), 100e18); - assertEq(dai.balanceOf(user1), 100e18); - assertEq(dai.balanceOf(address(psm)), 0); - - assertEq(psm.totalShares(), 0); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver1), 0); - - assertEq(psm.convertToShares(1e18), 1e18); - - uint256 newShares = psm.deposit(address(dai), receiver1, 100e18); - - assertEq(newShares, 100e18); - - assertEq(dai.allowance(user1, address(psm)), 0); - assertEq(dai.balanceOf(user1), 0); - assertEq(dai.balanceOf(address(psm)), 100e18); - - assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver1), 100e18); - - assertEq(psm.convertToShares(1e18), 1e18); - } - - function test_deposit_firstDepositUsdc() public { - usdc.mint(user1, 100e6); - - vm.startPrank(user1); - - usdc.approve(address(psm), 100e6); - - assertEq(usdc.allowance(user1, address(psm)), 100e6); - assertEq(usdc.balanceOf(user1), 100e6); - assertEq(usdc.balanceOf(address(psm)), 0); - - assertEq(psm.totalShares(), 0); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver1), 0); - - assertEq(psm.convertToShares(1e18), 1e18); - - uint256 newShares = psm.deposit(address(usdc), receiver1, 100e6); - - assertEq(newShares, 100e18); - - assertEq(usdc.allowance(user1, address(psm)), 0); - assertEq(usdc.balanceOf(user1), 0); - assertEq(usdc.balanceOf(address(psm)), 100e6); - - assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver1), 100e18); - - assertEq(psm.convertToShares(1e18), 1e18); - } - - function test_deposit_firstDepositSDai() public { - sDai.mint(user1, 100e18); - - vm.startPrank(user1); - - sDai.approve(address(psm), 100e18); - - assertEq(sDai.allowance(user1, address(psm)), 100e18); - assertEq(sDai.balanceOf(user1), 100e18); - assertEq(sDai.balanceOf(address(psm)), 0); - - assertEq(psm.totalShares(), 0); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver1), 0); - - assertEq(psm.convertToShares(1e18), 1e18); - - uint256 newShares = psm.deposit(address(sDai), receiver1, 100e18); - - assertEq(newShares, 125e18); - - assertEq(sDai.allowance(user1, address(psm)), 0); - assertEq(sDai.balanceOf(user1), 0); - assertEq(sDai.balanceOf(address(psm)), 100e18); - - assertEq(psm.totalShares(), 125e18); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver1), 125e18); - - assertEq(psm.convertToShares(1e18), 1e18); - } - - function test_deposit_usdcThenSDai() public { - usdc.mint(user1, 100e6); - - vm.startPrank(user1); - - usdc.approve(address(psm), 100e6); - - uint256 newShares = psm.deposit(address(usdc), receiver1, 100e6); - - assertEq(newShares, 100e18); - - sDai.mint(user1, 100e18); - sDai.approve(address(psm), 100e18); - - assertEq(usdc.balanceOf(address(psm)), 100e6); - - assertEq(sDai.allowance(user1, address(psm)), 100e18); - assertEq(sDai.balanceOf(user1), 100e18); - assertEq(sDai.balanceOf(address(psm)), 0); - - assertEq(psm.totalShares(), 100e18); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver1), 100e18); - - assertEq(psm.convertToShares(1e18), 1e18); - - newShares = psm.deposit(address(sDai), receiver1, 100e18); - - assertEq(newShares, 125e18); - - assertEq(usdc.balanceOf(address(psm)), 100e6); - - assertEq(sDai.allowance(user1, address(psm)), 0); - assertEq(sDai.balanceOf(user1), 0); - assertEq(sDai.balanceOf(address(psm)), 100e18); - - assertEq(psm.totalShares(), 225e18); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver1), 225e18); - - assertEq(psm.convertToShares(1e18), 1e18); - } - - function testFuzz_deposit_usdcThenSDai(uint256 usdcAmount, uint256 sDaiAmount) public { - // Zero amounts revert - usdcAmount = _bound(usdcAmount, 1, USDC_TOKEN_MAX); - sDaiAmount = _bound(sDaiAmount, 1, SDAI_TOKEN_MAX); - - usdc.mint(user1, usdcAmount); - - vm.startPrank(user1); - - usdc.approve(address(psm), usdcAmount); - - uint256 newShares = psm.deposit(address(usdc), receiver1, usdcAmount); - - assertEq(newShares, usdcAmount * 1e12); - - sDai.mint(user1, sDaiAmount); - sDai.approve(address(psm), sDaiAmount); - - assertEq(usdc.balanceOf(address(psm)), usdcAmount); - - assertEq(sDai.allowance(user1, address(psm)), sDaiAmount); - assertEq(sDai.balanceOf(user1), sDaiAmount); - assertEq(sDai.balanceOf(address(psm)), 0); - - assertEq(psm.totalShares(), usdcAmount * 1e12); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver1), usdcAmount * 1e12); - - assertEq(psm.convertToShares(1e18), 1e18); - - newShares = psm.deposit(address(sDai), receiver1, sDaiAmount); - - assertEq(newShares, sDaiAmount * 125/100); - - assertEq(usdc.balanceOf(address(psm)), usdcAmount); - - assertEq(sDai.allowance(user1, address(psm)), 0); - assertEq(sDai.balanceOf(user1), 0); - assertEq(sDai.balanceOf(address(psm)), sDaiAmount); - - assertEq(psm.totalShares(), usdcAmount * 1e12 + sDaiAmount * 125/100); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver1), usdcAmount * 1e12 + sDaiAmount * 125/100); - - assertEq(psm.convertToShares(1e18), 1e18); - } - - function test_deposit_multiUser_changeConversionRate() public { - usdc.mint(user1, 100e6); - - vm.startPrank(user1); - - usdc.approve(address(psm), 100e6); - - uint256 newShares = psm.deposit(address(usdc), receiver1, 100e6); - - assertEq(newShares, 100e18); - - sDai.mint(user1, 100e18); - sDai.approve(address(psm), 100e18); - - newShares = psm.deposit(address(sDai), receiver1, 100e18); - - assertEq(newShares, 125e18); - - vm.stopPrank(); - - assertEq(usdc.balanceOf(address(psm)), 100e6); - - assertEq(sDai.allowance(user1, address(psm)), 0); - assertEq(sDai.balanceOf(user1), 0); - assertEq(sDai.balanceOf(address(psm)), 100e18); - - assertEq(psm.totalShares(), 225e18); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(receiver1), 225e18); - - assertEq(psm.convertToShares(1e18), 1e18); - - assertEq(psm.convertToAssetValue(psm.shares(receiver1)), 225e18); - - rateProvider.__setConversionRate(1.5e27); - - // Total shares / (100 USDC + 150 sDAI value) - uint256 expectedConversionRate = 225 * 1e18 / 250; - - assertEq(expectedConversionRate, 0.9e18); - - assertEq(psm.convertToShares(1e18), expectedConversionRate); - - vm.startPrank(user2); - - sDai.mint(user2, 100e18); - sDai.approve(address(psm), 100e18); - - assertEq(sDai.allowance(user2, address(psm)), 100e18); - assertEq(sDai.balanceOf(user2), 100e18); - assertEq(sDai.balanceOf(address(psm)), 100e18); - - assertEq(psm.convertToAssetValue(psm.shares(receiver1)), 250e18); - assertEq(psm.convertToAssetValue(psm.shares(receiver2)), 0); - - assertEq(psm.getPsmTotalValue(), 250e18); - - newShares = psm.deposit(address(sDai), receiver2, 100e18); - - assertEq(newShares, 135e18); - - assertEq(sDai.allowance(user2, address(psm)), 0); - assertEq(sDai.balanceOf(user2), 0); - assertEq(sDai.balanceOf(address(psm)), 200e18); - - // Depositing 150 dollars of value at 0.9 exchange rate - uint256 expectedShares = 150e18 * 9/10; - - assertEq(expectedShares, 135e18); - - assertEq(psm.totalShares(), 360e18); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(user2), 0); - assertEq(psm.shares(receiver1), 225e18); - assertEq(psm.shares(receiver2), 135e18); - - // Receiver 1 earned $25 on 225, Receiver 2 has earned nothing - assertEq(psm.convertToAssetValue(psm.shares(receiver1)), 250e18); - assertEq(psm.convertToAssetValue(psm.shares(receiver2)), 150e18); - - assertEq(psm.getPsmTotalValue(), 400e18); - } - - // TODO: Add fuzz test - -} diff --git a/test/Events.t.sol b/test/Events.t.sol deleted file mode 100644 index 853b576..0000000 --- a/test/Events.t.sol +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; - -import { MockERC20, PSMTestBase } from "test/PSMTestBase.sol"; - -contract PSMEventTests is PSMTestBase { - - event Swap( - address indexed assetIn, - address indexed assetOut, - address sender, - address indexed receiver, - uint256 amountIn, - uint256 amountOut, - uint256 referralCode - ); - - event Deposit( - address indexed asset, - address indexed user, - address indexed receiver, - uint256 assetsDeposited, - uint256 sharesMinted - ); - - event Withdraw( - address indexed asset, - address indexed user, - address indexed receiver, - uint256 assetsWithdrawn, - uint256 sharesBurned - ); - - address sender = makeAddr("sender"); - address receiver = makeAddr("receiver"); - - function test_deposit_events() public { - vm.startPrank(sender); - - dai.mint(sender, 100e18); - dai.approve(address(psm), 100e18); - - vm.expectEmit(address(psm)); - emit Deposit(address(dai), sender, receiver, 100e18, 100e18); - psm.deposit(address(dai), receiver, 100e18); - - usdc.mint(sender, 100e6); - usdc.approve(address(psm), 100e6); - - vm.expectEmit(address(psm)); - emit Deposit(address(usdc), sender, receiver, 100e6, 100e18); - psm.deposit(address(usdc), receiver, 100e6); - - sDai.mint(sender, 100e18); - sDai.approve(address(psm), 100e18); - - vm.expectEmit(address(psm)); - emit Deposit(address(sDai), sender, receiver, 100e18, 125e18); - psm.deposit(address(sDai), receiver, 100e18); - } - - function test_withdraw_events() public { - _deposit(address(dai), sender, 100e18); - _deposit(address(usdc), sender, 100e6); - _deposit(address(sDai), sender, 100e18); - - vm.startPrank(sender); - - vm.expectEmit(address(psm)); - emit Withdraw(address(dai), sender, receiver, 100e18, 100e18); - psm.withdraw(address(dai), receiver, 100e18); - - vm.expectEmit(address(psm)); - emit Withdraw(address(usdc), sender, receiver, 100e6, 100e18); - psm.withdraw(address(usdc), receiver, 100e6); - - vm.expectEmit(address(psm)); - emit Withdraw(address(sDai), sender, receiver, 100e18, 125e18); - psm.withdraw(address(sDai), receiver, 100e18); - } - - function test_swap_events() public { - dai.mint(address(psm), 1000e18); - usdc.mint(address(psm), 1000e6); - sDai.mint(address(psm), 1000e18); - - vm.startPrank(sender); - - _swapEventTest(address(dai), address(usdc), 100e18, 100e6, 1); - _swapEventTest(address(dai), address(sDai), 100e18, 80e18, 2); - - _swapEventTest(address(usdc), address(dai), 100e6, 100e18, 3); - _swapEventTest(address(usdc), address(sDai), 100e6, 80e18, 4); - - _swapEventTest(address(sDai), address(dai), 100e18, 125e18, 5); - _swapEventTest(address(sDai), address(usdc), 100e18, 125e6, 6); - } - - function _swapEventTest( - address assetIn, - address assetOut, - uint256 amountIn, - uint256 expectedAmountOut, - uint16 referralCode - ) internal { - MockERC20(assetIn).mint(sender, amountIn); - MockERC20(assetIn).approve(address(psm), amountIn); - - vm.expectEmit(address(psm)); - emit Swap(assetIn, assetOut, sender, receiver, amountIn, expectedAmountOut, referralCode); - psm.swap(assetIn, assetOut, amountIn, 0, receiver, referralCode); - } -} From 74dba88f45542a3c94b009a5758c250085563549 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 14 Jun 2024 09:14:15 -0400 Subject: [PATCH 73/96] feat: update from review changes --- test/invariant/Invariants.t.sol | 2 -- test/invariant/handlers/HandlerBase.sol | 4 +--- test/invariant/handlers/LpHandler.sol | 8 ++++---- test/invariant/handlers/SwapperHandler.sol | 8 ++++---- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index cba6d87..d41ffa2 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -3,8 +3,6 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSM3 } from "src/PSM3.sol"; - import { PSMTestBase } from "test/PSMTestBase.sol"; import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; diff --git a/test/invariant/handlers/HandlerBase.sol b/test/invariant/handlers/HandlerBase.sol index 28c6bec..fbc8dff 100644 --- a/test/invariant/handlers/HandlerBase.sol +++ b/test/invariant/handlers/HandlerBase.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import "forge-std/Test.sol"; - import { MockERC20 } from "erc20-helpers/MockERC20.sol"; import { CommonBase } from "forge-std/Base.sol"; @@ -31,7 +29,7 @@ contract HandlerBase is CommonBase, StdCheatsSafe, StdUtils { } function _getAsset(uint256 indexSeed) internal view returns (MockERC20) { - return assets[_bound(indexSeed, 0, 2)]; + return assets[indexSeed % assets.length]; } function _hash(uint256 number_, string memory salt) internal pure returns (uint256 hash_) { diff --git a/test/invariant/handlers/LpHandler.sol b/test/invariant/handlers/LpHandler.sol index 95ef3d8..f321e2a 100644 --- a/test/invariant/handlers/LpHandler.sol +++ b/test/invariant/handlers/LpHandler.sol @@ -14,22 +14,22 @@ contract LpHandler is HandlerBase { uint256 public depositCount; uint256 public withdrawCount; - uint256 public constant TRILLION = 1e10; + uint256 public constant TRILLION = 1e12; constructor( PSM3 psm_, MockERC20 asset0, MockERC20 asset1, MockERC20 asset2, - uint256 lpCount + uint256 lpCount ) HandlerBase(psm_, asset0, asset1, asset2) { for (uint256 i = 0; i < lpCount; i++) { lps.push(makeAddr(string(abi.encodePacked("lp-", i)))); } } - function _getLP(uint256 lpSeed) internal view returns (address) { - return lps[_bound(lpSeed, 0, lps.length - 1)]; + function _getLP(uint256 indexSeed) internal view returns (address) { + return lps[indexSeed % lps.length]; } function deposit(uint256 assetSeed, uint256 lpSeed, uint256 amount) public { diff --git a/test/invariant/handlers/SwapperHandler.sol b/test/invariant/handlers/SwapperHandler.sol index 17b70ff..c0e2bb1 100644 --- a/test/invariant/handlers/SwapperHandler.sol +++ b/test/invariant/handlers/SwapperHandler.sol @@ -19,7 +19,7 @@ contract SwapperHandler is HandlerBase { MockERC20 asset0, MockERC20 asset1, MockERC20 asset2, - uint256 lpCount + uint256 lpCount ) HandlerBase(psm_, asset0, asset1, asset2) { for (uint256 i = 0; i < lpCount; i++) { swappers.push(makeAddr(string(abi.encodePacked("swapper-", i)))); @@ -27,7 +27,7 @@ contract SwapperHandler is HandlerBase { } function _getSwapper(uint256 indexSeed) internal view returns (address) { - return swappers[_bound(indexSeed, 0, swappers.length - 1)]; + return swappers[indexSeed % swappers.length]; } function swap( @@ -56,8 +56,8 @@ contract SwapperHandler is HandlerBase { uint256 maxAmountIn = psm.previewSwap( address(assetOut), address(assetIn), - assetOut.balanceOf(address(psm) - )); + assetOut.balanceOf(address(psm)) + ); // If there's zero balance a swap can't be performed if (maxAmountIn == 0) { From fa3a23267706f4ca7537f669acedd2423e614c33 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 19 Jun 2024 09:42:54 -0400 Subject: [PATCH 74/96] feat: add afterInvariant hook --- foundry.toml | 1 + test/invariant/Invariants.t.sol | 76 ++++++++++++++++++++++ test/invariant/handlers/LpHandler.sol | 2 +- test/invariant/handlers/SwapperHandler.sol | 2 +- 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index da4ae9b..3ffd808 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,6 +12,7 @@ runs = 1000 [invariant] runs = 20 depth = 1000 +shrink_run_limit=100 [profile.pr.invariant] runs = 200 diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index d41ffa2..06798be 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -67,4 +67,80 @@ contract PSMInvariantTests is PSMTestBase { ); } + function afterInvariant() public { + address lp0 = lpHandler.lps(0); + address lp1 = lpHandler.lps(1); + address lp2 = lpHandler.lps(2); + + // Get value of each LPs current deposits. + uint256 lp0DepositsValue = psm.convertToAssetValue(psm.shares(lp0)); + uint256 lp1DepositsValue = psm.convertToAssetValue(psm.shares(lp1)); + uint256 lp2DepositsValue = psm.convertToAssetValue(psm.shares(lp2)); + + // Get value of each LPs token holdings from previous withdrawals. + uint256 lp0WithdrawsValue = _getLpTokenValue(lp0); + uint256 lp1WithdrawsValue = _getLpTokenValue(lp1); + uint256 lp2WithdrawsValue = _getLpTokenValue(lp2); + + uint256 psmTotalValue = psm.getPsmTotalValue(); + + // Liquidity is unknown so withdraw all assets for all users to empty PSM. + _withdraw(address(dai), lp0, type(uint256).max); + _withdraw(address(usdc), lp0, type(uint256).max); + _withdraw(address(sDai), lp0, type(uint256).max); + + _withdraw(address(dai), lp1, type(uint256).max); + _withdraw(address(usdc), lp1, type(uint256).max); + _withdraw(address(sDai), lp1, type(uint256).max); + + _withdraw(address(dai), lp2, type(uint256).max); + _withdraw(address(usdc), lp2, type(uint256).max); + _withdraw(address(sDai), lp2, type(uint256).max); + + // All funds are completely withdrawn. + assertEq(psm.shares(lp0), 0); + assertEq(psm.shares(lp1), 0); + assertEq(psm.shares(lp2), 0); + + // PSM is empty. + assertEq(psm.totalShares(), 0); + assertEq(psm.getPsmTotalValue(), 0); + + // Tokens held by LPs are equal to the sum of their previous balance + // plus the amount of value originally represented in the PSM's shares. + // There can be rounding here because of share burning up to 1e12 when withdrawing USDC. + // It should be noted that LP2 here has a rounding error of 2e12 since both LP0 and LP1 + // could have rounding errors that accumulate to LP2. + assertApproxEqAbs(_getLpTokenValue(lp0), lp0DepositsValue + lp0WithdrawsValue, 1e12); + assertApproxEqAbs(_getLpTokenValue(lp1), lp1DepositsValue + lp1WithdrawsValue, 1e12); + assertApproxEqAbs(_getLpTokenValue(lp2), lp2DepositsValue + lp2WithdrawsValue, 2e12); + + // Current value of all LPs' token holdings. + uint256 sumLpValue = _getLpTokenValue(lp0) + _getLpTokenValue(lp1) + _getLpTokenValue(lp2); + + // Total amount just withdrawn from the PSM. + uint256 totalWithdrawals + = sumLpValue - (lp0WithdrawsValue + lp1WithdrawsValue + lp2WithdrawsValue); + + // Assert that all funds were withdrawn equals the original value of the PSM. + assertApproxEqAbs(totalWithdrawals, psmTotalValue, 2); + + // Get the starting sum of all LPs' deposits and withdrawals. + uint256 sumStartingValue = + (lp0DepositsValue + lp1DepositsValue + lp2DepositsValue) + + (lp0WithdrawsValue + lp1WithdrawsValue + lp2WithdrawsValue); + + // Assert that the sum of all LPs' deposits and withdrawals equals + // the sum of all LPs' resulting token holdings. Rounding errors are accumulated by LPs. + assertApproxEqAbs(sumLpValue, sumStartingValue, 6); + } + + function _getLpTokenValue(address lp) internal view returns (uint256) { + uint256 daiValue = dai.balanceOf(lp); + uint256 usdcValue = usdc.balanceOf(lp) * 1e12; + uint256 sDaiValue = sDai.balanceOf(lp) * rateProvider.getConversionRate() / 1e27; + + return daiValue + usdcValue + sDaiValue; + } + } diff --git a/test/invariant/handlers/LpHandler.sol b/test/invariant/handlers/LpHandler.sol index f321e2a..6e5a774 100644 --- a/test/invariant/handlers/LpHandler.sol +++ b/test/invariant/handlers/LpHandler.sol @@ -24,7 +24,7 @@ contract LpHandler is HandlerBase { uint256 lpCount ) HandlerBase(psm_, asset0, asset1, asset2) { for (uint256 i = 0; i < lpCount; i++) { - lps.push(makeAddr(string(abi.encodePacked("lp-", i)))); + lps.push(makeAddr(string(abi.encodePacked("lp-", vm.toString(i))))); } } diff --git a/test/invariant/handlers/SwapperHandler.sol b/test/invariant/handlers/SwapperHandler.sol index c0e2bb1..5fcbfeb 100644 --- a/test/invariant/handlers/SwapperHandler.sol +++ b/test/invariant/handlers/SwapperHandler.sol @@ -22,7 +22,7 @@ contract SwapperHandler is HandlerBase { uint256 lpCount ) HandlerBase(psm_, asset0, asset1, asset2) { for (uint256 i = 0; i < lpCount; i++) { - swappers.push(makeAddr(string(abi.encodePacked("swapper-", i)))); + swappers.push(makeAddr(string(abi.encodePacked("swapper-", vm.toString(i))))); } } From c4bf0cc2d93794f162ceb1845c5570fc35c081ee Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 20 Jun 2024 10:52:44 -0400 Subject: [PATCH 75/96] fix: update invariant --- test/invariant/Invariants.t.sol | 8 +- test/unit/DoSAttack.t.sol | 47 ++++++++++ test/unit/Rounding.t.sol | 161 ++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 test/unit/DoSAttack.t.sol create mode 100644 test/unit/Rounding.t.sol diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index d41ffa2..392679b 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -36,10 +36,10 @@ contract PSMInvariantTests is PSMTestBase { } function invariant_B() public view { - // Assumes exchange rate above 1 for sDAI - assertGe( - psm.getPsmTotalValue() + 1, // Make this adjustment to allow a negative tolerance of 1 - psm.totalShares() + assertApproxEqAbs( + psm.getPsmTotalValue(), + psm.convertToAssetValue(psm.totalShares()), + 2 ); } diff --git a/test/unit/DoSAttack.t.sol b/test/unit/DoSAttack.t.sol new file mode 100644 index 0000000..7aca07c --- /dev/null +++ b/test/unit/DoSAttack.t.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import { PSMTestBase } from "test/PSMTestBase.sol"; + +contract InflationAttackTests is PSMTestBase { + + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + + function test_dos_sendFundsBeforeFirstDeposit() public { + // Attack pool sending funds in before the first deposit + usdc.mint(address(this), 100e6); + usdc.transfer(address(psm), 100e6); + + assertEq(usdc.balanceOf(address(psm)), 100e6); + + assertEq(psm.totalShares(), 0); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(user2), 0); + + _deposit(address(usdc), address(user1), 1_000_000e6); + + // Since exchange rate is zero, convertToShares returns 1m * 0 / 100e6 + // because totalValue is not zero so it enters that if statement. + // This results in the funds going in the pool with no way for the user + // to recover them. + assertEq(usdc.balanceOf(address(psm)), 1_000_100e6); + + assertEq(psm.totalShares(), 0); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(user2), 0); + + // This issue is not related to the first deposit only because totalShares cannot + // get above zero. + _deposit(address(usdc), address(user2), 1_000_000e6); + + assertEq(usdc.balanceOf(address(psm)), 2_000_100e6); + + assertEq(psm.totalShares(), 0); + assertEq(psm.shares(user1), 0); + assertEq(psm.shares(user2), 0); + } + +} diff --git a/test/unit/Rounding.t.sol b/test/unit/Rounding.t.sol new file mode 100644 index 0000000..c9bd18c --- /dev/null +++ b/test/unit/Rounding.t.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import { PSMTestBase } from "test/PSMTestBase.sol"; + +import { MockERC20 } from "erc20-helpers/MockERC20.sol"; + +contract RoundingTests is PSMTestBase { + + address user = makeAddr("user"); + + function setUp() public override { + super.setUp(); + + // Seed the PSM with max liquidity so withdrawals can always be performed + _deposit(address(dai), address(this), DAI_TOKEN_MAX); + _deposit(address(sDai), address(this), SDAI_TOKEN_MAX); + _deposit(address(usdc), address(this), USDC_TOKEN_MAX); + + // Set an exchange rate that will cause rounding + rateProvider.__setConversionRate(1.25e27 * uint256(100) / 99); + } + + function test_roundAgainstUser_dai() public { + _deposit(address(dai), address(user), 1e18); + + assertEq(dai.balanceOf(address(user)), 0); + + vm.prank(user); + psm.withdraw(address(dai), address(user), 1e18); + + assertEq(dai.balanceOf(address(user)), 1e18 - 1); // Rounds against user + } + + function test_roundAgainstUser_usdc() public { + _deposit(address(usdc), address(user), 1e6); + + assertEq(usdc.balanceOf(address(user)), 0); + + vm.prank(user); + psm.withdraw(address(usdc), address(user), 1e6); + + assertEq(usdc.balanceOf(address(user)), 1e6 - 1); // Rounds against user + } + + function test_roundAgainstUser_sDai() public { + _deposit(address(sDai), address(user), 1e18); + + assertEq(sDai.balanceOf(address(user)), 0); + + vm.prank(user); + psm.withdraw(address(sDai), address(user), 1e18); + + assertEq(sDai.balanceOf(address(user)), 1e18 - 1); // Rounds against user + } + + function testFuzz_roundingAgainstUser_multiUser_dai( + uint256 rate1, + uint256 rate2, + uint256 amount1, + uint256 amount2 + ) + public + { + _runRoundingAgainstUsersFuzzTest( + dai, + DAI_TOKEN_MAX, + rate1, + rate2, + amount1, + amount2, + 4 + ); + } + + function testFuzz_roundingAgainstUser_multiUser_usdc( + uint256 rate1, + uint256 rate2, + uint256 amount1, + uint256 amount2 + ) + public + { + _runRoundingAgainstUsersFuzzTest( + usdc, + USDC_TOKEN_MAX, + rate1, + rate2, + amount1, + amount2, + 1 // Lower precision so rounding errors are lower + ); + } + + function testFuzz_roundingAgainstUser_multiUser_sDai( + uint256 rate1, + uint256 rate2, + uint256 amount1, + uint256 amount2 + ) + public + { + _runRoundingAgainstUsersFuzzTest( + sDai, + SDAI_TOKEN_MAX, + rate1, + rate2, + amount1, + amount2, + 4 // sDai has higher rounding errors that can be introduced because of rate conversion + ); + } + + function _runRoundingAgainstUsersFuzzTest( + MockERC20 asset, + uint256 tokenMax, + uint256 rate1, + uint256 rate2, + uint256 amount1, + uint256 amount2, + uint256 roundingTolerance + ) internal { + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + + rate1 = _bound(rate1, 1e27, 10e27); + rate2 = _bound(rate2, rate1, 10e27); + + amount1 = _bound(amount1, 1, tokenMax); + amount2 = _bound(amount2, 1, tokenMax); + + rateProvider.__setConversionRate(rate1); + + _deposit(address(asset), address(user1), amount1); + + assertEq(asset.balanceOf(address(user1)), 0); + + vm.prank(user1); + psm.withdraw(address(asset), address(user1), amount1); + + // Rounds against user up to one unit, always rounding down + assertApproxEqAbs(asset.balanceOf(address(user1)), amount1, roundingTolerance); + assertLe(asset.balanceOf(address(user1)), amount1); + + rateProvider.__setConversionRate(rate2); + + _deposit(address(asset), address(user2), amount2); + + assertEq(asset.balanceOf(address(user2)), 0); + + vm.prank(user2); + psm.withdraw(address(asset), address(user2), amount2); + + // Rounds against user up to one unit, always rounding down + + assertApproxEqAbs(asset.balanceOf(address(user2)), amount2, roundingTolerance); + assertLe(asset.balanceOf(address(user2)), amount2); + } +} From df65c1b5f80b8aa8b5017e2bc4d5b994510b4f82 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 20 Jun 2024 11:49:19 -0400 Subject: [PATCH 76/96] fix: add fuzz failure --- foundry.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/foundry.toml b/foundry.toml index da4ae9b..d92768d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -17,10 +17,16 @@ depth = 1000 runs = 200 depth = 1000 +[profile.pr.fuzz] +runs = 100_000 + [profile.master.invariant] runs = 200 depth = 10_000 +[profile.master.fuzz] +runs = 1_000_000 + # See more config options https://github.com/foundry-rs/foundry/tree/master/config remappings = [ From 42581aab7eb2cc43b2eaa15db100ca09646fd528 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 09:38:55 -0400 Subject: [PATCH 77/96] chore: rm indexing comment --- src/interfaces/IPSM3.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/interfaces/IPSM3.sol b/src/interfaces/IPSM3.sol index 6230014..06adef4 100644 --- a/src/interfaces/IPSM3.sol +++ b/src/interfaces/IPSM3.sol @@ -5,8 +5,6 @@ import { IERC20 } from "erc20-helpers/interfaces/IERC20.sol"; interface IPSM3 { - // TODO: Determine priority for indexing - /**********************************************************************************************/ /*** Events ***/ /**********************************************************************************************/ From 1b07d93c06da43a4e9b9842d58e8b2fb33485901 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 10:09:40 -0400 Subject: [PATCH 78/96] feat: refactor structure --- test/Rounding.t.sol | 161 -------------------------------- test/invariant/Invariants.t.sol | 78 +++++++++++----- 2 files changed, 55 insertions(+), 184 deletions(-) delete mode 100644 test/Rounding.t.sol diff --git a/test/Rounding.t.sol b/test/Rounding.t.sol deleted file mode 100644 index c9bd18c..0000000 --- a/test/Rounding.t.sol +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; - -import { PSMTestBase } from "test/PSMTestBase.sol"; - -import { MockERC20 } from "erc20-helpers/MockERC20.sol"; - -contract RoundingTests is PSMTestBase { - - address user = makeAddr("user"); - - function setUp() public override { - super.setUp(); - - // Seed the PSM with max liquidity so withdrawals can always be performed - _deposit(address(dai), address(this), DAI_TOKEN_MAX); - _deposit(address(sDai), address(this), SDAI_TOKEN_MAX); - _deposit(address(usdc), address(this), USDC_TOKEN_MAX); - - // Set an exchange rate that will cause rounding - rateProvider.__setConversionRate(1.25e27 * uint256(100) / 99); - } - - function test_roundAgainstUser_dai() public { - _deposit(address(dai), address(user), 1e18); - - assertEq(dai.balanceOf(address(user)), 0); - - vm.prank(user); - psm.withdraw(address(dai), address(user), 1e18); - - assertEq(dai.balanceOf(address(user)), 1e18 - 1); // Rounds against user - } - - function test_roundAgainstUser_usdc() public { - _deposit(address(usdc), address(user), 1e6); - - assertEq(usdc.balanceOf(address(user)), 0); - - vm.prank(user); - psm.withdraw(address(usdc), address(user), 1e6); - - assertEq(usdc.balanceOf(address(user)), 1e6 - 1); // Rounds against user - } - - function test_roundAgainstUser_sDai() public { - _deposit(address(sDai), address(user), 1e18); - - assertEq(sDai.balanceOf(address(user)), 0); - - vm.prank(user); - psm.withdraw(address(sDai), address(user), 1e18); - - assertEq(sDai.balanceOf(address(user)), 1e18 - 1); // Rounds against user - } - - function testFuzz_roundingAgainstUser_multiUser_dai( - uint256 rate1, - uint256 rate2, - uint256 amount1, - uint256 amount2 - ) - public - { - _runRoundingAgainstUsersFuzzTest( - dai, - DAI_TOKEN_MAX, - rate1, - rate2, - amount1, - amount2, - 4 - ); - } - - function testFuzz_roundingAgainstUser_multiUser_usdc( - uint256 rate1, - uint256 rate2, - uint256 amount1, - uint256 amount2 - ) - public - { - _runRoundingAgainstUsersFuzzTest( - usdc, - USDC_TOKEN_MAX, - rate1, - rate2, - amount1, - amount2, - 1 // Lower precision so rounding errors are lower - ); - } - - function testFuzz_roundingAgainstUser_multiUser_sDai( - uint256 rate1, - uint256 rate2, - uint256 amount1, - uint256 amount2 - ) - public - { - _runRoundingAgainstUsersFuzzTest( - sDai, - SDAI_TOKEN_MAX, - rate1, - rate2, - amount1, - amount2, - 4 // sDai has higher rounding errors that can be introduced because of rate conversion - ); - } - - function _runRoundingAgainstUsersFuzzTest( - MockERC20 asset, - uint256 tokenMax, - uint256 rate1, - uint256 rate2, - uint256 amount1, - uint256 amount2, - uint256 roundingTolerance - ) internal { - address user1 = makeAddr("user1"); - address user2 = makeAddr("user2"); - - rate1 = _bound(rate1, 1e27, 10e27); - rate2 = _bound(rate2, rate1, 10e27); - - amount1 = _bound(amount1, 1, tokenMax); - amount2 = _bound(amount2, 1, tokenMax); - - rateProvider.__setConversionRate(rate1); - - _deposit(address(asset), address(user1), amount1); - - assertEq(asset.balanceOf(address(user1)), 0); - - vm.prank(user1); - psm.withdraw(address(asset), address(user1), amount1); - - // Rounds against user up to one unit, always rounding down - assertApproxEqAbs(asset.balanceOf(address(user1)), amount1, roundingTolerance); - assertLe(asset.balanceOf(address(user1)), amount1); - - rateProvider.__setConversionRate(rate2); - - _deposit(address(asset), address(user2), amount2); - - assertEq(asset.balanceOf(address(user2)), 0); - - vm.prank(user2); - psm.withdraw(address(asset), address(user2), amount2); - - // Rounds against user up to one unit, always rounding down - - assertApproxEqAbs(asset.balanceOf(address(user2)), amount2, roundingTolerance); - assertLe(asset.balanceOf(address(user2)), amount2); - } -} diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 21f5f71..604fe40 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -8,25 +8,16 @@ import { PSMTestBase } from "test/PSMTestBase.sol"; import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; -contract PSMInvariantTests is PSMTestBase { +contract PSMInvariantTestBase is PSMTestBase { LpHandler public lpHandler; SwapperHandler public swapperHandler; - function setUp() public override { - super.setUp(); - - lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); - swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); - - // TODO: Add rate updates - rateProvider.__setConversionRate(1.25e27); + /**********************************************************************************************/ + /*** Invariant assertion functions ***/ + /**********************************************************************************************/ - targetContract(address(lpHandler)); - targetContract(address(swapperHandler)); - } - - function invariant_A() public view { + function _checkInvariant_A() public view { assertEq( psm.shares(address(lpHandler.lps(0))) + psm.shares(address(lpHandler.lps(1))) + @@ -35,7 +26,7 @@ contract PSMInvariantTests is PSMTestBase { ); } - function invariant_B() public view { + function _checkInvariant_B() public view { assertApproxEqAbs( psm.getPsmTotalValue(), psm.convertToAssetValue(psm.totalShares()), @@ -43,7 +34,7 @@ contract PSMInvariantTests is PSMTestBase { ); } - function invariant_C() public view { + function _checkInvariant_C() public view { assertApproxEqAbs( psm.convertToAssetValue(psm.shares(address(lpHandler.lps(0)))) + psm.convertToAssetValue(psm.shares(address(lpHandler.lps(1)))) + @@ -53,7 +44,11 @@ contract PSMInvariantTests is PSMTestBase { ); } - function invariant_logs() public view { + /**********************************************************************************************/ + /*** Helper functions ***/ + /**********************************************************************************************/ + + function _logHandlerCallCounts() public view { console.log("depositCount ", lpHandler.depositCount()); console.log("withdrawCount ", lpHandler.withdrawCount()); console.log("swapCount ", swapperHandler.swapCount()); @@ -67,7 +62,19 @@ contract PSMInvariantTests is PSMTestBase { ); } - function afterInvariant() public { + function _getLpTokenValue(address lp) internal view returns (uint256) { + uint256 daiValue = dai.balanceOf(lp); + uint256 usdcValue = usdc.balanceOf(lp) * 1e12; + uint256 sDaiValue = sDai.balanceOf(lp) * rateProvider.getConversionRate() / 1e27; + + return daiValue + usdcValue + sDaiValue; + } + + /**********************************************************************************************/ + /*** After invariant hook functions ***/ + /**********************************************************************************************/ + + function _withdrawAllPositions() public { address lp0 = lpHandler.lps(0); address lp1 = lpHandler.lps(1); address lp2 = lpHandler.lps(2); @@ -135,12 +142,37 @@ contract PSMInvariantTests is PSMTestBase { assertApproxEqAbs(sumLpValue, sumStartingValue, 6); } - function _getLpTokenValue(address lp) internal view returns (uint256) { - uint256 daiValue = dai.balanceOf(lp); - uint256 usdcValue = usdc.balanceOf(lp) * 1e12; - uint256 sDaiValue = sDai.balanceOf(lp) * rateProvider.getConversionRate() / 1e27; +} - return daiValue + usdcValue + sDaiValue; +contract PSMInvariants1 is PSMInvariantTestBase { + + function setUp() public override { + super.setUp(); + + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + + // TODO: Add rate updates + rateProvider.__setConversionRate(1.25e27); + + targetContract(address(lpHandler)); + targetContract(address(swapperHandler)); + } + + function invariant_A() public view { + _checkInvariant_A(); + } + + function invariant_B() public view { + _checkInvariant_B(); + } + + function invariant_C() public view { + _checkInvariant_C(); + } + + function afterInvariant() public { + _withdrawAllPositions(); } } From e5153a0b85fd863a654ff01110ff05862343d58f Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 13:31:25 -0400 Subject: [PATCH 79/96] feat: both invariants working --- foundry.toml | 2 +- test/invariant/Invariants.t.sol | 108 +++++++++++++++++--- test/invariant/handlers/HandlerBase.sol | 2 + test/invariant/handlers/LpHandler.sol | 2 - test/invariant/handlers/TransferHandler.sol | 37 +++++++ 5 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 test/invariant/handlers/TransferHandler.sol diff --git a/foundry.toml b/foundry.toml index d07e4b3..5b4b588 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,7 +12,7 @@ runs = 1000 [invariant] runs = 20 depth = 1000 -shrink_run_limit=100 +shrink_run_limit=1000 [profile.pr.invariant] runs = 200 diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 604fe40..b9c3fbc 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -5,13 +5,27 @@ import "forge-std/Test.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; -import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; +import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; +import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; +import { TransferHandler } from "test/invariant/handlers/TransferHandler.sol"; -contract PSMInvariantTestBase is PSMTestBase { +abstract contract PSMInvariantTestBase is PSMTestBase { - LpHandler public lpHandler; - SwapperHandler public swapperHandler; + LpHandler public lpHandler; + SwapperHandler public swapperHandler; + TransferHandler public transferHandler; + + address BURN_ADDRESS = makeAddr("burn-address"); + + // NOTE [CRITICAL]: All invariant tests are operating under the assumption that the initial seed + // deposit of 1e18 shares has been made. This is a key requirement and + // assumption for all invariant tests. + function setUp() public virtual override { + super.setUp(); + + // Seed the pool with 1e18 shares (1e18 of value) + _deposit(address(dai), BURN_ADDRESS, 1e18); + } /**********************************************************************************************/ /*** Invariant assertion functions ***/ @@ -21,7 +35,8 @@ contract PSMInvariantTestBase is PSMTestBase { assertEq( psm.shares(address(lpHandler.lps(0))) + psm.shares(address(lpHandler.lps(1))) + - psm.shares(address(lpHandler.lps(2))), + psm.shares(address(lpHandler.lps(2))) + + 1e18, // Seed amount psm.totalShares() ); } @@ -38,9 +53,10 @@ contract PSMInvariantTestBase is PSMTestBase { assertApproxEqAbs( psm.convertToAssetValue(psm.shares(address(lpHandler.lps(0)))) + psm.convertToAssetValue(psm.shares(address(lpHandler.lps(1)))) + - psm.convertToAssetValue(psm.shares(address(lpHandler.lps(2)))), + psm.convertToAssetValue(psm.shares(address(lpHandler.lps(2)))) + + psm.convertToAssetValue(1e18), // Seed amount psm.getPsmTotalValue(), - 3 + 4 ); } @@ -91,6 +107,8 @@ contract PSMInvariantTestBase is PSMTestBase { uint256 psmTotalValue = psm.getPsmTotalValue(); + uint256 startingSeedValue = psm.convertToAssetValue(1e18); + // Liquidity is unknown so withdraw all assets for all users to empty PSM. _withdraw(address(dai), lp0, type(uint256).max); _withdraw(address(usdc), lp0, type(uint256).max); @@ -109,9 +127,11 @@ contract PSMInvariantTestBase is PSMTestBase { assertEq(psm.shares(lp1), 0); assertEq(psm.shares(lp2), 0); - // PSM is empty. - assertEq(psm.totalShares(), 0); - assertEq(psm.getPsmTotalValue(), 0); + uint256 seedValue = psm.convertToAssetValue(1e18); + + // PSM is empty (besides seed amount). + assertEq(psm.totalShares(), 1e18); + assertEq(psm.getPsmTotalValue(), seedValue); // Tokens held by LPs are equal to the sum of their previous balance // plus the amount of value originally represented in the PSM's shares. @@ -122,6 +142,9 @@ contract PSMInvariantTestBase is PSMTestBase { assertApproxEqAbs(_getLpTokenValue(lp1), lp1DepositsValue + lp1WithdrawsValue, 1e12); assertApproxEqAbs(_getLpTokenValue(lp2), lp2DepositsValue + lp2WithdrawsValue, 2e12); + // All rounding errors from LPs can accrue to the burn address after withdrawals are made. + assertApproxEqAbs(seedValue, startingSeedValue, 3e12); + // Current value of all LPs' token holdings. uint256 sumLpValue = _getLpTokenValue(lp0) + _getLpTokenValue(lp1) + _getLpTokenValue(lp2); @@ -129,8 +152,9 @@ contract PSMInvariantTestBase is PSMTestBase { uint256 totalWithdrawals = sumLpValue - (lp0WithdrawsValue + lp1WithdrawsValue + lp2WithdrawsValue); - // Assert that all funds were withdrawn equals the original value of the PSM. - assertApproxEqAbs(totalWithdrawals, psmTotalValue, 2); + // Assert that all funds were withdrawn equals the original value of the PSM minus the + // 1e18 share seed deposit. + assertApproxEqAbs(totalWithdrawals, psmTotalValue - seedValue, 2); // Get the starting sum of all LPs' deposits and withdrawals. uint256 sumStartingValue = @@ -139,12 +163,31 @@ contract PSMInvariantTestBase is PSMTestBase { // Assert that the sum of all LPs' deposits and withdrawals equals // the sum of all LPs' resulting token holdings. Rounding errors are accumulated by LPs. - assertApproxEqAbs(sumLpValue, sumStartingValue, 6); + assertApproxEqAbs(sumLpValue, sumStartingValue, seedValue - startingSeedValue + 1); + + // NOTE: Below logic is not realistic, shown to demonstrate precision. + + _withdraw(address(dai), BURN_ADDRESS, type(uint256).max); + _withdraw(address(usdc), BURN_ADDRESS, type(uint256).max); + _withdraw(address(sDai), BURN_ADDRESS, type(uint256).max); + + // When all funds are completely withdrawn, the sum of all funds withdrawn is equal to the + // sum of value of all LPs including the burn address. All rounding errors get reduced to + // a few wei. + assertApproxEqAbs( + sumLpValue + _getLpTokenValue(BURN_ADDRESS), + sumStartingValue + startingSeedValue, + 4 + ); + + // All funds can always be withdrawn completely. + assertEq(psm.totalShares(), 0); + assertEq(psm.getPsmTotalValue(), 0); } } -contract PSMInvariants1 is PSMInvariantTestBase { +contract PSMInvariants_ConstantRate_NoTransfer is PSMInvariantTestBase { function setUp() public override { super.setUp(); @@ -152,7 +195,6 @@ contract PSMInvariants1 is PSMInvariantTestBase { lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); - // TODO: Add rate updates rateProvider.__setConversionRate(1.25e27); targetContract(address(lpHandler)); @@ -176,3 +218,37 @@ contract PSMInvariants1 is PSMInvariantTestBase { } } + +contract PSMInvariants_ConstantRate_WithTransfers is PSMInvariantTestBase { + + function setUp() public override { + super.setUp(); + + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + transferHandler = new TransferHandler(psm, dai, usdc, sDai); + + rateProvider.__setConversionRate(1.25e27); + + targetContract(address(lpHandler)); + targetContract(address(swapperHandler)); + targetContract(address(transferHandler)); + } + + function invariant_A() public view { + _checkInvariant_A(); + } + + function invariant_B() public view { + _checkInvariant_B(); + } + + function invariant_C_test() public view { + _checkInvariant_C(); + } + + function afterInvariant() public { + _withdrawAllPositions(); + } + +} diff --git a/test/invariant/handlers/HandlerBase.sol b/test/invariant/handlers/HandlerBase.sol index fbc8dff..d4f2f88 100644 --- a/test/invariant/handlers/HandlerBase.sol +++ b/test/invariant/handlers/HandlerBase.sol @@ -15,6 +15,8 @@ contract HandlerBase is CommonBase, StdCheatsSafe, StdUtils { MockERC20[3] public assets; + uint256 public constant TRILLION = 1e12; + constructor( PSM3 psm_, MockERC20 asset0, diff --git a/test/invariant/handlers/LpHandler.sol b/test/invariant/handlers/LpHandler.sol index 6e5a774..57f08c9 100644 --- a/test/invariant/handlers/LpHandler.sol +++ b/test/invariant/handlers/LpHandler.sol @@ -14,8 +14,6 @@ contract LpHandler is HandlerBase { uint256 public depositCount; uint256 public withdrawCount; - uint256 public constant TRILLION = 1e12; - constructor( PSM3 psm_, MockERC20 asset0, diff --git a/test/invariant/handlers/TransferHandler.sol b/test/invariant/handlers/TransferHandler.sol new file mode 100644 index 0000000..37820c6 --- /dev/null +++ b/test/invariant/handlers/TransferHandler.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { MockERC20 } from "erc20-helpers/MockERC20.sol"; + +import { HandlerBase } from "test/invariant/handlers/HandlerBase.sol"; + +import { PSM3 } from "src/PSM3.sol"; + +contract TransferHandler is HandlerBase { + + uint256 public transferCount; + + constructor( + PSM3 psm_, + MockERC20 asset0, + MockERC20 asset1, + MockERC20 asset2 + ) HandlerBase(psm_, asset0, asset1, asset2) {} + + function transfer(uint256 assetSeed, string memory senderSeed, uint256 amount) external { + MockERC20 asset = _getAsset(assetSeed); + address sender = makeAddr(senderSeed); + + // Bounding to 1 million here because 1 trillion introduces unrealistic conditions with + // large rounding errors. Would rather keep tolerances smaller with a lower upper bound + // on transfer amounts. + amount = _bound(amount, 1, 1_000_000 * 10 ** asset.decimals()); + + asset.mint(sender, amount); + + vm.prank(sender); + asset.transfer(address(psm), amount); + + transferCount += 1; + } +} From c664f2c2067f4b34994a00aa4ccd8381a612fe31 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 13:32:16 -0400 Subject: [PATCH 80/96] fix: update comment --- test/invariant/Invariants.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index b9c3fbc..5f66bc1 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -162,7 +162,8 @@ abstract contract PSMInvariantTestBase is PSMTestBase { (lp0WithdrawsValue + lp1WithdrawsValue + lp2WithdrawsValue); // Assert that the sum of all LPs' deposits and withdrawals equals - // the sum of all LPs' resulting token holdings. Rounding errors are accumulated by LPs. + // the sum of all LPs' resulting token holdings. Rounding errors are accumulated to the + // burn address. assertApproxEqAbs(sumLpValue, sumStartingValue, seedValue - startingSeedValue + 1); // NOTE: Below logic is not realistic, shown to demonstrate precision. From 3bcb0d05bfb8837c7ca59106ceda537632ca27df Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 13:52:45 -0400 Subject: [PATCH 81/96] feat: add rate setting logic --- test/invariant/Invariants.t.sol | 106 ++++++++++++++++-- test/invariant/handlers/RateSetterHandler.sol | 29 +++++ 2 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 test/invariant/handlers/RateSetterHandler.sol diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 5f66bc1..93cb17e 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -5,15 +5,17 @@ import "forge-std/Test.sol"; import { PSMTestBase } from "test/PSMTestBase.sol"; -import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; -import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; -import { TransferHandler } from "test/invariant/handlers/TransferHandler.sol"; +import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; +import { RateSetterHandler } from "test/invariant/handlers/RateSetterHandler.sol"; +import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; +import { TransferHandler } from "test/invariant/handlers/TransferHandler.sol"; abstract contract PSMInvariantTestBase is PSMTestBase { - LpHandler public lpHandler; - SwapperHandler public swapperHandler; - TransferHandler public transferHandler; + LpHandler public lpHandler; + RateSetterHandler public rateSetterHandler; + SwapperHandler public swapperHandler; + TransferHandler public transferHandler; address BURN_ADDRESS = makeAddr("burn-address"); @@ -69,12 +71,14 @@ abstract contract PSMInvariantTestBase is PSMTestBase { console.log("withdrawCount ", lpHandler.withdrawCount()); console.log("swapCount ", swapperHandler.swapCount()); console.log("zeroBalanceCount", swapperHandler.zeroBalanceCount()); + console.log("setRateCount ", rateSetterHandler.setRateCount()); console.log( "sum ", lpHandler.depositCount() + lpHandler.withdrawCount() + swapperHandler.swapCount() + - swapperHandler.zeroBalanceCount() + swapperHandler.zeroBalanceCount() + + rateSetterHandler.setRateCount() ); } @@ -178,7 +182,7 @@ abstract contract PSMInvariantTestBase is PSMTestBase { assertApproxEqAbs( sumLpValue + _getLpTokenValue(BURN_ADDRESS), sumStartingValue + startingSeedValue, - 4 + 5 ); // All funds can always be withdrawn completely. @@ -196,8 +200,6 @@ contract PSMInvariants_ConstantRate_NoTransfer is PSMInvariantTestBase { lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); - rateProvider.__setConversionRate(1.25e27); - targetContract(address(lpHandler)); targetContract(address(swapperHandler)); } @@ -214,6 +216,10 @@ contract PSMInvariants_ConstantRate_NoTransfer is PSMInvariantTestBase { _checkInvariant_C(); } + function invariant_log() public view { + _logHandlerCallCounts(); + } + function afterInvariant() public { _withdrawAllPositions(); } @@ -229,9 +235,81 @@ contract PSMInvariants_ConstantRate_WithTransfers is PSMInvariantTestBase { swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); transferHandler = new TransferHandler(psm, dai, usdc, sDai); - rateProvider.__setConversionRate(1.25e27); + targetContract(address(lpHandler)); + targetContract(address(swapperHandler)); + targetContract(address(transferHandler)); + } + + function invariant_A() public view { + _checkInvariant_A(); + } + + function invariant_B() public view { + _checkInvariant_B(); + } + + function invariant_C() public view { + _checkInvariant_C(); + } + + function invariant_log() public view { + _logHandlerCallCounts(); + } + + function afterInvariant() public { + _withdrawAllPositions(); + } + +} + +contract PSMInvariants_RateSetting_NoTransfer is PSMInvariantTestBase { + + function setUp() public override { + super.setUp(); + + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + rateSetterHandler = new RateSetterHandler(rateProvider, 1.25e27); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + + targetContract(address(lpHandler)); + targetContract(address(rateSetterHandler)); + targetContract(address(swapperHandler)); + } + + function invariant_A_test() public view { + _checkInvariant_A(); + } + + function invariant_B() public view { + _checkInvariant_B(); + } + + function invariant_C() public view { + _checkInvariant_C(); + } + + function invariant_log_rate() public view { + _logHandlerCallCounts(); + } + + function afterInvariant() public { + _withdrawAllPositions(); + } + +} + +contract PSMInvariants_RateSetting_WithTransfers is PSMInvariantTestBase { + + function setUp() public override { + super.setUp(); + + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + rateSetterHandler = new RateSetterHandler(rateProvider, 1.25e27); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + transferHandler = new TransferHandler(psm, dai, usdc, sDai); targetContract(address(lpHandler)); + targetContract(address(rateSetterHandler)); targetContract(address(swapperHandler)); targetContract(address(transferHandler)); } @@ -244,10 +322,14 @@ contract PSMInvariants_ConstantRate_WithTransfers is PSMInvariantTestBase { _checkInvariant_B(); } - function invariant_C_test() public view { + function invariant_C() public view { _checkInvariant_C(); } + function invariant_log() public view { + _logHandlerCallCounts(); + } + function afterInvariant() public { _withdrawAllPositions(); } diff --git a/test/invariant/handlers/RateSetterHandler.sol b/test/invariant/handlers/RateSetterHandler.sol new file mode 100644 index 0000000..1ca12e0 --- /dev/null +++ b/test/invariant/handlers/RateSetterHandler.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { StdUtils } from "forge-std/StdUtils.sol"; + +import { MockRateProvider } from "test/mocks/MockRateProvider.sol"; + +contract RateSetterHandler is StdUtils { + + uint256 public rate; + + MockRateProvider public rateProvider; + + uint256 public setRateCount; + + constructor(MockRateProvider rateProvider_, uint256 initialRate) { + rateProvider = rateProvider_; + rate = initialRate; + } + + function setRate(uint256 rateIncrease) external { + // Increase the rate by up to 100% + rate += _bound(rateIncrease, 0, 1e27); + + rateProvider.__setConversionRate(rate); + + setRateCount++; + } +} From 54e8ae9a3e099da14378026631c04ac7a7627e82 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 13:59:40 -0400 Subject: [PATCH 82/96] fix: update toml --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 5b4b588..9c7ce0d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,7 +12,7 @@ runs = 1000 [invariant] runs = 20 depth = 1000 -shrink_run_limit=1000 +shrink_run_limit = 1000 [profile.pr.invariant] runs = 200 From f982ff36065c5f6aae9c75e36adc1de2692fbb1f Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 14:00:33 -0400 Subject: [PATCH 83/96] fix: rm redundant files from merge --- test/DoSAttack.t.sol | 47 ------------- test/Rounding.t.sol | 161 ------------------------------------------- 2 files changed, 208 deletions(-) delete mode 100644 test/DoSAttack.t.sol delete mode 100644 test/Rounding.t.sol diff --git a/test/DoSAttack.t.sol b/test/DoSAttack.t.sol deleted file mode 100644 index 7aca07c..0000000 --- a/test/DoSAttack.t.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; - -import { PSMTestBase } from "test/PSMTestBase.sol"; - -contract InflationAttackTests is PSMTestBase { - - address user1 = makeAddr("user1"); - address user2 = makeAddr("user2"); - - function test_dos_sendFundsBeforeFirstDeposit() public { - // Attack pool sending funds in before the first deposit - usdc.mint(address(this), 100e6); - usdc.transfer(address(psm), 100e6); - - assertEq(usdc.balanceOf(address(psm)), 100e6); - - assertEq(psm.totalShares(), 0); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(user2), 0); - - _deposit(address(usdc), address(user1), 1_000_000e6); - - // Since exchange rate is zero, convertToShares returns 1m * 0 / 100e6 - // because totalValue is not zero so it enters that if statement. - // This results in the funds going in the pool with no way for the user - // to recover them. - assertEq(usdc.balanceOf(address(psm)), 1_000_100e6); - - assertEq(psm.totalShares(), 0); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(user2), 0); - - // This issue is not related to the first deposit only because totalShares cannot - // get above zero. - _deposit(address(usdc), address(user2), 1_000_000e6); - - assertEq(usdc.balanceOf(address(psm)), 2_000_100e6); - - assertEq(psm.totalShares(), 0); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(user2), 0); - } - -} diff --git a/test/Rounding.t.sol b/test/Rounding.t.sol deleted file mode 100644 index c9bd18c..0000000 --- a/test/Rounding.t.sol +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; - -import { PSMTestBase } from "test/PSMTestBase.sol"; - -import { MockERC20 } from "erc20-helpers/MockERC20.sol"; - -contract RoundingTests is PSMTestBase { - - address user = makeAddr("user"); - - function setUp() public override { - super.setUp(); - - // Seed the PSM with max liquidity so withdrawals can always be performed - _deposit(address(dai), address(this), DAI_TOKEN_MAX); - _deposit(address(sDai), address(this), SDAI_TOKEN_MAX); - _deposit(address(usdc), address(this), USDC_TOKEN_MAX); - - // Set an exchange rate that will cause rounding - rateProvider.__setConversionRate(1.25e27 * uint256(100) / 99); - } - - function test_roundAgainstUser_dai() public { - _deposit(address(dai), address(user), 1e18); - - assertEq(dai.balanceOf(address(user)), 0); - - vm.prank(user); - psm.withdraw(address(dai), address(user), 1e18); - - assertEq(dai.balanceOf(address(user)), 1e18 - 1); // Rounds against user - } - - function test_roundAgainstUser_usdc() public { - _deposit(address(usdc), address(user), 1e6); - - assertEq(usdc.balanceOf(address(user)), 0); - - vm.prank(user); - psm.withdraw(address(usdc), address(user), 1e6); - - assertEq(usdc.balanceOf(address(user)), 1e6 - 1); // Rounds against user - } - - function test_roundAgainstUser_sDai() public { - _deposit(address(sDai), address(user), 1e18); - - assertEq(sDai.balanceOf(address(user)), 0); - - vm.prank(user); - psm.withdraw(address(sDai), address(user), 1e18); - - assertEq(sDai.balanceOf(address(user)), 1e18 - 1); // Rounds against user - } - - function testFuzz_roundingAgainstUser_multiUser_dai( - uint256 rate1, - uint256 rate2, - uint256 amount1, - uint256 amount2 - ) - public - { - _runRoundingAgainstUsersFuzzTest( - dai, - DAI_TOKEN_MAX, - rate1, - rate2, - amount1, - amount2, - 4 - ); - } - - function testFuzz_roundingAgainstUser_multiUser_usdc( - uint256 rate1, - uint256 rate2, - uint256 amount1, - uint256 amount2 - ) - public - { - _runRoundingAgainstUsersFuzzTest( - usdc, - USDC_TOKEN_MAX, - rate1, - rate2, - amount1, - amount2, - 1 // Lower precision so rounding errors are lower - ); - } - - function testFuzz_roundingAgainstUser_multiUser_sDai( - uint256 rate1, - uint256 rate2, - uint256 amount1, - uint256 amount2 - ) - public - { - _runRoundingAgainstUsersFuzzTest( - sDai, - SDAI_TOKEN_MAX, - rate1, - rate2, - amount1, - amount2, - 4 // sDai has higher rounding errors that can be introduced because of rate conversion - ); - } - - function _runRoundingAgainstUsersFuzzTest( - MockERC20 asset, - uint256 tokenMax, - uint256 rate1, - uint256 rate2, - uint256 amount1, - uint256 amount2, - uint256 roundingTolerance - ) internal { - address user1 = makeAddr("user1"); - address user2 = makeAddr("user2"); - - rate1 = _bound(rate1, 1e27, 10e27); - rate2 = _bound(rate2, rate1, 10e27); - - amount1 = _bound(amount1, 1, tokenMax); - amount2 = _bound(amount2, 1, tokenMax); - - rateProvider.__setConversionRate(rate1); - - _deposit(address(asset), address(user1), amount1); - - assertEq(asset.balanceOf(address(user1)), 0); - - vm.prank(user1); - psm.withdraw(address(asset), address(user1), amount1); - - // Rounds against user up to one unit, always rounding down - assertApproxEqAbs(asset.balanceOf(address(user1)), amount1, roundingTolerance); - assertLe(asset.balanceOf(address(user1)), amount1); - - rateProvider.__setConversionRate(rate2); - - _deposit(address(asset), address(user2), amount2); - - assertEq(asset.balanceOf(address(user2)), 0); - - vm.prank(user2); - psm.withdraw(address(asset), address(user2), amount2); - - // Rounds against user up to one unit, always rounding down - - assertApproxEqAbs(asset.balanceOf(address(user2)), amount2, roundingTolerance); - assertLe(asset.balanceOf(address(user2)), amount2); - } -} From ffa6eeee647d0ebdc970369799fdb59f2bfc44f2 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 14:23:45 -0400 Subject: [PATCH 84/96] fix: update tolerances --- foundry.toml | 2 +- test/invariant/Invariants.t.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/foundry.toml b/foundry.toml index 9c7ce0d..16bcba3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,7 +12,7 @@ runs = 1000 [invariant] runs = 20 depth = 1000 -shrink_run_limit = 1000 +shrink_run_limit = 0 [profile.pr.invariant] runs = 200 diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 5f66bc1..0f4cb3c 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -164,7 +164,7 @@ abstract contract PSMInvariantTestBase is PSMTestBase { // Assert that the sum of all LPs' deposits and withdrawals equals // the sum of all LPs' resulting token holdings. Rounding errors are accumulated to the // burn address. - assertApproxEqAbs(sumLpValue, sumStartingValue, seedValue - startingSeedValue + 1); + assertApproxEqAbs(sumLpValue, sumStartingValue, seedValue - startingSeedValue + 2); // NOTE: Below logic is not realistic, shown to demonstrate precision. @@ -178,7 +178,7 @@ abstract contract PSMInvariantTestBase is PSMTestBase { assertApproxEqAbs( sumLpValue + _getLpTokenValue(BURN_ADDRESS), sumStartingValue + startingSeedValue, - 4 + 5 ); // All funds can always be withdrawn completely. From a2e6f05f05a7165d3df1e2cb0227e1da9232fceb Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 16:45:41 -0400 Subject: [PATCH 85/96] feat: update to add seeding as part of invariants --- test/invariant/Invariants.t.sol | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 392679b..c2d7013 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -13,9 +13,17 @@ contract PSMInvariantTests is PSMTestBase { LpHandler public lpHandler; SwapperHandler public swapperHandler; + address BURN_ADDRESS = makeAddr("burn-address"); + + // NOTE [CRITICAL]: All invariant tests are operating under the assumption that the initial seed + // deposit of 1e18 shares has been made. This is a key requirement and + // assumption for all invariant tests. function setUp() public override { super.setUp(); + // Seed the pool with 1e18 shares (1e18 of value) + _deposit(address(dai), BURN_ADDRESS, 1e18); + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); @@ -30,7 +38,8 @@ contract PSMInvariantTests is PSMTestBase { assertEq( psm.shares(address(lpHandler.lps(0))) + psm.shares(address(lpHandler.lps(1))) + - psm.shares(address(lpHandler.lps(2))), + psm.shares(address(lpHandler.lps(2))) + + 1e18, // Seed amount psm.totalShares() ); } @@ -47,9 +56,10 @@ contract PSMInvariantTests is PSMTestBase { assertApproxEqAbs( psm.convertToAssetValue(psm.shares(address(lpHandler.lps(0)))) + psm.convertToAssetValue(psm.shares(address(lpHandler.lps(1)))) + - psm.convertToAssetValue(psm.shares(address(lpHandler.lps(2)))), + psm.convertToAssetValue(psm.shares(address(lpHandler.lps(2)))) + + psm.convertToAssetValue(1e18), // Seed amount psm.getPsmTotalValue(), - 3 + 4 ); } From adf11a5d11ce8c40c8772616cea8b3fa737947ec Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 13:59:40 -0400 Subject: [PATCH 86/96] fix: update toml --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 5b4b588..9c7ce0d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,7 +12,7 @@ runs = 1000 [invariant] runs = 20 depth = 1000 -shrink_run_limit=1000 +shrink_run_limit = 1000 [profile.pr.invariant] runs = 200 From 07d372e20bec0adef5506fbff20e535babfdc75b Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 14:00:33 -0400 Subject: [PATCH 87/96] fix: rm redundant files from merge --- test/DoSAttack.t.sol | 47 -------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 test/DoSAttack.t.sol diff --git a/test/DoSAttack.t.sol b/test/DoSAttack.t.sol deleted file mode 100644 index 7aca07c..0000000 --- a/test/DoSAttack.t.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; - -import { PSMTestBase } from "test/PSMTestBase.sol"; - -contract InflationAttackTests is PSMTestBase { - - address user1 = makeAddr("user1"); - address user2 = makeAddr("user2"); - - function test_dos_sendFundsBeforeFirstDeposit() public { - // Attack pool sending funds in before the first deposit - usdc.mint(address(this), 100e6); - usdc.transfer(address(psm), 100e6); - - assertEq(usdc.balanceOf(address(psm)), 100e6); - - assertEq(psm.totalShares(), 0); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(user2), 0); - - _deposit(address(usdc), address(user1), 1_000_000e6); - - // Since exchange rate is zero, convertToShares returns 1m * 0 / 100e6 - // because totalValue is not zero so it enters that if statement. - // This results in the funds going in the pool with no way for the user - // to recover them. - assertEq(usdc.balanceOf(address(psm)), 1_000_100e6); - - assertEq(psm.totalShares(), 0); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(user2), 0); - - // This issue is not related to the first deposit only because totalShares cannot - // get above zero. - _deposit(address(usdc), address(user2), 1_000_000e6); - - assertEq(usdc.balanceOf(address(psm)), 2_000_100e6); - - assertEq(psm.totalShares(), 0); - assertEq(psm.shares(user1), 0); - assertEq(psm.shares(user2), 0); - } - -} From 7b1bf35d09e9cd4af2dbc9517300a410e617fa80 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 14:23:45 -0400 Subject: [PATCH 88/96] fix: update tolerances --- foundry.toml | 2 +- test/invariant/Invariants.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index 9c7ce0d..16bcba3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,7 +12,7 @@ runs = 1000 [invariant] runs = 20 depth = 1000 -shrink_run_limit = 1000 +shrink_run_limit = 0 [profile.pr.invariant] runs = 200 diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 93cb17e..d6488df 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -168,7 +168,7 @@ abstract contract PSMInvariantTestBase is PSMTestBase { // Assert that the sum of all LPs' deposits and withdrawals equals // the sum of all LPs' resulting token holdings. Rounding errors are accumulated to the // burn address. - assertApproxEqAbs(sumLpValue, sumStartingValue, seedValue - startingSeedValue + 1); + assertApproxEqAbs(sumLpValue, sumStartingValue, seedValue - startingSeedValue + 2); // NOTE: Below logic is not realistic, shown to demonstrate precision. From d78c0248b6053def30dbd4e052a287711557e0bb Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Fri, 21 Jun 2024 16:50:52 -0400 Subject: [PATCH 89/96] fix: rm invariant logs --- test/invariant/Invariants.t.sol | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index d6488df..25a4684 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -216,10 +216,6 @@ contract PSMInvariants_ConstantRate_NoTransfer is PSMInvariantTestBase { _checkInvariant_C(); } - function invariant_log() public view { - _logHandlerCallCounts(); - } - function afterInvariant() public { _withdrawAllPositions(); } @@ -252,10 +248,6 @@ contract PSMInvariants_ConstantRate_WithTransfers is PSMInvariantTestBase { _checkInvariant_C(); } - function invariant_log() public view { - _logHandlerCallCounts(); - } - function afterInvariant() public { _withdrawAllPositions(); } @@ -288,10 +280,6 @@ contract PSMInvariants_RateSetting_NoTransfer is PSMInvariantTestBase { _checkInvariant_C(); } - function invariant_log_rate() public view { - _logHandlerCallCounts(); - } - function afterInvariant() public { _withdrawAllPositions(); } @@ -326,10 +314,6 @@ contract PSMInvariants_RateSetting_WithTransfers is PSMInvariantTestBase { _checkInvariant_C(); } - function invariant_log() public view { - _logHandlerCallCounts(); - } - function afterInvariant() public { _withdrawAllPositions(); } From e3584370a7315e9daca128c3373b9602aa20378a Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 24 Jun 2024 08:32:18 -0400 Subject: [PATCH 90/96] fix: update tolerance --- test/invariant/Invariants.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 25a4684..178d7e2 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -47,7 +47,7 @@ abstract contract PSMInvariantTestBase is PSMTestBase { assertApproxEqAbs( psm.getPsmTotalValue(), psm.convertToAssetValue(psm.totalShares()), - 2 + 3 ); } From da05a922f901f9cf3a4b7f17607a4f2b534bf31f Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 24 Jun 2024 08:34:08 -0400 Subject: [PATCH 91/96] forge install: xchain-dsr-oracle v1.0.0 --- .gitmodules | 3 +++ lib/xchain-dsr-oracle | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/xchain-dsr-oracle diff --git a/.gitmodules b/.gitmodules index 7122960..8db1840 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/erc20-helpers"] path = lib/erc20-helpers url = https://github.com/marsfoundation/erc20-helpers +[submodule "lib/xchain-dsr-oracle"] + path = lib/xchain-dsr-oracle + url = https://github.com/marsfoundation/xchain-dsr-oracle diff --git a/lib/xchain-dsr-oracle b/lib/xchain-dsr-oracle new file mode 160000 index 0000000..a02e592 --- /dev/null +++ b/lib/xchain-dsr-oracle @@ -0,0 +1 @@ +Subproject commit a02e59274d20f878f642baec43447b3bc183aca3 From 62578bbc02af5b837eb75d01074b5d60de5cc084 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 24 Jun 2024 10:19:57 -0400 Subject: [PATCH 92/96] feat: setup timebased handler, refactor rate provider structure --- foundry.toml | 2 +- src/PSM3.sol | 7 +- src/interfaces/IRateProviderLike.sol | 6 ++ test/PSMTestBase.sol | 10 ++- test/invariant/Invariants.t.sol | 84 ++++++++++++++++--- test/invariant/handlers/RateSetterHandler.sol | 4 +- .../handlers/TimeBasedRateHandler.sol | 45 ++++++++++ test/unit/Conversions.t.sol | 16 ++-- test/unit/Deposit.t.sol | 4 +- test/unit/Getters.t.sol | 16 ++-- test/unit/Previews.t.sol | 10 +-- test/unit/Rounding.t.sol | 8 +- test/unit/Swaps.t.sol | 10 +-- 13 files changed, 168 insertions(+), 54 deletions(-) create mode 100644 src/interfaces/IRateProviderLike.sol create mode 100644 test/invariant/handlers/TimeBasedRateHandler.sol diff --git a/foundry.toml b/foundry.toml index 16bcba3..9c7ce0d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,7 +12,7 @@ runs = 1000 [invariant] runs = 20 depth = 1000 -shrink_run_limit = 0 +shrink_run_limit = 1000 [profile.pr.invariant] runs = 200 diff --git a/src/PSM3.sol b/src/PSM3.sol index de8946a..2f0d81d 100644 --- a/src/PSM3.sol +++ b/src/PSM3.sol @@ -5,11 +5,8 @@ import { IERC20 } from "erc20-helpers/interfaces/IERC20.sol"; import { SafeERC20 } from "erc20-helpers/SafeERC20.sol"; -import { IPSM3 } from "src/interfaces/IPSM3.sol"; - -interface IRateProviderLike { - function getConversionRate() external view returns (uint256); -} +import { IPSM3 } from "src/interfaces/IPSM3.sol"; +import { IRateProviderLike } from "src/interfaces/IRateProviderLike.sol"; contract PSM3 is IPSM3 { diff --git a/src/interfaces/IRateProviderLike.sol b/src/interfaces/IRateProviderLike.sol new file mode 100644 index 0000000..75137d2 --- /dev/null +++ b/src/interfaces/IRateProviderLike.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +interface IRateProviderLike { + function getConversionRate() external view returns (uint256); +} diff --git a/test/PSMTestBase.sol b/test/PSMTestBase.sol index ed3b547..2b7ae2b 100644 --- a/test/PSMTestBase.sol +++ b/test/PSMTestBase.sol @@ -5,6 +5,8 @@ import "forge-std/Test.sol"; import { PSM3 } from "src/PSM3.sol"; +import { IRateProviderLike } from "src/interfaces/IRateProviderLike.sol"; + import { MockERC20 } from "erc20-helpers/MockERC20.sol"; import { MockRateProvider } from "test/mocks/MockRateProvider.sol"; @@ -18,7 +20,7 @@ contract PSMTestBase is Test { MockERC20 public usdc; MockERC20 public sDai; - MockRateProvider public rateProvider; + IRateProviderLike public rateProvider; modifier assertAtomicPsmValueDoesNotChange { uint256 beforeValue = _getPsmValue(); @@ -36,10 +38,12 @@ contract PSMTestBase is Test { usdc = new MockERC20("usdc", "usdc", 6); sDai = new MockERC20("sDai", "sDai", 18); - rateProvider = new MockRateProvider(); + MockRateProvider mockRateProvider = new MockRateProvider(); // NOTE: Using 1.25 for easy two way conversions - rateProvider.__setConversionRate(1.25e27); + mockRateProvider.__setConversionRate(1.25e27); + + rateProvider = IRateProviderLike(address(mockRateProvider)); psm = new PSM3(address(dai), address(usdc), address(sDai), address(rateProvider)); diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 178d7e2..fe54026 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -3,19 +3,27 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; +import { DSRAuthOracle } from "lib/xchain-dsr-oracle/src/DSRAuthOracle.sol"; + +import { PSM3 } from "src/PSM3.sol"; + +import { IRateProviderLike } from "src/interfaces/IRateProviderLike.sol"; + import { PSMTestBase } from "test/PSMTestBase.sol"; -import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; -import { RateSetterHandler } from "test/invariant/handlers/RateSetterHandler.sol"; -import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; -import { TransferHandler } from "test/invariant/handlers/TransferHandler.sol"; +import { LpHandler } from "test/invariant/handlers/LpHandler.sol"; +import { RateSetterHandler } from "test/invariant/handlers/RateSetterHandler.sol"; +import { SwapperHandler } from "test/invariant/handlers/SwapperHandler.sol"; +import { TimeBasedRateHandler } from "test/invariant/handlers/TimeBasedRateHandler.sol"; +import { TransferHandler } from "test/invariant/handlers/TransferHandler.sol"; abstract contract PSMInvariantTestBase is PSMTestBase { - LpHandler public lpHandler; - RateSetterHandler public rateSetterHandler; - SwapperHandler public swapperHandler; - TransferHandler public transferHandler; + LpHandler public lpHandler; + RateSetterHandler public rateSetterHandler; + SwapperHandler public swapperHandler; + TransferHandler public transferHandler; + TimeBasedRateHandler public timeBasedRateHandler; address BURN_ADDRESS = makeAddr("burn-address"); @@ -113,6 +121,8 @@ abstract contract PSMInvariantTestBase is PSMTestBase { uint256 startingSeedValue = psm.convertToAssetValue(1e18); + console.log("startingSeedValue", startingSeedValue); + // Liquidity is unknown so withdraw all assets for all users to empty PSM. _withdraw(address(dai), lp0, type(uint256).max); _withdraw(address(usdc), lp0, type(uint256).max); @@ -260,7 +270,7 @@ contract PSMInvariants_RateSetting_NoTransfer is PSMInvariantTestBase { super.setUp(); lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); - rateSetterHandler = new RateSetterHandler(rateProvider, 1.25e27); + rateSetterHandler = new RateSetterHandler(address(rateProvider), 1.25e27); swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); targetContract(address(lpHandler)); @@ -268,7 +278,7 @@ contract PSMInvariants_RateSetting_NoTransfer is PSMInvariantTestBase { targetContract(address(swapperHandler)); } - function invariant_A_test() public view { + function invariant_A() public view { _checkInvariant_A(); } @@ -292,7 +302,7 @@ contract PSMInvariants_RateSetting_WithTransfers is PSMInvariantTestBase { super.setUp(); lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); - rateSetterHandler = new RateSetterHandler(rateProvider, 1.25e27); + rateSetterHandler = new RateSetterHandler(address(rateProvider), 1.25e27); swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); transferHandler = new TransferHandler(psm, dai, usdc, sDai); @@ -319,3 +329,55 @@ contract PSMInvariants_RateSetting_WithTransfers is PSMInvariantTestBase { } } + +contract PSMInvariants_TimeBasedRateSetting_NoTransfer is PSMInvariantTestBase { + + function setUp() public override { + super.setUp(); + + DSRAuthOracle dsrOracle = new DSRAuthOracle(); + + // Redeploy PSM with new rate provider + psm = new PSM3(address(dai), address(usdc), address(sDai), address(dsrOracle)); + + // Seed the new PSM with 1e18 shares (1e18 of value) + _deposit(address(dai), BURN_ADDRESS, 1e18); + + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + timeBasedRateHandler = new TimeBasedRateHandler(dsrOracle); + + // Handler acts in the same way as a receiver on L2, so add as a data provider to the + // oracle. + dsrOracle.grantRole(dsrOracle.DATA_PROVIDER_ROLE(), address(timeBasedRateHandler)); + + rateProvider = IRateProviderLike(address(dsrOracle)); + + // Manually set initial values for the oracle through the handler to start + timeBasedRateHandler.setPotData(1e27, 1e27, block.timestamp); + + targetContract(address(lpHandler)); + // targetContract(address(swapperHandler)); + targetContract(address(timeBasedRateHandler)); + } + + function invariant_A_test() public view { + _checkInvariant_A(); + } + + function invariant_B() public view { + _checkInvariant_B(); + } + + function invariant_C() public view { + _checkInvariant_C(); + } + + function afterInvariant() public { + _withdrawAllPositions(); + } + +} + + +// TODO: Cast `rateProvider` to interface diff --git a/test/invariant/handlers/RateSetterHandler.sol b/test/invariant/handlers/RateSetterHandler.sol index 1ca12e0..5d58109 100644 --- a/test/invariant/handlers/RateSetterHandler.sol +++ b/test/invariant/handlers/RateSetterHandler.sol @@ -13,8 +13,8 @@ contract RateSetterHandler is StdUtils { uint256 public setRateCount; - constructor(MockRateProvider rateProvider_, uint256 initialRate) { - rateProvider = rateProvider_; + constructor(address rateProvider_, uint256 initialRate) { + rateProvider = MockRateProvider(rateProvider_); rate = initialRate; } diff --git a/test/invariant/handlers/TimeBasedRateHandler.sol b/test/invariant/handlers/TimeBasedRateHandler.sol new file mode 100644 index 0000000..5e52acf --- /dev/null +++ b/test/invariant/handlers/TimeBasedRateHandler.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { console } from "forge-std/console.sol"; +import { StdCheats } from "forge-std/StdCheats.sol"; +import { StdUtils } from "forge-std/StdUtils.sol"; + +import { DSRAuthOracle } from "lib/xchain-dsr-oracle/src/DSRAuthOracle.sol"; +import { IDSROracle } from "lib/xchain-dsr-oracle/src/interfaces/IDSROracle.sol"; + +contract TimeBasedRateHandler is StdCheats, StdUtils { + + uint256 public dsr; + uint256 public chi; + uint256 public rho; + + uint256 constant ONE_HUNDRED_PCT_APY_DSR = 1.00000002197955315123915302e27; + + DSRAuthOracle public dsrOracle; + + uint256 public setRateCount; + + constructor(DSRAuthOracle dsrOracle_) { + dsrOracle = dsrOracle_; + } + + // This acts as a receiver on an L2. + // Note that the chi value is not derived from previous values, this is to test if + // PSM will work as expected with different chi values. + function setPotData(uint256 newDsr, uint256 newChi, uint256 newRho) external { + dsr = _bound(newDsr, 1e27, ONE_HUNDRED_PCT_APY_DSR); + chi = _bound(newChi, chi, 1e27); + rho = _bound(newRho, rho, block.timestamp); + + dsrOracle.setPotData(IDSROracle.PotData({ + dsr: uint96(dsr), + chi: uint120(chi), + rho: uint40(rho) + })); + } + + // function warp(uint256 skipTime) external { + // skip(_bound(skipTime, 0, 45 days)); + // } +} diff --git a/test/unit/Conversions.t.sol b/test/unit/Conversions.t.sol index 1b1c484..63bc97d 100644 --- a/test/unit/Conversions.t.sol +++ b/test/unit/Conversions.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import { PSM3 } from "src/PSM3.sol"; -import { PSMTestBase } from "test/PSMTestBase.sol"; +import { MockRateProvider, PSMTestBase } from "test/PSMTestBase.sol"; // TODO: Add failure modes tests @@ -59,7 +59,7 @@ contract PSMConvertToAssetsTests is PSMTestBase { conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); amount = _bound(amount, 0, SDAI_TOKEN_MAX); - rateProvider.__setConversionRate(conversionRate); + MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); assertEq(psm.convertToAssets(address(sDai), amount), amount * 1e27 / conversionRate); } @@ -79,7 +79,7 @@ contract PSMConvertToAssetValueTests is PSMTestBase { assertEq(psm.convertToAssetValue(1e18), 1e18); - rateProvider.__setConversionRate(2e27); + MockRateProvider(address(rateProvider)).__setConversionRate(2e27); // $300 dollars of value deposited, 300 shares minted. // sDAI portion becomes worth $160, full pool worth $360, each share worth $1.20 @@ -125,7 +125,7 @@ contract PSMConvertToSharesTests is PSMTestBase { // 80 sDAI now worth $120, 200 shares in pool with $220 of value // Each share should be worth $1.10. - rateProvider.__setConversionRate(1.5e27); + MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); assertEq(psm.convertToShares(10), 9); assertEq(psm.convertToShares(11), 10); @@ -195,7 +195,7 @@ contract PSMConvertToSharesWithDaiTests is PSMTestBase { // 80 sDAI now worth $120, 200 shares in pool with $220 of value // Each share should be worth $1.10. - rateProvider.__setConversionRate(1.5e27); + MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); assertEq(psm.convertToShares(address(dai), 10), 9); assertEq(psm.convertToShares(address(dai), 11), 10); @@ -256,7 +256,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMTestBase { // 80 sDAI now worth $120, 200 shares in pool with $220 of value // Each share should be worth $1.10. - rateProvider.__setConversionRate(1.5e27); + MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); assertEq(psm.convertToShares(address(usdc), 10), 9.090909090909e12); assertEq(psm.convertToShares(address(usdc), 11), 10e12); @@ -291,7 +291,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMTestBase { amount = _bound(amount, 1000, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 1000e27); - rateProvider.__setConversionRate(conversionRate); + MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); assertEq(psm.convertToShares(address(sDai), amount), amount * conversionRate / 1e27); } @@ -322,7 +322,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMTestBase { // 80 sDAI now worth $120, 200 shares in pool with $220 of value // Each share should be worth $1.10. Since 1 sDAI is now worth 1.5 USDC, 1 sDAI is worth // 1.50/1.10 = 1.3636... shares - rateProvider.__setConversionRate(1.5e27); + MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); // TODO: Reinvestigate this, interesting difference in rounding assertEq(psm.convertToShares(address(sDai), 1), 0); diff --git a/test/unit/Deposit.t.sol b/test/unit/Deposit.t.sol index c85e54a..fcbf382 100644 --- a/test/unit/Deposit.t.sol +++ b/test/unit/Deposit.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import { PSM3 } from "src/PSM3.sol"; -import { PSMTestBase } from "test/PSMTestBase.sol"; +import { MockRateProvider, PSMTestBase } from "test/PSMTestBase.sol"; contract PSMDepositTests is PSMTestBase { @@ -279,7 +279,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToAssetValue(psm.shares(receiver1)), 225e18); - rateProvider.__setConversionRate(1.5e27); + MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); // Total shares / (100 USDC + 150 sDAI value) uint256 expectedConversionRate = 225 * 1e18 / 250; diff --git a/test/unit/Getters.t.sol b/test/unit/Getters.t.sol index eb2c2e3..11744cd 100644 --- a/test/unit/Getters.t.sol +++ b/test/unit/Getters.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSMTestBase } from "test/PSMTestBase.sol"; +import { MockRateProvider, PSMTestBase } from "test/PSMTestBase.sol"; import { PSM3Harness } from "test/unit/harnesses/PSM3Harness.sol"; @@ -72,7 +72,7 @@ contract PSMHarnessTests is PSMTestBase { assertEq(psmHarness.getAsset2Value(3e18), 3.75e18); assertEq(psmHarness.getAsset2Value(4e18), 5e18); - rateProvider.__setConversionRate(1.6e27); + MockRateProvider(address(rateProvider)).__setConversionRate(1.6e27); assertEq(psmHarness.getAsset2Value(1), 1); assertEq(psmHarness.getAsset2Value(2), 3); @@ -84,7 +84,7 @@ contract PSMHarnessTests is PSMTestBase { assertEq(psmHarness.getAsset2Value(3e18), 4.8e18); assertEq(psmHarness.getAsset2Value(4e18), 6.4e18); - rateProvider.__setConversionRate(0.8e27); + MockRateProvider(address(rateProvider)).__setConversionRate(0.8e27); assertEq(psmHarness.getAsset2Value(1), 0); assertEq(psmHarness.getAsset2Value(2), 1); @@ -101,7 +101,7 @@ contract PSMHarnessTests is PSMTestBase { conversionRate = _bound(conversionRate, 0, 1000e27); amount = _bound(amount, 0, SDAI_TOKEN_MAX); - rateProvider.__setConversionRate(conversionRate); + MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); assertEq(psmHarness.getAsset2Value(amount), amount * conversionRate / 1e27); } @@ -184,11 +184,11 @@ contract GetPsmTotalValueTests is PSMTestBase { assertEq(psm.getPsmTotalValue(), 3.25e18); - rateProvider.__setConversionRate(1.5e27); + MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); assertEq(psm.getPsmTotalValue(), 3.5e18); - rateProvider.__setConversionRate(0.8e27); + MockRateProvider(address(rateProvider)).__setConversionRate(0.8e27); assertEq(psm.getPsmTotalValue(), 2.8e18); } @@ -202,7 +202,7 @@ contract GetPsmTotalValueTests is PSMTestBase { assertEq(psm.getPsmTotalValue(), 3.25e18); - rateProvider.__setConversionRate(1.5e27); + MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); assertEq(psm.getPsmTotalValue(), 3.5e18); @@ -228,7 +228,7 @@ contract GetPsmTotalValueTests is PSMTestBase { usdc.mint(address(psm), usdcAmount); sDai.mint(address(psm), sDaiAmount); - rateProvider.__setConversionRate(conversionRate); + MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); assertEq( psm.getPsmTotalValue(), diff --git a/test/unit/Previews.t.sol b/test/unit/Previews.t.sol index 13d2557..74f5c02 100644 --- a/test/unit/Previews.t.sol +++ b/test/unit/Previews.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSMTestBase } from "test/PSMTestBase.sol"; +import { MockRateProvider, PSMTestBase } from "test/PSMTestBase.sol"; contract PSMPreviewSwapFailureTests is PSMTestBase { @@ -61,7 +61,7 @@ contract PSMPreviewSwapDaiAssetInTests is PSMTestBase { amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate; @@ -94,7 +94,7 @@ contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; @@ -115,7 +115,7 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); uint256 amountOut = amountIn * conversionRate / 1e27; @@ -132,7 +132,7 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; diff --git a/test/unit/Rounding.t.sol b/test/unit/Rounding.t.sol index c9bd18c..725dcee 100644 --- a/test/unit/Rounding.t.sol +++ b/test/unit/Rounding.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { PSMTestBase } from "test/PSMTestBase.sol"; +import { MockRateProvider, PSMTestBase } from "test/PSMTestBase.sol"; import { MockERC20 } from "erc20-helpers/MockERC20.sol"; @@ -20,7 +20,7 @@ contract RoundingTests is PSMTestBase { _deposit(address(usdc), address(this), USDC_TOKEN_MAX); // Set an exchange rate that will cause rounding - rateProvider.__setConversionRate(1.25e27 * uint256(100) / 99); + MockRateProvider(address(rateProvider)).__setConversionRate(1.25e27 * uint256(100) / 99); } function test_roundAgainstUser_dai() public { @@ -131,7 +131,7 @@ contract RoundingTests is PSMTestBase { amount1 = _bound(amount1, 1, tokenMax); amount2 = _bound(amount2, 1, tokenMax); - rateProvider.__setConversionRate(rate1); + MockRateProvider(address(rateProvider)).__setConversionRate(rate1); _deposit(address(asset), address(user1), amount1); @@ -144,7 +144,7 @@ contract RoundingTests is PSMTestBase { assertApproxEqAbs(asset.balanceOf(address(user1)), amount1, roundingTolerance); assertLe(asset.balanceOf(address(user1)), amount1); - rateProvider.__setConversionRate(rate2); + MockRateProvider(address(rateProvider)).__setConversionRate(rate2); _deposit(address(asset), address(user2), amount2); diff --git a/test/unit/Swaps.t.sol b/test/unit/Swaps.t.sol index 43e4bbd..a106069 100644 --- a/test/unit/Swaps.t.sol +++ b/test/unit/Swaps.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import { PSM3 } from "src/PSM3.sol"; -import { MockERC20, PSMTestBase } from "test/PSMTestBase.sol"; +import { MockERC20, MockRateProvider, PSMTestBase } from "test/PSMTestBase.sol"; contract PSMSwapFailureTests is PSMTestBase { @@ -222,7 +222,7 @@ contract PSMSwapDaiAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate; @@ -276,7 +276,7 @@ contract PSMSwapUsdcAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; @@ -316,7 +316,7 @@ contract PSMSwapSDaiAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); uint256 amountOut = amountIn * conversionRate / 1e27; @@ -336,7 +336,7 @@ contract PSMSwapSDaiAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - rateProvider.__setConversionRate(conversionRate); + MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; From b1212b763fb027f4c2fb8d12103b10931dd6205b Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Mon, 24 Jun 2024 10:31:21 -0400 Subject: [PATCH 93/96] feat: tests passing --- test/invariant/Invariants.t.sol | 4 +--- test/invariant/handlers/TimeBasedRateHandler.sol | 7 ++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index fe54026..cb90c7b 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -121,8 +121,6 @@ abstract contract PSMInvariantTestBase is PSMTestBase { uint256 startingSeedValue = psm.convertToAssetValue(1e18); - console.log("startingSeedValue", startingSeedValue); - // Liquidity is unknown so withdraw all assets for all users to empty PSM. _withdraw(address(dai), lp0, type(uint256).max); _withdraw(address(usdc), lp0, type(uint256).max); @@ -357,7 +355,7 @@ contract PSMInvariants_TimeBasedRateSetting_NoTransfer is PSMInvariantTestBase { timeBasedRateHandler.setPotData(1e27, 1e27, block.timestamp); targetContract(address(lpHandler)); - // targetContract(address(swapperHandler)); + targetContract(address(swapperHandler)); targetContract(address(timeBasedRateHandler)); } diff --git a/test/invariant/handlers/TimeBasedRateHandler.sol b/test/invariant/handlers/TimeBasedRateHandler.sol index 5e52acf..c23da27 100644 --- a/test/invariant/handlers/TimeBasedRateHandler.sol +++ b/test/invariant/handlers/TimeBasedRateHandler.sol @@ -39,7 +39,8 @@ contract TimeBasedRateHandler is StdCheats, StdUtils { })); } - // function warp(uint256 skipTime) external { - // skip(_bound(skipTime, 0, 45 days)); - // } + function warp(uint256 skipTime) external { + skip(_bound(skipTime, 0, 45 days)); + } + } From ae6c47b1bd3d2108e133bdcc4274319184906e96 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 3 Jul 2024 08:38:13 -0400 Subject: [PATCH 94/96] chore: first cleanup --- test/PSMTestBase.sol | 6 ++- test/invariant/Invariants.t.sol | 53 +++++++++++++++++- .../handlers/TimeBasedRateHandler.sol | 1 - test/unit/Conversions.t.sol | 54 +++++++++---------- test/unit/Deposit.t.sol | 4 +- test/unit/Getters.t.sol | 14 ++--- test/unit/Previews.t.sol | 8 +-- test/unit/Rounding.t.sol | 6 +-- test/unit/Swaps.t.sol | 8 +-- test/unit/Withdraw.t.sol | 4 +- 10 files changed, 104 insertions(+), 54 deletions(-) diff --git a/test/PSMTestBase.sol b/test/PSMTestBase.sol index 2b7ae2b..c599c39 100644 --- a/test/PSMTestBase.sol +++ b/test/PSMTestBase.sol @@ -20,7 +20,9 @@ contract PSMTestBase is Test { MockERC20 public usdc; MockERC20 public sDai; - IRateProviderLike public rateProvider; + IRateProviderLike public rateProvider; // Can be overridden by dsrOracle using same interface + + MockRateProvider public mockRateProvider; // Interface used for mocking modifier assertAtomicPsmValueDoesNotChange { uint256 beforeValue = _getPsmValue(); @@ -38,7 +40,7 @@ contract PSMTestBase is Test { usdc = new MockERC20("usdc", "usdc", 6); sDai = new MockERC20("sDai", "sDai", 18); - MockRateProvider mockRateProvider = new MockRateProvider(); + mockRateProvider = new MockRateProvider(); // NOTE: Using 1.25 for easy two way conversions mockRateProvider.__setConversionRate(1.25e27); diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index 1b02dd4..e3fb5d7 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -55,7 +55,7 @@ abstract contract PSMInvariantTestBase is PSMTestBase { assertApproxEqAbs( psm.getPsmTotalValue(), psm.convertToAssetValue(psm.totalShares()), - 3 + 4 ); } @@ -377,4 +377,53 @@ contract PSMInvariants_TimeBasedRateSetting_NoTransfer is PSMInvariantTestBase { } -// TODO: Add transfers (check other PR) +contract PSMInvariants_TimeBasedRateSetting_WithTransfers is PSMInvariantTestBase { + + function setUp() public override { + super.setUp(); + + DSRAuthOracle dsrOracle = new DSRAuthOracle(); + + // Redeploy PSM with new rate provider + psm = new PSM3(address(dai), address(usdc), address(sDai), address(dsrOracle)); + + // Seed the new PSM with 1e18 shares (1e18 of value) + _deposit(address(dai), BURN_ADDRESS, 1e18); + + lpHandler = new LpHandler(psm, dai, usdc, sDai, 3); + swapperHandler = new SwapperHandler(psm, dai, usdc, sDai, 3); + timeBasedRateHandler = new TimeBasedRateHandler(dsrOracle); + transferHandler = new TransferHandler(psm, dai, usdc, sDai); + + // Handler acts in the same way as a receiver on L2, so add as a data provider to the + // oracle. + dsrOracle.grantRole(dsrOracle.DATA_PROVIDER_ROLE(), address(timeBasedRateHandler)); + + rateProvider = IRateProviderLike(address(dsrOracle)); + + // Manually set initial values for the oracle through the handler to start + timeBasedRateHandler.setPotData(1e27, 1e27, block.timestamp); + + targetContract(address(lpHandler)); + targetContract(address(swapperHandler)); + targetContract(address(timeBasedRateHandler)); + targetContract(address(transferHandler)); + } + + function invariant_A_test() public view { + _checkInvariant_A(); + } + + function invariant_B() public view { + _checkInvariant_B(); + } + + function invariant_C() public view { + _checkInvariant_C(); + } + + function afterInvariant() public { + _withdrawAllPositions(); + } + +} diff --git a/test/invariant/handlers/TimeBasedRateHandler.sol b/test/invariant/handlers/TimeBasedRateHandler.sol index c23da27..e97298a 100644 --- a/test/invariant/handlers/TimeBasedRateHandler.sol +++ b/test/invariant/handlers/TimeBasedRateHandler.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import { console } from "forge-std/console.sol"; import { StdCheats } from "forge-std/StdCheats.sol"; import { StdUtils } from "forge-std/StdUtils.sol"; diff --git a/test/unit/Conversions.t.sol b/test/unit/Conversions.t.sol index ab09279..1123c9e 100644 --- a/test/unit/Conversions.t.sol +++ b/test/unit/Conversions.t.sol @@ -94,7 +94,7 @@ contract PSMConvertToAssetsTests is PSMTestBase { conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); amount = _bound(amount, 0, SDAI_TOKEN_MAX); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); assertEq(psm.convertToAssets(address(sDai), amount), amount * 1e27 / conversionRate); } @@ -114,7 +114,7 @@ contract PSMConvertToAssetValueTests is PSMConversionTestBase { assertEq(psm.convertToAssetValue(1e18), 1e18); - MockRateProvider(address(rateProvider)).__setConversionRate(2e27); + 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 @@ -129,7 +129,7 @@ contract PSMConvertToAssetValueTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(1e27); // Start lower than 1.25 for this test + mockRateProvider.__setConversionRate(1e27); // Start lower than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 1e27, @@ -146,7 +146,7 @@ contract PSMConvertToAssetValueTests is PSMConversionTestBase { // 1:1 between shares and dollar value assertEq(psm.convertToAssetValue(vars.expectedShares), initialValue); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -165,7 +165,7 @@ contract PSMConvertToAssetValueTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(2e27); // Start higher than 1.25 for this test + mockRateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 2e27, @@ -182,7 +182,7 @@ contract PSMConvertToAssetValueTests is PSMConversionTestBase { // 1:1 between shares and dollar value assertEq(psm.convertToAssetValue(vars.expectedShares), initialValue); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -234,7 +234,7 @@ contract PSMConvertToSharesTests is PSMConversionTestBase { // 80 sDAI now worth $120, 200 shares in pool with $220 of value // Each share should be worth $1.10. - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); assertEq(psm.convertToShares(10), 9); assertEq(psm.convertToShares(11), 10); @@ -253,7 +253,7 @@ contract PSMConvertToSharesTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(1e27); // Start lower than 1.25 for this test + mockRateProvider.__setConversionRate(1e27); // Start lower than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 1e27, @@ -270,7 +270,7 @@ contract PSMConvertToSharesTests is PSMConversionTestBase { // 1:1 between shares and dollar value assertEq(psm.convertToShares(initialValue), vars.expectedShares); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -289,7 +289,7 @@ contract PSMConvertToSharesTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(2e27); // Start higher than 1.25 for this test + mockRateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 2e27, @@ -306,7 +306,7 @@ contract PSMConvertToSharesTests is PSMConversionTestBase { // 1:1 between shares and dollar value assertEq(psm.convertToShares(initialValue), vars.expectedShares); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -380,7 +380,7 @@ contract PSMConvertToSharesWithDaiTests is PSMConversionTestBase { // 80 sDAI now worth $120, 200 shares in pool with $220 of value // Each share should be worth $1.10. - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); assertEq(psm.convertToShares(address(dai), 10), 9); assertEq(psm.convertToShares(address(dai), 11), 10); @@ -402,7 +402,7 @@ contract PSMConvertToSharesWithDaiTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(1e27); // Start lower than 1.25 for this test + mockRateProvider.__setConversionRate(1e27); // Start lower than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 1e27, @@ -419,7 +419,7 @@ contract PSMConvertToSharesWithDaiTests is PSMConversionTestBase { // 1:1 between shares and dollar value assertEq(psm.convertToShares(address(dai), initialValue), vars.expectedShares); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -438,7 +438,7 @@ contract PSMConvertToSharesWithDaiTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(2e27); // Start higher than 1.25 for this test + mockRateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 2e27, @@ -455,7 +455,7 @@ contract PSMConvertToSharesWithDaiTests is PSMConversionTestBase { // 1:1 between shares and dollar value assertEq(psm.convertToShares(address(dai), initialValue), vars.expectedShares); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -520,7 +520,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMConversionTestBase { // 80 sDAI now worth $120, 200 shares in pool with $220 of value // Each share should be worth $1.10. - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); assertEq(psm.convertToShares(address(usdc), 10), 9.090909090909e12); assertEq(psm.convertToShares(address(usdc), 11), 10e12); @@ -539,7 +539,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(1e27); // Start lower than 1.25 for this test + mockRateProvider.__setConversionRate(1e27); // Start lower than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 1e27, @@ -560,7 +560,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMConversionTestBase { vars.expectedShares / 1e12 * 1e12 ); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -596,7 +596,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(2e27); // Start higher than 1.25 for this test + mockRateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 2e27, @@ -617,7 +617,7 @@ contract PSMConvertToSharesWithUsdcTests is PSMConversionTestBase { vars.expectedShares / 1e12 * 1e12 ); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -673,7 +673,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { amount = _bound(amount, 1000, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 1000e27); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); assertEq(psm.convertToShares(address(sDai), amount), amount * conversionRate / 1e27); } @@ -704,7 +704,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { // 80 sDAI now worth $120, 200 shares in pool with $220 of value // Each share should be worth $1.10. Since 1 sDAI is now worth 1.5 USDC, 1 sDAI is worth // 1.50/1.10 = 1.3636... shares - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); // TODO: Reinvestigate this, interesting difference in rounding assertEq(psm.convertToShares(address(sDai), 1), 0); @@ -727,7 +727,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { public { // NOTE: Not using 1e27 for this test because initialSDaiValue needs to be different - MockRateProvider(address(rateProvider)).__setConversionRate(1.1e27); // Start lower than 1.25 for this test + mockRateProvider.__setConversionRate(1.1e27); // Start lower than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 1.1e27, @@ -749,7 +749,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { 1 ); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; @@ -791,7 +791,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { ) public { - MockRateProvider(address(rateProvider)).__setConversionRate(2e27); // Start higher than 1.25 for this test + mockRateProvider.__setConversionRate(2e27); // Start higher than 1.25 for this test FuzzVars memory vars = _setUpConversionFuzzTest( 2e27, @@ -813,7 +813,7 @@ contract PSMConvertToSharesWithSDaiTests is PSMConversionTestBase { 1 ); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 newValue = vars.daiAmount + vars.usdcAmount * 1e12 + vars.sDaiAmount * conversionRate / 1e27; diff --git a/test/unit/Deposit.t.sol b/test/unit/Deposit.t.sol index 2e3566f..32f56c6 100644 --- a/test/unit/Deposit.t.sol +++ b/test/unit/Deposit.t.sol @@ -279,7 +279,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.convertToAssetValue(psm.shares(receiver1)), 225e18); - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); // Total shares / (100 USDC + 150 sDAI value) uint256 expectedConversionRate = 225 * 1e18 / 250; @@ -375,7 +375,7 @@ contract PSMDepositTests is PSMTestBase { assertEq(psm.shares(user1), 0); assertEq(psm.shares(receiver1), receiver1Shares); - MockRateProvider(address(rateProvider)).__setConversionRate(newRate); + mockRateProvider.__setConversionRate(newRate); vm.startPrank(user2); diff --git a/test/unit/Getters.t.sol b/test/unit/Getters.t.sol index 11744cd..408d046 100644 --- a/test/unit/Getters.t.sol +++ b/test/unit/Getters.t.sol @@ -72,7 +72,7 @@ contract PSMHarnessTests is PSMTestBase { assertEq(psmHarness.getAsset2Value(3e18), 3.75e18); assertEq(psmHarness.getAsset2Value(4e18), 5e18); - MockRateProvider(address(rateProvider)).__setConversionRate(1.6e27); + mockRateProvider.__setConversionRate(1.6e27); assertEq(psmHarness.getAsset2Value(1), 1); assertEq(psmHarness.getAsset2Value(2), 3); @@ -84,7 +84,7 @@ contract PSMHarnessTests is PSMTestBase { assertEq(psmHarness.getAsset2Value(3e18), 4.8e18); assertEq(psmHarness.getAsset2Value(4e18), 6.4e18); - MockRateProvider(address(rateProvider)).__setConversionRate(0.8e27); + mockRateProvider.__setConversionRate(0.8e27); assertEq(psmHarness.getAsset2Value(1), 0); assertEq(psmHarness.getAsset2Value(2), 1); @@ -101,7 +101,7 @@ contract PSMHarnessTests is PSMTestBase { conversionRate = _bound(conversionRate, 0, 1000e27); amount = _bound(amount, 0, SDAI_TOKEN_MAX); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); assertEq(psmHarness.getAsset2Value(amount), amount * conversionRate / 1e27); } @@ -184,11 +184,11 @@ contract GetPsmTotalValueTests is PSMTestBase { assertEq(psm.getPsmTotalValue(), 3.25e18); - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); assertEq(psm.getPsmTotalValue(), 3.5e18); - MockRateProvider(address(rateProvider)).__setConversionRate(0.8e27); + mockRateProvider.__setConversionRate(0.8e27); assertEq(psm.getPsmTotalValue(), 2.8e18); } @@ -202,7 +202,7 @@ contract GetPsmTotalValueTests is PSMTestBase { assertEq(psm.getPsmTotalValue(), 3.25e18); - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); assertEq(psm.getPsmTotalValue(), 3.5e18); @@ -228,7 +228,7 @@ contract GetPsmTotalValueTests is PSMTestBase { usdc.mint(address(psm), usdcAmount); sDai.mint(address(psm), sDaiAmount); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); assertEq( psm.getPsmTotalValue(), diff --git a/test/unit/Previews.t.sol b/test/unit/Previews.t.sol index 74f5c02..2d7e2f0 100644 --- a/test/unit/Previews.t.sol +++ b/test/unit/Previews.t.sol @@ -61,7 +61,7 @@ contract PSMPreviewSwapDaiAssetInTests is PSMTestBase { amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate; @@ -94,7 +94,7 @@ contract PSMPreviewSwapUSDCAssetInTests is PSMTestBase { amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; @@ -115,7 +115,7 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * conversionRate / 1e27; @@ -132,7 +132,7 @@ contract PSMPreviewSwapSDaiAssetInTests is PSMTestBase { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.0001e27, 1000e27); // 0.01% to 100,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; diff --git a/test/unit/Rounding.t.sol b/test/unit/Rounding.t.sol index 725dcee..641c32f 100644 --- a/test/unit/Rounding.t.sol +++ b/test/unit/Rounding.t.sol @@ -20,7 +20,7 @@ contract RoundingTests is PSMTestBase { _deposit(address(usdc), address(this), USDC_TOKEN_MAX); // Set an exchange rate that will cause rounding - MockRateProvider(address(rateProvider)).__setConversionRate(1.25e27 * uint256(100) / 99); + mockRateProvider.__setConversionRate(1.25e27 * uint256(100) / 99); } function test_roundAgainstUser_dai() public { @@ -131,7 +131,7 @@ contract RoundingTests is PSMTestBase { amount1 = _bound(amount1, 1, tokenMax); amount2 = _bound(amount2, 1, tokenMax); - MockRateProvider(address(rateProvider)).__setConversionRate(rate1); + mockRateProvider.__setConversionRate(rate1); _deposit(address(asset), address(user1), amount1); @@ -144,7 +144,7 @@ contract RoundingTests is PSMTestBase { assertApproxEqAbs(asset.balanceOf(address(user1)), amount1, roundingTolerance); assertLe(asset.balanceOf(address(user1)), amount1); - MockRateProvider(address(rateProvider)).__setConversionRate(rate2); + mockRateProvider.__setConversionRate(rate2); _deposit(address(asset), address(user2), amount2); diff --git a/test/unit/Swaps.t.sol b/test/unit/Swaps.t.sol index a106069..67739d8 100644 --- a/test/unit/Swaps.t.sol +++ b/test/unit/Swaps.t.sol @@ -222,7 +222,7 @@ contract PSMSwapDaiAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, DAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate; @@ -276,7 +276,7 @@ contract PSMSwapUsdcAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, USDC_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * 1e27 / conversionRate * 1e12; @@ -316,7 +316,7 @@ contract PSMSwapSDaiAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * conversionRate / 1e27; @@ -336,7 +336,7 @@ contract PSMSwapSDaiAssetInTests is PSMSwapSuccessTestsBase { amountIn = _bound(amountIn, 1, SDAI_TOKEN_MAX); conversionRate = _bound(conversionRate, 0.01e27, 100e27); // 1% to 10,000% conversion rate - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 amountOut = amountIn * conversionRate / 1e27 / 1e12; diff --git a/test/unit/Withdraw.t.sol b/test/unit/Withdraw.t.sol index 1a21805..e2a65ed 100644 --- a/test/unit/Withdraw.t.sol +++ b/test/unit/Withdraw.t.sol @@ -435,7 +435,7 @@ contract PSMWithdrawTests is PSMTestBase { assertEq(psm.convertToShares(1e18), 1e18); - MockRateProvider(address(rateProvider)).__setConversionRate(1.5e27); + mockRateProvider.__setConversionRate(1.5e27); // Total shares / (100 USDC + 150 sDAI value) uint256 expectedConversionRate = 225 * 1e18 / 250; @@ -525,7 +525,7 @@ contract PSMWithdrawTests is PSMTestBase { _deposit(address(usdc), user1, usdcAmount); _deposit(address(sDai), user2, sDaiAmount); - MockRateProvider(address(rateProvider)).__setConversionRate(conversionRate); + mockRateProvider.__setConversionRate(conversionRate); uint256 user1Shares = usdcAmount * 1e12; uint256 user2Shares = sDaiAmount * 125/100; From 5f1e228f04fb62a91c6d4dcd7bccf603af2597f5 Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Wed, 3 Jul 2024 08:39:41 -0400 Subject: [PATCH 95/96] fix: test names --- test/invariant/Invariants.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index e3fb5d7..e336e92 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -359,7 +359,7 @@ contract PSMInvariants_TimeBasedRateSetting_NoTransfer is PSMInvariantTestBase { targetContract(address(timeBasedRateHandler)); } - function invariant_A_test() public view { + function invariant_A() public view { _checkInvariant_A(); } @@ -410,7 +410,7 @@ contract PSMInvariants_TimeBasedRateSetting_WithTransfers is PSMInvariantTestBas targetContract(address(transferHandler)); } - function invariant_A_test() public view { + function invariant_A() public view { _checkInvariant_A(); } From 4a1e43d6a936ec25734708634f3d434cfcdf21bf Mon Sep 17 00:00:00 2001 From: lucas-manuel Date: Thu, 4 Jul 2024 06:29:10 -0400 Subject: [PATCH 96/96] fix: update dsr value, change chi approach --- test/invariant/Invariants.t.sol | 4 ++-- test/invariant/handlers/TimeBasedRateHandler.sol | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/test/invariant/Invariants.t.sol b/test/invariant/Invariants.t.sol index e336e92..39c6faf 100644 --- a/test/invariant/Invariants.t.sol +++ b/test/invariant/Invariants.t.sol @@ -352,7 +352,7 @@ contract PSMInvariants_TimeBasedRateSetting_NoTransfer is PSMInvariantTestBase { rateProvider = IRateProviderLike(address(dsrOracle)); // Manually set initial values for the oracle through the handler to start - timeBasedRateHandler.setPotData(1e27, 1e27, block.timestamp); + timeBasedRateHandler.setPotData(1e27, block.timestamp); targetContract(address(lpHandler)); targetContract(address(swapperHandler)); @@ -402,7 +402,7 @@ contract PSMInvariants_TimeBasedRateSetting_WithTransfers is PSMInvariantTestBas rateProvider = IRateProviderLike(address(dsrOracle)); // Manually set initial values for the oracle through the handler to start - timeBasedRateHandler.setPotData(1e27, 1e27, block.timestamp); + timeBasedRateHandler.setPotData(1e27, block.timestamp); targetContract(address(lpHandler)); targetContract(address(swapperHandler)); diff --git a/test/invariant/handlers/TimeBasedRateHandler.sol b/test/invariant/handlers/TimeBasedRateHandler.sol index e97298a..2eee7d2 100644 --- a/test/invariant/handlers/TimeBasedRateHandler.sol +++ b/test/invariant/handlers/TimeBasedRateHandler.sol @@ -13,7 +13,7 @@ contract TimeBasedRateHandler is StdCheats, StdUtils { uint256 public chi; uint256 public rho; - uint256 constant ONE_HUNDRED_PCT_APY_DSR = 1.00000002197955315123915302e27; + uint256 constant ONE_HUNDRED_PCT_APY_DSR = 1.000000021979553151239153027e27; DSRAuthOracle public dsrOracle; @@ -24,13 +24,15 @@ contract TimeBasedRateHandler is StdCheats, StdUtils { } // This acts as a receiver on an L2. - // Note that the chi value is not derived from previous values, this is to test if - // PSM will work as expected with different chi values. - function setPotData(uint256 newDsr, uint256 newChi, uint256 newRho) external { + function setPotData(uint256 newDsr, uint256 newRho) external { dsr = _bound(newDsr, 1e27, ONE_HUNDRED_PCT_APY_DSR); - chi = _bound(newChi, chi, 1e27); rho = _bound(newRho, rho, block.timestamp); + // If chi hasn't been set yet, set to 1e27, else recalculate it in the same way it would + // happen during a refresh. + uint256 rate = dsrOracle.getConversionRate(); + uint256 chi = rate == 0 ? 1e27 : rate; + dsrOracle.setPotData(IDSROracle.PotData({ dsr: uint96(dsr), chi: uint120(chi),