diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml new file mode 100644 index 0000000..4c123f9 --- /dev/null +++ b/.github/workflows/test-integration.yml @@ -0,0 +1,30 @@ +name: Forge Integration Tests + +on: + push: + branches: + - main + pull_request: + +permissions: write-all + +jobs: + check: + name: Integration Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Run Forge build + run: | + forge --version + make sizes + id: build + + - name: Run Forge integration tests + run: make integration profile=ci diff --git a/test/Integration.t.sol b/test/integration/Protocol.t.sol similarity index 92% rename from test/Integration.t.sol rename to test/integration/Protocol.t.sol index b850988..bad4d18 100644 --- a/test/Integration.t.sol +++ b/test/integration/Protocol.t.sol @@ -2,37 +2,37 @@ pragma solidity 0.8.23; -import { Test, console2 } from "../lib/forge-std/src/Test.sol"; - -import { IBatchGovernor } from "../lib/ttg/src/abstract/interfaces/IBatchGovernor.sol"; -import { IDistributionVault } from "../lib/ttg/src/interfaces/IDistributionVault.sol"; -import { IEarnerRateModel } from "../lib/protocol/src/rateModels/interfaces/IEarnerRateModel.sol"; -import { IEmergencyGovernor } from "../lib/ttg/src/interfaces/IEmergencyGovernor.sol"; -import { IEmergencyGovernorDeployer } from "../lib/ttg/src/interfaces/IEmergencyGovernorDeployer.sol"; -import { IERC20 } from "../lib/protocol/lib/common/src/interfaces/IERC20.sol"; -import { IERC5805 } from "../lib/ttg/src/abstract/interfaces/IERC5805.sol"; -import { IGovernor } from "../lib/ttg/src/abstract/interfaces/IGovernor.sol"; -import { IMinterGateway } from "../lib/protocol/src/interfaces/IMinterGateway.sol"; -import { IMinterRateModel } from "../lib/protocol/src/rateModels/interfaces/IMinterRateModel.sol"; -import { IMToken } from "../lib/protocol/src/interfaces/IMToken.sol"; -import { IPowerToken } from "../lib/ttg/src/interfaces/IPowerToken.sol"; -import { IPowerTokenDeployer } from "../lib/ttg/src/interfaces/IPowerTokenDeployer.sol"; -import { IRegistrar } from "../lib/ttg/src/interfaces/IRegistrar.sol"; -import { IStandardGovernor } from "../lib/ttg/src/interfaces/IStandardGovernor.sol"; -import { IStandardGovernorDeployer } from "../lib/ttg/src/interfaces/IStandardGovernorDeployer.sol"; -import { IZeroGovernor } from "../lib/ttg/src/interfaces/IZeroGovernor.sol"; -import { IZeroToken } from "../lib/ttg/src/interfaces/IZeroToken.sol"; - -import { PureEpochs } from "../lib/ttg/src/libs/PureEpochs.sol"; - -import { InitialAccountsFixture } from "./fixture/InitialAccountsFixture.sol"; - -import { IWETH } from "./utils/IWETH.sol"; -import { TestUtils } from "./utils/TestUtils.sol"; - -import { DeployBase } from "../script/DeployBase.sol"; - -contract IntegrationTests is TestUtils, DeployBase, InitialAccountsFixture { +import { Test, console2 } from "../../lib/forge-std/src/Test.sol"; + +import { IBatchGovernor } from "../../lib/ttg/src/abstract/interfaces/IBatchGovernor.sol"; +import { IDistributionVault } from "../../lib/ttg/src/interfaces/IDistributionVault.sol"; +import { IEarnerRateModel } from "../../lib/protocol/src/rateModels/interfaces/IEarnerRateModel.sol"; +import { IEmergencyGovernor } from "../../lib/ttg/src/interfaces/IEmergencyGovernor.sol"; +import { IEmergencyGovernorDeployer } from "../../lib/ttg/src/interfaces/IEmergencyGovernorDeployer.sol"; +import { IERC20 } from "../../lib/protocol/lib/common/src/interfaces/IERC20.sol"; +import { IERC5805 } from "../../lib/ttg/src/abstract/interfaces/IERC5805.sol"; +import { IGovernor } from "../../lib/ttg/src/abstract/interfaces/IGovernor.sol"; +import { IMinterGateway } from "../../lib/protocol/src/interfaces/IMinterGateway.sol"; +import { IMinterRateModel } from "../../lib/protocol/src/rateModels/interfaces/IMinterRateModel.sol"; +import { IMToken } from "../../lib/protocol/src/interfaces/IMToken.sol"; +import { IPowerToken } from "../../lib/ttg/src/interfaces/IPowerToken.sol"; +import { IPowerTokenDeployer } from "../../lib/ttg/src/interfaces/IPowerTokenDeployer.sol"; +import { IRegistrar } from "../../lib/ttg/src/interfaces/IRegistrar.sol"; +import { IStandardGovernor } from "../../lib/ttg/src/interfaces/IStandardGovernor.sol"; +import { IStandardGovernorDeployer } from "../../lib/ttg/src/interfaces/IStandardGovernorDeployer.sol"; +import { IZeroGovernor } from "../../lib/ttg/src/interfaces/IZeroGovernor.sol"; +import { IZeroToken } from "../../lib/ttg/src/interfaces/IZeroToken.sol"; + +import { PureEpochs } from "../../lib/ttg/src/libs/PureEpochs.sol"; + +import { InitialAccountsFixture } from "../fixture/InitialAccountsFixture.sol"; + +import { IWETH } from "../utils/IWETH.sol"; +import { TestUtils } from "../utils/TestUtils.sol"; + +import { DeployBase } from "../../script/DeployBase.sol"; + +contract ProtocolIntegrationTests is TestUtils, DeployBase, InitialAccountsFixture { address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; uint256 internal constant _DEPLOYER_NONCE = 0; diff --git a/test/integration/WrappedMToken.t.sol b/test/integration/WrappedMToken.t.sol new file mode 100644 index 0000000..4f98c88 --- /dev/null +++ b/test/integration/WrappedMToken.t.sol @@ -0,0 +1,572 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.23; + +import { MTokenHarness } from "../utils/MTokenHarness.sol"; +import { WrappedMTokenHarness } from "../utils/WrappedMTokenHarness.sol"; +import { MockRateModel, MockRegistrar } from "../utils/Mocks.sol"; +import { TestUtils } from "../utils/TestUtils.sol"; + +contract WrappedMTokenIntegrationTests is TestUtils { + uint32 internal constant _EARNER_RATE = 5_000; // 5% APY + + bytes32 internal constant _EARNERS_LIST = "earners"; + bytes32 internal constant _EARNER_RATE_MODEL = "earner_rate_model"; + + address internal _alice = makeAddr("alice"); + address internal _bob = makeAddr("bob"); + address internal _carol = makeAddr("carol"); + address internal _dave = makeAddr("dave"); + + address internal _minterGateway = makeAddr("minterGateway"); + address internal _migrationAdmin = makeAddr("migrationAdmin"); + + address internal _vault = makeAddr("vault"); + + MTokenHarness internal _mToken; + MockRateModel internal _earnerRateModel; + MockRegistrar internal _registrar; + WrappedMTokenHarness internal _wrappedMToken; + + function setUp() external { + _earnerRateModel = new MockRateModel(); + _earnerRateModel.setRate(_EARNER_RATE); + + _registrar = new MockRegistrar(); + _registrar.set(_EARNER_RATE_MODEL, bytes32(uint256(uint160(address(_earnerRateModel))))); + _registrar.setVault(_vault); + + _mToken = new MTokenHarness(address(_registrar), _minterGateway); + _mToken.setLatestIndex(_EXP_SCALED_ONE); + + _wrappedMToken = new WrappedMTokenHarness(address(_mToken), _migrationAdmin); + + _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + + _registrar.setListContains(_EARNERS_LIST, _alice, true); + _registrar.setListContains(_EARNERS_LIST, _bob, true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.startEarningFor(_alice); + _wrappedMToken.startEarningFor(_bob); + } + + function test_integration_yieldAccumulation() external { + uint256 amount_ = 100e6; + + vm.prank(_minterGateway); + _mToken.mint(_alice, amount_); + + assertEq(_mToken.balanceOf(_alice), amount_); + + _wrap(_mToken, _wrappedMToken, _alice, _alice, amount_); + + // Assert Alice (Earner) + assertEq(_wrappedMToken.balanceOf(_alice), amount_); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + // Assert Globals + assertEq(_wrappedMToken.totalEarningSupply(), amount_); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalSupply(), amount_); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.excess(), 0); + + vm.prank(_minterGateway); + _mToken.mint(_carol, amount_); + + _wrap(_mToken, _wrappedMToken, _carol, _carol, amount_); + + // Assert Carol (Non-Earner) + assertEq(_wrappedMToken.balanceOf(_carol), amount_); + assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); + + // Assert Globals + uint256 totalEarningSupply_ = amount_; + uint256 totalNonEarningSupply_ = amount_; + uint256 totalSupply_ = amount_ * 2; + uint256 totalAccruedYield_ = 0; + uint256 excess_ = 0; + + assertEq(_wrappedMToken.totalEarningSupply(), totalEarningSupply_); + assertEq(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_); + assertEq(_wrappedMToken.totalSupply(), totalSupply_); + assertEq(_wrappedMToken.totalAccruedYield(), totalAccruedYield_); + assertEq(_wrappedMToken.excess(), excess_); + + // Fast forward 90 days in the future to generate yield + uint32 timeElapsed_ = 90 days; + vm.warp(vm.getBlockTimestamp() + timeElapsed_); + + uint128 currentIndex_ = _getContinuousIndexAt(_EARNER_RATE, _EXP_SCALED_ONE, timeElapsed_); + assertEq(_mToken.currentIndex(), currentIndex_); + + // Assert Alice (Earner) + uint240 accruedYield_ = _getAccruedYieldOf(_wrappedMToken, _alice, currentIndex_); + + assertEq(_wrappedMToken.balanceOf(_alice), amount_); + assertEq(_wrappedMToken.accruedYieldOf(_alice), accruedYield_); + + // Assert Carol (Non-Earner) + assertEq(_wrappedMToken.balanceOf(_carol), amount_); + assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); + + // Assert Globals + totalAccruedYield_ += accruedYield_; // accrued yield of Alice + excess_ += accruedYield_; // Carol is not earning so her yield is in excess + + assertEq(_wrappedMToken.totalEarningSupply(), totalEarningSupply_); + assertEq(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_); + assertEq(_wrappedMToken.totalSupply(), totalSupply_); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), totalAccruedYield_, 1); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 1); + + vm.prank(_minterGateway); + _mToken.mint(_bob, amount_); + + _wrap(_mToken, _wrappedMToken, _bob, _bob, amount_); + + // Assert Bob (Earner) + assertApproxEqAbs(_wrappedMToken.balanceOf(_bob), amount_, 1); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); + + // Assert Globals + totalEarningSupply_ += amount_; + totalSupply_ += amount_; + + assertEq(_wrappedMToken.totalEarningSupply(), totalEarningSupply_); + assertEq(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_); + assertEq(_wrappedMToken.totalSupply(), totalSupply_); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), accruedYield_, 1); + assertApproxEqAbs(_wrappedMToken.excess(), accruedYield_, 1); + + vm.prank(_minterGateway); + _mToken.mint(_dave, amount_); + + _wrap(_mToken, _wrappedMToken, _dave, _dave, amount_); + + // Assert Dave (Non-Earner) + assertEq(_wrappedMToken.balanceOf(_dave), amount_); + assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); + + // Assert Globals + totalNonEarningSupply_ += amount_; + totalSupply_ += amount_; + + assertEq(_wrappedMToken.totalEarningSupply(), totalEarningSupply_); + assertEq(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_); + assertEq(_wrappedMToken.totalSupply(), totalSupply_); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), accruedYield_, 1); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 1); + + assertEq(_wrappedMToken.balanceOf(_alice), amount_); + + uint256 yield_ = _wrappedMToken.claimFor(_alice); + + assertApproxEqAbs(yield_, accruedYield_, 2); + + // Assert Alice (Earner) + uint256 aliceBalance_ = amount_ + accruedYield_; + + assertApproxEqAbs(_wrappedMToken.balanceOf(_alice), aliceBalance_, 2); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + // Assert Globals + totalEarningSupply_ += accruedYield_; + totalSupply_ += accruedYield_; + totalAccruedYield_ -= accruedYield_; + + assertApproxEqAbs(_wrappedMToken.totalEarningSupply(), totalEarningSupply_, 2); + assertApproxEqAbs(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_, 2); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 2); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), totalAccruedYield_, 2); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 1); + + // Fast forward 180 days in the future to generate yield + timeElapsed_ = 180 days; + vm.warp(vm.getBlockTimestamp() + timeElapsed_); + + currentIndex_ = _getContinuousIndexAt(_EARNER_RATE, currentIndex_, timeElapsed_); + assertApproxEqAbs(_mToken.currentIndex(), currentIndex_, 1); + + // Assert Alice (Earner) + uint240 accruedYieldOfAlice_ = _getAccruedYieldOf(_wrappedMToken, _alice, currentIndex_); + + assertApproxEqAbs(_wrappedMToken.balanceOf(_alice), aliceBalance_, 2); + assertEq(_wrappedMToken.accruedYieldOf(_alice), accruedYieldOfAlice_); + + // Assert Bob (Earner) + uint240 accruedYieldOfBob_ = _getAccruedYieldOf(_wrappedMToken, _bob, currentIndex_); + + assertApproxEqAbs(_wrappedMToken.balanceOf(_bob), amount_, 1); + assertEq(_wrappedMToken.accruedYieldOf(_bob), accruedYieldOfBob_); + + // Assert Carol (Non-Earner) + assertEq(_wrappedMToken.balanceOf(_carol), amount_); + assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); + + // Assert Dave (Non-Earner) + assertEq(_wrappedMToken.balanceOf(_dave), amount_); + assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); + + // Assert Globals + totalAccruedYield_ += accruedYieldOfAlice_ + accruedYieldOfBob_; + + // Yield of Carol and Dave which deposited at the same time than Alice and Bob respectively but are not earning + excess_ += accruedYieldOfAlice_ + accruedYieldOfBob_; + + assertApproxEqAbs(_wrappedMToken.totalEarningSupply(), totalEarningSupply_, 2); + assertEq(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 2); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), totalAccruedYield_, 2); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 1); + } + + function test_integration_yieldTransfer() external { + uint256 amount_ = 100e6; + + vm.prank(_minterGateway); + _mToken.mint(_alice, amount_); + + _wrap(_mToken, _wrappedMToken, _alice, _alice, amount_); + + vm.prank(_minterGateway); + _mToken.mint(_carol, amount_); + + _wrap(_mToken, _wrappedMToken, _carol, _carol, amount_); + + // Fast forward 180 days in the future to generate yield + uint32 timeElapsed_ = 180 days; + vm.warp(vm.getBlockTimestamp() + timeElapsed_); + + vm.prank(_minterGateway); + _mToken.mint(_bob, amount_); + + _wrap(_mToken, _wrappedMToken, _bob, _bob, amount_); + + vm.prank(_minterGateway); + _mToken.mint(_dave, amount_); + + _wrap(_mToken, _wrappedMToken, _dave, _dave, amount_); + + uint128 firstIndex_ = _getContinuousIndexAt(_EARNER_RATE, _EXP_SCALED_ONE, timeElapsed_); + uint256 accruedYieldOfAlice_ = _getAccruedYieldOf(_wrappedMToken, _alice, firstIndex_); + + vm.prank(_alice); + _wrappedMToken.transfer(_carol, amount_); + + // Alice has transferred all her tokens and only keeps her accrued yield + uint256 aliceBalance_ = accruedYieldOfAlice_; + + // Assert Alice (Earner) + assertApproxEqAbs(_wrappedMToken.balanceOf(_alice), aliceBalance_, 3); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + // Assert Carol (Non-Earner) + assertEq(_wrappedMToken.balanceOf(_carol), amount_ * 2); + assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); + + // Assert Globals + uint256 totalEarningSupply_ = aliceBalance_ + amount_; + uint256 totalNonEarningSupply_ = amount_ * 3; + uint256 totalSupply_ = totalEarningSupply_ + totalNonEarningSupply_; + uint256 totalAccruedYield_ = 0; // 0 since Alice has claimed her yield + + // Yield of Carol which deposited at the same time than Alice but is not earning + uint256 excess_ = accruedYieldOfAlice_; + + assertApproxEqAbs(_wrappedMToken.totalEarningSupply(), totalEarningSupply_, 2); + assertEq(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 2); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), totalAccruedYield_, 3); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 2); + + vm.prank(_dave); + _wrappedMToken.transfer(_bob, amount_ / 2); + + // Assert Bob (Earner) + uint256 bobBalance_ = amount_ + amount_ / 2; + + assertApproxEqAbs(_wrappedMToken.balanceOf(_bob), bobBalance_, 2); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); + + // Assert Dave (Non-Earner) + assertEq(_wrappedMToken.balanceOf(_dave), amount_ / 2); + assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); + + totalEarningSupply_ += amount_ / 2; + totalNonEarningSupply_ -= amount_ / 2; + + // Assert Globals + assertApproxEqAbs(_wrappedMToken.totalEarningSupply(), totalEarningSupply_, 2); + assertEq(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 2); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), totalAccruedYield_, 2); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 1); + + // Fast forward 180 days in the future to generate yield + timeElapsed_ = 180 days; + vm.warp(vm.getBlockTimestamp() + timeElapsed_); + + uint128 secondIndex_ = _getContinuousIndexAt(_EARNER_RATE, firstIndex_, timeElapsed_); + assertApproxEqAbs(_mToken.currentIndex(), secondIndex_, 1); + + // Assert Alice (Earner) + accruedYieldOfAlice_ = _getAccruedYieldOf(_wrappedMToken, _alice, secondIndex_); + + assertApproxEqAbs(_wrappedMToken.balanceOf(_alice), aliceBalance_, 3); + assertEq(_wrappedMToken.accruedYieldOf(_alice), accruedYieldOfAlice_); + + // Assert Bob (Earner) + uint256 accruedYieldOfBob_ = _getAccruedYieldOf(_wrappedMToken, _bob, secondIndex_); + + assertApproxEqAbs(_wrappedMToken.balanceOf(_bob), bobBalance_, 2); + assertEq(_wrappedMToken.accruedYieldOf(_bob), accruedYieldOfBob_); + + // Assert Carol (Non-Earner) + assertEq(_wrappedMToken.balanceOf(_carol), amount_ * 2); + assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); + + // Assert Dave (Non-Earner) + assertEq(_wrappedMToken.balanceOf(_dave), amount_ / 2); + assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); + + // Assert Globals + totalAccruedYield_ += accruedYieldOfAlice_ + accruedYieldOfBob_; + excess_ = + _getAccruedYield(uint240(amount_), _EXP_SCALED_ONE, secondIndex_) + // Carol's yield + _getAccruedYield(uint240(amount_), firstIndex_, secondIndex_) + // Yield of Alice's amount transferred to Carol + _getAccruedYield(uint240(amount_ / 2), firstIndex_, secondIndex_); // Dave's yield + + assertApproxEqAbs(_wrappedMToken.totalEarningSupply(), totalEarningSupply_, 2); + assertEq(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 2); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), totalAccruedYield_, 3); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 1); + } + + function test_integration_yieldClaimUnwrap() external { + uint256 amount_ = 100e6; + + vm.prank(_minterGateway); + _mToken.mint(_alice, amount_); + + _wrap(_mToken, _wrappedMToken, _alice, _alice, amount_); + + vm.prank(_minterGateway); + _mToken.mint(_carol, amount_); + + _wrap(_mToken, _wrappedMToken, _carol, _carol, amount_); + + // Fast forward 180 days in the future to generate yield + uint32 timeElapsed_ = 180 days; + vm.warp(vm.getBlockTimestamp() + timeElapsed_); + + vm.prank(_minterGateway); + _mToken.mint(_bob, amount_); + + _wrap(_mToken, _wrappedMToken, _bob, _bob, amount_); + + vm.prank(_minterGateway); + _mToken.mint(_dave, amount_); + + _wrap(_mToken, _wrappedMToken, _dave, _dave, amount_); + + uint128 firstIndex_ = _getContinuousIndexAt(_EARNER_RATE, _EXP_SCALED_ONE, timeElapsed_); + assertEq(_mToken.currentIndex(), firstIndex_); + + uint256 accruedYieldOfAlice_ = _getAccruedYieldOf(_wrappedMToken, _alice, firstIndex_); + assertEq(_wrappedMToken.accruedYieldOf(_alice), accruedYieldOfAlice_); + + // Fast forward 90 days in the future to generate yield + timeElapsed_ = 90 days; + vm.warp(vm.getBlockTimestamp() + timeElapsed_); + + uint128 secondIndex_ = _getContinuousIndexAt(_EARNER_RATE, firstIndex_, timeElapsed_); + assertApproxEqAbs(_mToken.currentIndex(), secondIndex_, 1); + + accruedYieldOfAlice_ = _getAccruedYieldOf(_wrappedMToken, _alice, secondIndex_); + assertEq(_wrappedMToken.accruedYieldOf(_alice), accruedYieldOfAlice_); + + uint256 accruedYieldOfBob_ = _getAccruedYieldOf(_wrappedMToken, _bob, secondIndex_); + assertEq(_wrappedMToken.accruedYieldOf(_bob), accruedYieldOfBob_); + + // Stop earning for Alice + _registrar.setListContains(_EARNERS_LIST, _alice, false); + _wrappedMToken.stopEarningFor(_alice); + + // Assert Alice (Non-Earner) + // Yield of Alice is claimed when stopping earning + assertApproxEqAbs(_wrappedMToken.balanceOf(_alice), amount_ + accruedYieldOfAlice_, 1); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + // Assert Globals + uint256 totalEarningSupply_ = amount_; // Only Bob is earning + uint256 totalNonEarningSupply_ = amount_ * 3 + accruedYieldOfAlice_; + uint256 totalSupply_ = totalEarningSupply_ + totalNonEarningSupply_; + + // Yield of Carol and Dave which deposited at the same time than Alice and Bob respectively but are not earning + uint256 excess_ = accruedYieldOfAlice_ + accruedYieldOfBob_; + + assertEq(_wrappedMToken.totalEarningSupply(), totalEarningSupply_); + assertApproxEqAbs(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), accruedYieldOfBob_, 3); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 2); + + _registrar.setListContains(_EARNERS_LIST, _carol, true); + _wrappedMToken.startEarningFor(_carol); + + // Assert Carol (Earner) + assertApproxEqAbs(_wrappedMToken.balanceOf(_carol), amount_, 2); + assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); + + // Assert Globals + totalEarningSupply_ += amount_; + totalNonEarningSupply_ -= amount_; + + assertEq(_wrappedMToken.totalEarningSupply(), totalEarningSupply_); + assertApproxEqAbs(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), accruedYieldOfBob_, 2); + assertEq(_wrappedMToken.excess(), excess_); + + // Fast forward 180 days in the future to generate yield + timeElapsed_ = 180 days; + vm.warp(vm.getBlockTimestamp() + timeElapsed_); + + uint128 thirdIndex_ = _getContinuousIndexAt(_EARNER_RATE, secondIndex_, timeElapsed_); + assertApproxEqAbs(_mToken.currentIndex(), thirdIndex_, 9); + + // Assert Alice (Non-Earner) + assertApproxEqAbs(_wrappedMToken.balanceOf(_alice), amount_ + accruedYieldOfAlice_, 1); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + // Assert Bob (Earner) + accruedYieldOfBob_ = _getAccruedYieldOf(_wrappedMToken, _bob, thirdIndex_); + + assertApproxEqAbs(_wrappedMToken.balanceOf(_bob), amount_, 1); + assertEq(_wrappedMToken.accruedYieldOf(_bob), accruedYieldOfBob_); + + // Assert Carol (Earner) + uint256 accruedYieldOfCarol_ = _getAccruedYieldOf(_wrappedMToken, _carol, thirdIndex_); + + assertApproxEqAbs(_wrappedMToken.balanceOf(_carol), amount_, 2); + assertEq(_wrappedMToken.accruedYieldOf(_carol), accruedYieldOfCarol_); + + // Assert Dave (Non-Earner) + assertEq(_wrappedMToken.balanceOf(_dave), amount_); + assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); + + // Assert Globals + uint256 accruedYieldOfCarolBeforeEarning_ = _getAccruedYield(uint240(amount_), _EXP_SCALED_ONE, secondIndex_); + + excess_ = + accruedYieldOfCarolBeforeEarning_ + + _getAccruedYield(uint240(accruedYieldOfCarolBeforeEarning_), secondIndex_, thirdIndex_) + // Carol's yield + _getAccruedYield(uint240(amount_ + accruedYieldOfAlice_), secondIndex_, thirdIndex_) + // Alice's yield + _getAccruedYield(uint240(amount_), firstIndex_, thirdIndex_); // Dave's yield + + assertEq(_wrappedMToken.totalEarningSupply(), totalEarningSupply_); + assertApproxEqAbs(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), accruedYieldOfBob_ + accruedYieldOfCarol_, 2); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 4); + + uint256 aliceBalance_ = _wrappedMToken.balanceOf(_alice); + + vm.prank(_alice); + _wrappedMToken.unwrap(_alice, aliceBalance_); + + // Assert Alice (Non-Earner) + assertEq(_mToken.balanceOf(_alice), aliceBalance_); + assertEq(_wrappedMToken.balanceOf(_alice), 0); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + // Assert Globals + totalNonEarningSupply_ -= aliceBalance_; + totalSupply_ -= aliceBalance_; + + assertEq(_wrappedMToken.totalEarningSupply(), totalEarningSupply_); + assertApproxEqAbs(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), accruedYieldOfBob_ + accruedYieldOfCarol_, 2); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 4); + + uint256 bobBalance_ = _wrappedMToken.balanceOf(_bob); + + vm.prank(_bob); + + // Accrued yield of Bob is claimed when unwrapping + _wrappedMToken.unwrap(_bob, bobBalance_ + accruedYieldOfBob_ - 2); + + // Assert Bob (Earner) + assertEq(_mToken.balanceOf(_bob), bobBalance_ + accruedYieldOfBob_ - 2); + + assertEq(_wrappedMToken.balanceOf(_bob), 0); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); + + // Assert Globals + totalEarningSupply_ -= bobBalance_; + totalSupply_ -= bobBalance_; + + assertEq(_wrappedMToken.totalEarningSupply(), totalEarningSupply_); + assertApproxEqAbs(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), accruedYieldOfCarol_, 5); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 4); + + uint256 carolBalance_ = _wrappedMToken.balanceOf(_carol); + + vm.prank(_carol); + + // Accrued yield of Carol is claimed when unwrapping + _wrappedMToken.unwrap(_carol, carolBalance_ + accruedYieldOfCarol_ - 2); + + // Assert Carol (Earner) + assertEq(_mToken.balanceOf(_carol), carolBalance_ + accruedYieldOfCarol_ - 2); + assertEq(_wrappedMToken.balanceOf(_carol), 0); + assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); + + // Assert Globals + totalEarningSupply_ -= carolBalance_; + totalSupply_ -= carolBalance_; + + assertApproxEqAbs(_wrappedMToken.totalEarningSupply(), totalEarningSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), 0, 9); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 4); + + uint256 daveBalance_ = _wrappedMToken.balanceOf(_dave); + + vm.prank(_dave); + _wrappedMToken.unwrap(_dave, daveBalance_); + + // Assert Dave (Non-Earner) + assertEq(_mToken.balanceOf(_dave), daveBalance_); + assertEq(_wrappedMToken.balanceOf(_dave), 0); + assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); + + // // Assert Globals + totalNonEarningSupply_ -= daveBalance_; + totalSupply_ -= daveBalance_; + + assertApproxEqAbs(_wrappedMToken.totalEarningSupply(), totalEarningSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), 0, 9); + assertApproxEqAbs(_wrappedMToken.excess(), excess_, 4); + + uint240 excessYield_ = _wrappedMToken.claimExcess(); + assertEq(_mToken.balanceOf(_vault), excessYield_); + + // Assert Globals + assertApproxEqAbs(_wrappedMToken.totalEarningSupply(), totalEarningSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalNonEarningSupply(), totalNonEarningSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalSupply(), totalSupply_, 1); + assertApproxEqAbs(_wrappedMToken.totalAccruedYield(), 0, 9); + assertEq(_wrappedMToken.excess(), 0); + } +} diff --git a/test/utils/MTokenHarness.sol b/test/utils/MTokenHarness.sol new file mode 100644 index 0000000..2cf3cbc --- /dev/null +++ b/test/utils/MTokenHarness.sol @@ -0,0 +1,46 @@ + +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.23; + +import { MToken } from "../../lib/protocol/src/MToken.sol"; + +contract MTokenHarness is MToken { + constructor(address ttgRegistrar_, address minterGateway_) MToken(ttgRegistrar_, minterGateway_) {} + + function setLatestIndex(uint256 index_) external { + latestIndex = uint128(index_); + } + + function setLatestRate(uint256 rate_) external { + _latestRate = uint32(rate_); + } + + function setLatestUpdated(uint256 timestamp_) external { + latestUpdateTimestamp = uint40(timestamp_); + } + + function setIsEarning(address account_, bool isEarning_) external { + _balances[account_].isEarning = isEarning_; + } + + function setTotalNonEarningSupply(uint256 totalNonEarningSupply_) external { + totalNonEarningSupply = uint240(totalNonEarningSupply_); + } + + function setPrincipalOfTotalEarningSupply(uint256 principalOfTotalEarningSupply_) external { + principalOfTotalEarningSupply = uint112(principalOfTotalEarningSupply_); + } + + function setInternalBalanceOf(address account_, uint256 balance_) external { + _balances[account_].rawBalance = uint240(balance_); + } + + function internalBalanceOf(address account_) external view returns (uint256 balance_) { + return _balances[account_].rawBalance; + } + + function rate() external view returns (uint32 rate_) { + return _rate(); + } +} diff --git a/test/utils/Mocks.sol b/test/utils/Mocks.sol new file mode 100644 index 0000000..fe83be6 --- /dev/null +++ b/test/utils/Mocks.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.23; + +contract MockRegistrar { + address public vault; + + mapping(bytes32 key => bytes32 value) public get; + + mapping(bytes32 list => mapping(address account => bool contains)) public listContains; + + function set(bytes32 key_, bytes32 value_) external { + get[key_] = value_; + } + + function setListContains(bytes32 list_, address account_, bool contains_) external { + listContains[list_][account_] = contains_; + } + + function setVault(address vault_) external { + vault = vault_; + } +} + +contract MockRateModel { + uint256 public rate; + + function setRate(uint256 rate_) external { + rate = rate_; + } +} diff --git a/test/utils/TestUtils.sol b/test/utils/TestUtils.sol index cda157f..4263cb0 100644 --- a/test/utils/TestUtils.sol +++ b/test/utils/TestUtils.sol @@ -2,13 +2,51 @@ pragma solidity 0.8.23; +import { Test } from "../../lib/forge-std/src/Test.sol"; + +import { ContinuousIndexingMath } from "../../lib/protocol/src/libs/ContinuousIndexingMath.sol"; + import { PureEpochs } from "../../lib/ttg/src/libs/PureEpochs.sol"; -import { Test } from "../../lib/forge-std/src/Test.sol"; +import { WrappedMTokenHarness } from "./WrappedMTokenHarness.sol"; +import { MTokenHarness } from "./MTokenHarness.sol"; + contract TestUtils is Test { - /* ============ Helpers ============ */ + uint56 internal constant _EXP_SCALED_ONE = 1e12; + + /* ============ Index helpers ============ */ + function _getContinuousIndexAt( + uint32 minterRate_, + uint128 initialIndex_, + uint32 elapsedTime_ + ) internal pure returns (uint128) { + return + uint128( + ContinuousIndexingMath.multiplyIndicesUp( + initialIndex_, + ContinuousIndexingMath.getContinuousIndex( + ContinuousIndexingMath.convertFromBasisPoints(minterRate_), + elapsedTime_ + ) + ) + ); + } + + /* ============ Principal / Present conversions ============ */ + + /* ============ Principal ============ */ + function _getPrincipalAmountRoundedDown(uint240 presentAmount_, uint128 index_) internal pure returns (uint112) { + return ContinuousIndexingMath.divideDown(presentAmount_, index_); + } + /* ============ Present ============ */ + function _getPresentAmountRoundedDown(uint112 principalAmount_, uint128 index_) internal pure returns (uint240) { + return ContinuousIndexingMath.multiplyDown(principalAmount_, index_); + } + + /* ============ TTG Helpers ============ */ + /* ============ Epochs ============ */ function _currentEpoch() internal view returns (uint16) { return PureEpochs.currentEpoch(); @@ -53,4 +91,41 @@ contract TestUtils is Test { return abi.encodePacked(r_, s_, v_); } + + /* ============ Wrapped M helpers ============ */ + + /* ============ Wrap ============ */ + function _wrap( + MTokenHarness mToken_, + WrappedMTokenHarness wrappedMToken_, + address account_, + address recipient_, + uint256 amount_ + ) internal { + vm.prank(account_); + mToken_.approve(address(wrappedMToken_), amount_); + + vm.prank(account_); + wrappedMToken_.wrap(recipient_, amount_); + } + + /* ============ Accrued Yield ============ */ + function _getAccruedYieldOf( + WrappedMTokenHarness wrappedMToken_, + address account_, + uint128 currentIndex_ + ) internal view returns (uint240) { + (, , uint112 principal_, uint240 balance_) = wrappedMToken_.internalBalanceInfo(account_); + return _getPresentAmountRoundedDown(principal_, currentIndex_) - balance_; + } + + function _getAccruedYield( + uint240 startingPresentAmount_, + uint128 startingIndex_, + uint128 currentIndex_ + ) internal pure returns (uint240) { + uint112 startingPrincipal_ = _getPrincipalAmountRoundedDown(startingPresentAmount_, startingIndex_); + return _getPresentAmountRoundedDown(startingPrincipal_, currentIndex_) - startingPresentAmount_; + } + } diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol new file mode 100644 index 0000000..a267468 --- /dev/null +++ b/test/utils/WrappedMTokenHarness.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.23; + +import { WrappedMToken } from "../../lib/wrapped-m-token/src/WrappedMToken.sol"; + +contract WrappedMTokenHarness is WrappedMToken { + constructor(address mToken_, address migrationAdmin_) WrappedMToken(mToken_, migrationAdmin_) {} + + function setIsEarningOf(address account_, bool isEarning_) external { + (, uint128 index_, , uint240 balance_) = _getBalanceInfo(account_); + _setBalanceInfo(account_, isEarning_, index_, balance_); + } + + function setIndexOf(address account_, uint256 index_) external { + (bool isEarning_, , , uint240 balance_) = _getBalanceInfo(account_); + _setBalanceInfo(account_, isEarning_, uint128(index_), balance_); + } + + function setBalanceOf(address account_, uint256 balance_) external { + (bool isEarning_, uint128 index_, , ) = _getBalanceInfo(account_); + _setBalanceInfo(account_, isEarning_, index_, uint240(balance_)); + } + + function setAccountOf(address account_, bool isEarning_, uint256 index_, uint256 balance_) external { + _setBalanceInfo(account_, isEarning_, uint128(index_), uint240(balance_)); + } + + function setTotalNonEarningSupply(uint256 totalNonEarningSupply_) external { + totalNonEarningSupply = uint240(totalNonEarningSupply_); + } + + function setPrincipalOfTotalEarningSupply(uint256 principalOfTotalEarningSupply_) external { + _principalOfTotalEarningSupply = uint112(principalOfTotalEarningSupply_); + } + + function setIndexOfTotalEarningSupply(uint256 indexOfTotalEarningSupply_) external { + _indexOfTotalEarningSupply = uint128(indexOfTotalEarningSupply_); + } + + function internalBalanceInfo( + address account_ + ) external view returns (bool isEarning_, uint128 index_, uint112 principal_, uint240 balance_) { + (isEarning_, index_, principal_, balance_) = _getBalanceInfo(account_); + } + + function internalBalanceOf(address account_) external view returns (uint240 balance_) { + (, , , balance_) = _getBalanceInfo(account_); + } + + function internalIndexOf(address account_) external view returns (uint128 index_) { + (, index_, , ) = _getBalanceInfo(account_); + } + + function internalPrincipalOf(address account_) external view returns (uint112 principal_) { + (, , principal_, ) = _getBalanceInfo(account_); + } + + function principalOfTotalEarningSupply() external view returns (uint240 principalOfTotalEarningSupply_) { + principalOfTotalEarningSupply_ = _principalOfTotalEarningSupply; + } + + function indexOfTotalEarningSupply() external view returns (uint128 indexOfTotalEarningSupply_) { + indexOfTotalEarningSupply_ = _indexOfTotalEarningSupply; + } +}