diff --git a/src/MigrationActions.sol b/src/MigrationActions.sol new file mode 100644 index 0000000..66d2b88 --- /dev/null +++ b/src/MigrationActions.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.25; + +import { IERC20 } from "lib/forge-std/src/interfaces/IERC20.sol"; +import { IERC4626 } from "lib/forge-std/src/interfaces/IERC4626.sol"; + +interface JoinLike { + function vat() external view returns (VatLike); + function join(address, uint256) external; + function exit(address, uint256) external; +} + +interface VatLike { + function hope(address) external; +} + +/** + * @notice Actions for migrating from DAI/sDAI to NST/sNST. + * @dev Also contains 1 downgrade path from NST to DAI for convenience. + */ +contract MigrationActions { + + IERC20 public immutable dai; + IERC20 public immutable nst; + IERC4626 public immutable sdai; + IERC4626 public immutable snst; + + VatLike public immutable vat; + JoinLike public immutable daiJoin; + JoinLike public immutable nstJoin; + + constructor( + address _sdai, + address _snst, + address _daiJoin, + address _nstJoin + ) { + sdai = IERC4626(_sdai); + snst = IERC4626(_snst); + + dai = IERC20(sdai.asset()); + nst = IERC20(snst.asset()); + + daiJoin = JoinLike(_daiJoin); + nstJoin = JoinLike(_nstJoin); + vat = daiJoin.vat(); + + // Infinite approvals + dai.approve(_daiJoin, type(uint256).max); + nst.approve(_nstJoin, type(uint256).max); + nst.approve(_snst, type(uint256).max); + + // Vat permissioning + vat.hope(_daiJoin); + vat.hope(_nstJoin); + } + + /** + * @notice Migrate `assetsIn` of `dai` to `nst`. + * @param receiver The receiver of `nst`. + * @param assetsIn The amount of `dai` to migrate. + */ + function migrateDAIToNST(address receiver, uint256 assetsIn) public { + dai.transferFrom(msg.sender, address(this), assetsIn); + _migrateDAIToNST(receiver, assetsIn); + } + + /** + * @notice Migrate `assetsIn` of `dai` to `snst`. + * @param receiver The receiver of `snst`. + * @param assetsIn The amount of `dai` to migrate. + * @return sharesOut The amount of `snst` shares received. + */ + function migrateDAIToSNST(address receiver, uint256 assetsIn) external returns (uint256 sharesOut) { + migrateDAIToNST(address(this), assetsIn); + sharesOut = snst.deposit(assetsIn, receiver); + } + + /** + * @notice Migrate `assetsIn` of `sdai` to `nst`. + * @param receiver The receiver of `nst`. + * @param assetsIn The amount of `sdai` to migrate in assets. + */ + function migrateSDAIAssetsToNST(address receiver, uint256 assetsIn) public { + sdai.withdraw(assetsIn, address(this), msg.sender); + _migrateDAIToNST(receiver, assetsIn); + } + + /** + * @notice Migrate `sharesIn` of `sdai` to `nst`. + * @param receiver The receiver of `nst`. + * @param sharesIn The amount of `sdai` to migrate in shares. + * @return assetsOut The amount of `nst` assets received. + */ + function migrateSDAISharesToNST(address receiver, uint256 sharesIn) public returns (uint256 assetsOut) { + assetsOut = sdai.redeem(sharesIn, address(this), msg.sender); + _migrateDAIToNST(receiver, assetsOut); + } + + /** + * @notice Migrate `assetsIn` of `sdai` (denominated in `dai`) to `snst`. + * @param receiver The receiver of `snst`. + * @param assetsIn The amount of `sdai` to migrate (denominated in `dai`). + * @return sharesOut The amount of `snst` shares received. + */ + function migrateSDAIAssetsToSNST(address receiver, uint256 assetsIn) external returns (uint256 sharesOut) { + migrateSDAIAssetsToNST(address(this), assetsIn); + sharesOut = snst.deposit(assetsIn, receiver); + } + + /** + * @notice Migrate `sharesIn` of `sdai` to `snst`. + * @param receiver The receiver of `snst`. + * @param sharesIn The amount of `sdai` to migrate in shares. + * @return sharesOut The amount of `snst` shares received. + */ + function migrateSDAISharesToSNST(address receiver, uint256 sharesIn) external returns (uint256 sharesOut) { + uint256 assets = migrateSDAISharesToNST(address(this), sharesIn); + sharesOut = snst.deposit(assets, receiver); + } + + /** + * @notice Downgrade `assetsIn` of `nst` to `dai`. + * @param receiver The receiver of `dai`. + * @param assetsIn The amount of `nst` to downgrade. + */ + function downgradeNSTToDAI(address receiver, uint256 assetsIn) external { + nst.transferFrom(msg.sender, address(this), assetsIn); + nstJoin.join(address(this), assetsIn); + daiJoin.exit(receiver, assetsIn); + } + + /**********************************************************************************************/ + /*** Internal helper functions ***/ + /**********************************************************************************************/ + + function _migrateDAIToNST(address receiver, uint256 amount) internal { + daiJoin.join(address(this), amount); + nstJoin.exit(receiver, amount); + } + +} diff --git a/test/MigrationActions.t.sol b/test/MigrationActions.t.sol new file mode 100644 index 0000000..e2cae2f --- /dev/null +++ b/test/MigrationActions.t.sol @@ -0,0 +1,737 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import { MockERC20 } from "lib/erc20-helpers/src/MockERC20.sol"; + +import { VatMock } from "./mocks/VatMock.sol"; +import { JoinMock } from "./mocks/JoinMock.sol"; +import { ERC4626Mock } from "./mocks/ERC4626Mock.sol"; + +import { MigrationActions } from "src/MigrationActions.sol"; + +abstract contract MigrationActionsBase is Test { + + MockERC20 public dai; + MockERC20 public nst; + ERC4626Mock public sdai; + ERC4626Mock public snst; + + VatMock public vat; + JoinMock public daiJoin; + JoinMock public nstJoin; + + MigrationActions public actions; + + address receiver = makeAddr("receiver"); + + function setUp() public { + dai = new MockERC20("DAI", "DAI", 18); + nst = new MockERC20("NST", "NST", 18); + sdai = new ERC4626Mock(dai, "sDAI", "sDAI", 18); + snst = new ERC4626Mock(nst, "sNST", "sNST", 18); + + vat = new VatMock(); + daiJoin = new JoinMock(vat, dai); + nstJoin = new JoinMock(vat, nst); + + // Set the different exchange rates for different asset/share conversion + sdai.__setShareConversionRate(2e18); + snst.__setShareConversionRate(1.25e18); + + // Give some existing balance to represent existing ERC20s + vat.__setDaiBalance(address(daiJoin), 1_000_000e45); + vat.__setDaiBalance(address(nstJoin), 1_000_000e45); + + actions = new MigrationActions( + address(sdai), + address(snst), + address(daiJoin), + address(nstJoin) + ); + } + + function _assertBalances( + address user, + uint256 daiBalance, + uint256 sdaiBalance, + uint256 nstBalance, + uint256 snstBalance + ) internal view { + assertEq(dai.balanceOf(user), daiBalance); + assertEq(sdai.balanceOf(user), sdaiBalance); + assertEq(nst.balanceOf(user), nstBalance); + assertEq(snst.balanceOf(user), snstBalance); + } + +} + +contract MigrationActionsConstructorTests is MigrationActionsBase { + + function test_constructor() public { + // For coverage + actions = new MigrationActions( + address(sdai), + address(snst), + address(daiJoin), + address(nstJoin) + ); + + assertEq(address(actions.dai()), address(dai)); + assertEq(address(actions.sdai()), address(sdai)); + assertEq(address(actions.nst()), address(nst)); + assertEq(address(actions.snst()), address(snst)); + assertEq(address(actions.vat()), address(vat)); + assertEq(address(actions.daiJoin()), address(daiJoin)); + assertEq(address(actions.nstJoin()), address(nstJoin)); + + assertEq(dai.allowance(address(actions), address(daiJoin)), type(uint256).max); + assertEq(nst.allowance(address(actions), address(nstJoin)), type(uint256).max); + assertEq(nst.allowance(address(actions), address(snst)), type(uint256).max); + + assertEq(vat.can(address(actions), address(daiJoin)), 1); + assertEq(vat.can(address(actions), address(nstJoin)), 1); + } + +} + +contract MigrationActionsMigrateDAIToNSTTests is MigrationActionsBase { + + function test_migrateDAIToNST_insufficientBalance_boundary() public { + dai.approve(address(actions), 100e18); + dai.mint(address(this), 100e18 - 1); + + vm.expectRevert(stdError.arithmeticError); + actions.migrateDAIToNST(receiver, 100e18); + + dai.mint(address(this), 1); + + actions.migrateDAIToNST(receiver, 100e18); + } + + function test_migrateDAIToNST_insufficientApproval_boundary() public { + dai.approve(address(actions), 100e18 - 1); + dai.mint(address(this), 100e18); + + vm.expectRevert(stdError.arithmeticError); + actions.migrateDAIToNST(receiver, 100e18); + + dai.approve(address(actions), 100e18); + + actions.migrateDAIToNST(receiver, 100e18); + } + + function test_migrateDAIToNST_differentReceiver() public { + dai.approve(address(actions), 100e18); + dai.mint(address(this), 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 100e18, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + + actions.migrateDAIToNST(receiver, 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 100e18, + snstBalance: 0 + }); + } + + function test_migrateDAIToNST_sameReceiver() public { + dai.approve(address(actions), 100e18); + dai.mint(address(this), 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 100e18, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + + actions.migrateDAIToNST(address(this), 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 100e18, + snstBalance: 0 + }); + } + +} + +contract MigrationActionsMigrateDAIToSNSTTests is MigrationActionsBase { + + function test_migrateDAIToSNST_insufficientBalance_boundary() public { + dai.approve(address(actions), 100e18); + dai.mint(address(this), 100e18 - 1); + + vm.expectRevert(stdError.arithmeticError); + actions.migrateDAIToSNST(receiver, 100e18); + + dai.mint(address(this), 1); + + actions.migrateDAIToSNST(receiver, 100e18); + } + + function test_migrateDAIToSNST_insufficientApproval_boundary() public { + dai.approve(address(actions), 100e18 - 1); + dai.mint(address(this), 100e18); + + vm.expectRevert(stdError.arithmeticError); + actions.migrateDAIToSNST(receiver, 100e18); + + dai.approve(address(actions), 100e18); + + actions.migrateDAIToSNST(receiver, 100e18); + } + + function test_migrateDAIToSNST_differentReceiver() public { + dai.approve(address(actions), 100e18); + dai.mint(address(this), 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 100e18, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + + uint256 sharesOut = actions.migrateDAIToSNST(receiver, 100e18); + assertEq(sharesOut, 80e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 80e18 + }); + } + + function test_migrateDAIToSNST_sameReceiver() public { + dai.approve(address(actions), 100e18); + dai.mint(address(this), 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 100e18, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + + uint256 sharesOut = actions.migrateDAIToSNST(address(this), 100e18); + assertEq(sharesOut, 80e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 80e18 + }); + } + +} + +contract MigrationActionsMigrateSDAIAssetsToNSTTests is MigrationActionsBase { + + function test_migrateSDAIAssetsToNST_insufficientBalance_boundary() public { + dai.mint(address(sdai), 100e18); // Ensure dai is available in sdai + sdai.approve(address(actions), 50e18); + sdai.mint(address(this), 50e18 - 1); + + vm.expectRevert(stdError.arithmeticError); + actions.migrateSDAIAssetsToNST(receiver, 100e18); + + sdai.mint(address(this), 1); + + actions.migrateSDAIAssetsToNST(receiver, 100e18); + } + + function test_migrateSDAIAssetsToNST_insufficientApproval_boundary() public { + dai.mint(address(sdai), 100e18); + sdai.approve(address(actions), 50e18 - 1); + sdai.mint(address(this), 50e18); + + vm.expectRevert(stdError.arithmeticError); + actions.migrateSDAIAssetsToNST(receiver, 100e18); + + sdai.approve(address(actions), 50e18); + + actions.migrateSDAIAssetsToNST(receiver, 100e18); + } + + function test_migrateSDAIAssetsToNST_differentReceiver() public { + dai.mint(address(sdai), 100e18); + sdai.approve(address(actions), 50e18); + sdai.mint(address(this), 50e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 50e18, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + + actions.migrateSDAIAssetsToNST(receiver, 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 100e18, + snstBalance: 0 + }); + } + + function test_migrateSDAIAssetsToNST_sameReceiver() public { + dai.mint(address(sdai), 100e18); + sdai.approve(address(actions), 50e18); + sdai.mint(address(this), 50e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 50e18, + nstBalance: 0, + snstBalance: 0 + }); + + actions.migrateSDAIAssetsToNST(address(this), 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 100e18, + snstBalance: 0 + }); + } + +} + +contract MigrationActionsMigrateSDAISharesToNSTTests is MigrationActionsBase { + + function test_migrateSDAISharesToNST_insufficientBalance_boundary() public { + dai.mint(address(sdai), 100e18); // Ensure dai is available in sdai + sdai.approve(address(actions), 50e18); + sdai.mint(address(this), 50e18 - 1); + + vm.expectRevert(stdError.arithmeticError); + actions.migrateSDAISharesToNST(receiver, 50e18); + + sdai.mint(address(this), 1); + + actions.migrateSDAISharesToNST(receiver, 50e18); + } + + function test_migrateSDAISharesToNST_insufficientApproval_boundary() public { + dai.mint(address(sdai), 100e18); + sdai.approve(address(actions), 50e18 - 1); + sdai.mint(address(this), 50e18); + + vm.expectRevert(stdError.arithmeticError); + actions.migrateSDAISharesToNST(receiver, 50e18); + + sdai.approve(address(actions), 50e18); + + actions.migrateSDAISharesToNST(receiver, 50e18); + } + + function test_migrateSDAISharesToNST_differentReceiver() public { + dai.mint(address(sdai), 100e18); + sdai.approve(address(actions), 50e18); + sdai.mint(address(this), 50e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 50e18, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + + uint256 assetsOut = actions.migrateSDAISharesToNST(receiver, 50e18); + assertEq(assetsOut, 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 100e18, + snstBalance: 0 + }); + } + + function test_migrateSDAISharesToNST_sameReceiver() public { + dai.mint(address(sdai), 100e18); + sdai.approve(address(actions), 50e18); + sdai.mint(address(this), 50e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 50e18, + nstBalance: 0, + snstBalance: 0 + }); + + uint256 assetsOut = actions.migrateSDAISharesToNST(address(this), 50e18); + assertEq(assetsOut, 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 100e18, + snstBalance: 0 + }); + } + +} + +contract MigrationActionsMigrateSDAIAssetsToSNSTTests is MigrationActionsBase { + + function test_migrateSDAIAssetsToSNST_insufficientBalance_boundary() public { + dai.mint(address(sdai), 100e18); // Ensure dai is available in sdai + sdai.approve(address(actions), 50e18); + sdai.mint(address(this), 50e18 - 1); + + vm.expectRevert(stdError.arithmeticError); + actions.migrateSDAIAssetsToSNST(receiver, 100e18); + + sdai.mint(address(this), 1); + + actions.migrateSDAIAssetsToSNST(receiver, 100e18); + } + + function test_migrateSDAIAssetsToSNST_insufficientApproval_boundary() public { + dai.mint(address(sdai), 100e18); + sdai.approve(address(actions), 50e18 - 1); + sdai.mint(address(this), 50e18); + + vm.expectRevert(stdError.arithmeticError); + actions.migrateSDAIAssetsToSNST(receiver, 100e18); + + sdai.approve(address(actions), 50e18); + + actions.migrateSDAIAssetsToSNST(receiver, 100e18); + } + + function test_migrateSDAIAssetsToSNST_differentReceiver() public { + dai.mint(address(sdai), 100e18); + sdai.approve(address(actions), 50e18); + sdai.mint(address(this), 50e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 50e18, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + + uint256 sharesOut = actions.migrateSDAIAssetsToSNST(receiver, 100e18); + assertEq(sharesOut, 80e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 80e18 + }); + } + + function test_migrateSDAIAssetsToSNST_sameReceiver() public { + dai.mint(address(sdai), 100e18); + sdai.approve(address(actions), 50e18); + sdai.mint(address(this), 50e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 50e18, + nstBalance: 0, + snstBalance: 0 + }); + + uint256 sharesOut = actions.migrateSDAIAssetsToSNST(address(this), 100e18); + assertEq(sharesOut, 80e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 80e18 + }); + } + +} + +contract MigrationActionsMigrateSDAISharesToSNSTTests is MigrationActionsBase { + + function test_migrateSDAISharesToSNST_insufficientBalance_boundary() public { + dai.mint(address(sdai), 100e18); // Ensure dai is available in sdai + sdai.approve(address(actions), 50e18); + sdai.mint(address(this), 50e18 - 1); + + vm.expectRevert(stdError.arithmeticError); + actions.migrateSDAISharesToSNST(receiver, 50e18); + + sdai.mint(address(this), 1); + + actions.migrateSDAISharesToSNST(receiver, 50e18); + } + + function test_migrateSDAISharesToSNST_insufficientApproval_boundary() public { + dai.mint(address(sdai), 100e18); + sdai.approve(address(actions), 50e18 - 1); + sdai.mint(address(this), 50e18); + + vm.expectRevert(stdError.arithmeticError); + actions.migrateSDAISharesToSNST(receiver, 50e18); + + sdai.approve(address(actions), 50e18); + + actions.migrateSDAISharesToSNST(receiver, 50e18); + } + + function test_migrateSDAISharesToSNST_differentReceiver() public { + dai.mint(address(sdai), 100e18); + sdai.approve(address(actions), 50e18); + sdai.mint(address(this), 50e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 50e18, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + + uint256 sharesOut = actions.migrateSDAISharesToSNST(receiver, 50e18); + assertEq(sharesOut, 80e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 80e18 + }); + } + + function test_migrateSDAISharesToSNST_sameReceiver() public { + dai.mint(address(sdai), 100e18); + sdai.approve(address(actions), 50e18); + sdai.mint(address(this), 50e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 50e18, + nstBalance: 0, + snstBalance: 0 + }); + + uint256 sharesOut = actions.migrateSDAISharesToSNST(address(this), 50e18); + assertEq(sharesOut, 80e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 80e18 + }); + } + +} + +contract MigrationActionsDowngradeNSTToDAITests is MigrationActionsBase { + + function test_downgradeNSTToDAI_insufficientBalance_boundary() public { + nst.approve(address(actions), 100e18); + nst.mint(address(this), 100e18 - 1); + + vm.expectRevert(stdError.arithmeticError); + actions.downgradeNSTToDAI(receiver, 100e18); + + nst.mint(address(this), 1); + + actions.downgradeNSTToDAI(receiver, 100e18); + } + + function test_downgradeNSTToDAI_insufficientApproval_boundary() public { + nst.approve(address(actions), 100e18 - 1); + nst.mint(address(this), 100e18); + + vm.expectRevert(stdError.arithmeticError); + actions.downgradeNSTToDAI(receiver, 100e18); + + nst.approve(address(actions), 100e18); + + actions.downgradeNSTToDAI(receiver, 100e18); + } + + function test_downgradeNSTToDAI_differentReceiver() public { + nst.approve(address(actions), 100e18); + nst.mint(address(this), 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 100e18, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + + actions.downgradeNSTToDAI(receiver, 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + _assertBalances({ + user: receiver, + daiBalance: 100e18, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + } + + function test_downgradeNSTToDAI_sameReceiver() public { + nst.approve(address(actions), 100e18); + nst.mint(address(this), 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 0, + sdaiBalance: 0, + nstBalance: 100e18, + snstBalance: 0 + }); + + actions.downgradeNSTToDAI(address(this), 100e18); + + _assertBalances({ + user: address(this), + daiBalance: 100e18, + sdaiBalance: 0, + nstBalance: 0, + snstBalance: 0 + }); + } + +} diff --git a/test/mocks/JoinMock.sol b/test/mocks/JoinMock.sol new file mode 100644 index 0000000..ab2558f --- /dev/null +++ b/test/mocks/JoinMock.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +import { MockERC20 } from "lib/erc20-helpers/src/MockERC20.sol"; + +import { VatMock } from "./VatMock.sol"; + +contract JoinMock { + + VatMock public vat; + MockERC20 public dai; + + constructor(VatMock _vat, MockERC20 _dai) { + vat = _vat; + dai = _dai; + } + + function join(address usr, uint wad) external { + vat.move(address(this), usr, wad * 1e27); + dai.burn(msg.sender, wad); + } + + function exit(address usr, uint wad) external { + vat.move(msg.sender, address(this), wad * 1e27); + dai.mint(usr, wad); + } + +} diff --git a/test/mocks/VatMock.sol b/test/mocks/VatMock.sol new file mode 100644 index 0000000..9ff415e --- /dev/null +++ b/test/mocks/VatMock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +contract VatMock { + + mapping(address => mapping (address => uint256)) public can; + + mapping (address => uint256) public dai; + + function hope(address usr) external { + can[msg.sender][usr] = 1; + } + + function move(address src, address dst, uint256 amount) external { + require(msg.sender == src || can[src][msg.sender] == 1, "Vat/not-allowed"); + + dai[src] -= amount; + dai[dst] += amount; + } + + function __setDaiBalance(address usr, uint256 amount) external { + dai[usr] = amount; + } + +}