diff --git a/src/pools/D3MAaveV3NSTNoSupplyCapTypePool.sol b/src/pools/D3MAaveV3NSTNoSupplyCapTypePool.sol new file mode 100644 index 00000000..39d57362 --- /dev/null +++ b/src/pools/D3MAaveV3NSTNoSupplyCapTypePool.sol @@ -0,0 +1,273 @@ +// SPDX-FileCopyrightText: © 2021 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.14; + +import "./ID3MPool.sol"; + +interface TokenLike { + function balanceOf(address) external view returns (uint256); + function approve(address, uint256) external returns (bool); + function transfer(address, uint256) external returns (bool); +} + +interface VatLike { + function live() external view returns (uint256); + function hope(address) external; + function nope(address) external; +} + +interface D3mHubLike { + function vat() external view returns (address); + function end() external view returns (EndLike); +} + +interface EndLike { + function Art(bytes32) external view returns (uint256); +} + +// https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/tokenization/AToken.sol +interface ATokenLike is TokenLike { + function scaledBalanceOf(address) external view returns (uint256); + function getIncentivesController() external view returns (address); +} + +// https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/pool/Pool.sol +interface PoolLike { + + // Need to use a struct as too many variables to return on the stack + struct ReserveData { + //stores the reserve configuration + uint256 configuration; + //the liquidity index. Expressed in ray + uint128 liquidityIndex; + //the current supply rate. Expressed in ray + uint128 currentLiquidityRate; + //variable borrow index. Expressed in ray + uint128 variableBorrowIndex; + //the current variable borrow rate. Expressed in ray + uint128 currentVariableBorrowRate; + //the current stable borrow rate. Expressed in ray + uint128 currentStableBorrowRate; + //timestamp of last update + uint40 lastUpdateTimestamp; + //the id of the reserve. Represents the position in the list of the active reserves + uint16 id; + //aToken address + address aTokenAddress; + //stableDebtToken address + address stableDebtTokenAddress; + //variableDebtToken address + address variableDebtTokenAddress; + //address of the interest rate strategy + address interestRateStrategyAddress; + //the current treasury balance, scaled + uint128 accruedToTreasury; + //the outstanding unbacked aTokens minted through the bridging feature + uint128 unbacked; + //the outstanding debt borrowed against this asset in isolation mode + uint128 isolationModeTotalDebt; + } + + function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external; + function withdraw(address asset, uint256 amount, address to) external; + function getReserveNormalizedIncome(address asset) external view returns (uint256); + function getReserveData(address asset) external view returns (ReserveData memory); +} + +// https://github.com/aave/aave-v3-periphery/blob/master/contracts/rewards/RewardsController.sol +interface RewardsClaimerLike { + function claimRewards(address[] calldata assets, uint256 amount, address to, address reward) external returns (uint256); +} + +interface JoinLike { + function dai() external view returns (address); + function nst() external view returns (address); + function join(address, uint256) external; + function exit(address, uint256) external; +} + +contract D3MAaveV3NSTNoSupplyCapTypePool is ID3MPool { + + mapping (address => uint256) public wards; + address public hub; + address public king; // Who gets the rewards + uint256 public exited; + + bytes32 public immutable ilk; + VatLike public immutable vat; + PoolLike public immutable pool; + ATokenLike public immutable stableDebt; + ATokenLike public immutable variableDebt; + ATokenLike public immutable anst; + JoinLike public immutable nstJoin; + TokenLike public immutable nst; // Asset + JoinLike public immutable daiJoin; + TokenLike public immutable dai; + + // --- Events --- + event Rely(address indexed usr); + event Deny(address indexed usr); + event File(bytes32 indexed what, address data); + event Collect(address indexed king, address indexed gift, uint256 amt); + + constructor(bytes32 ilk_, address hub_, address nstJoin_, address daiJoin_, address pool_) { + ilk = ilk_; + nstJoin = JoinLike(nstJoin_); + nst = TokenLike(nstJoin.nst()); + daiJoin = JoinLike(daiJoin_); + dai = TokenLike(daiJoin.dai()); + pool = PoolLike(pool_); + + // Fetch the reserve data from Aave + PoolLike.ReserveData memory data = pool.getReserveData(address(nst)); + require(data.aTokenAddress != address(0), "D3MAaveV3NoSupplyCapTypePool/invalid-anst"); + require(data.stableDebtTokenAddress != address(0), "D3MAaveV3NoSupplyCapTypePool/invalid-stableDebt"); + require(data.variableDebtTokenAddress != address(0), "D3MAaveV3NoSupplyCapTypePool/invalid-variableDebt"); + + anst = ATokenLike(data.aTokenAddress); + stableDebt = ATokenLike(data.stableDebtTokenAddress); + variableDebt = ATokenLike(data.variableDebtTokenAddress); + + hub = hub_; + vat = VatLike(D3mHubLike(hub_).vat()); + vat.hope(hub_); + + nst.approve(pool_, type(uint256).max); + dai.approve(daiJoin_, type(uint256).max); + vat.hope(daiJoin_); + nst.approve(nstJoin_, type(uint256).max); + vat.hope(nstJoin_); + + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + modifier auth { + require(wards[msg.sender] == 1, "D3MAaveV3NoSupplyCapTypePool/not-authorized"); + _; + } + + modifier onlyHub { + require(msg.sender == hub, "D3MAaveV3NoSupplyCapTypePool/only-hub"); + _; + } + + // --- Math --- + uint256 internal constant RAY = 10 ** 27; + function _rdiv(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = (x * RAY) / y; + } + function _min(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = x <= y ? x : y; + } + + // --- Admin --- + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + function file(bytes32 what, address data) external auth { + require(vat.live() == 1, "D3MAaveV3NoSupplyCapTypePool/no-file-during-shutdown"); + if (what == "hub") { + vat.nope(hub); + hub = data; + vat.hope(data); + } else if (what == "king") king = data; + else revert("D3MAaveV3NoSupplyCapTypePool/file-unrecognized-param"); + emit File(what, data); + } + + // Deposits NST to Aave in exchange for anst which is received by this contract + // Aave: https://docs.aave.com/developers/core-contracts/pool#supply + function deposit(uint256 wad) external override onlyHub { + daiJoin.join(address(this), wad); + nstJoin.exit(address(this), wad); + + uint256 scaledPrev = anst.scaledBalanceOf(address(this)); + + pool.supply(address(nst), wad, address(this), 0); + + // Verify the correct amount of anst shows up + uint256 interestIndex = pool.getReserveNormalizedIncome(address(nst)); + uint256 scaledAmount = _rdiv(wad, interestIndex); + require(anst.scaledBalanceOf(address(this)) >= (scaledPrev + scaledAmount), "D3MAaveV3NoSupplyCapTypePool/incorrect-anst-balance-received"); + } + + // Withdraws NST from Aave in exchange for anst + // Aave: https://docs.aave.com/developers/core-contracts/pool#withdraw + function withdraw(uint256 wad) external override onlyHub { + uint256 prevNst = nst.balanceOf(address(this)); + + pool.withdraw(address(nst), wad, address(this)); + + require(nst.balanceOf(address(this)) == prevNst + wad, "D3MAaveV3NoSupplyCapTypePool/incorrect-nst-balance-received"); + + nstJoin.join(address(this), wad); + daiJoin.exit(msg.sender, wad); + } + + function exit(address dst, uint256 wad) external override onlyHub { + uint256 exited_ = exited; + exited = exited_ + wad; + uint256 amt = wad * assetBalance() / (D3mHubLike(hub).end().Art(ilk) - exited_); + require(anst.transfer(dst, amt), "D3MAaveV3NoSupplyCapTypePool/transfer-failed"); + } + + function quit(address dst) external override auth { + require(vat.live() == 1, "D3MAaveV3NoSupplyCapTypePool/no-quit-during-shutdown"); + require(anst.transfer(dst, anst.balanceOf(address(this))), "D3MAaveV3NoSupplyCapTypePool/transfer-failed"); + } + + function preDebtChange() external override {} + + function postDebtChange() external override {} + + // --- Balance of the underlying asset (NST) + function assetBalance() public view override returns (uint256) { + return anst.balanceOf(address(this)); + } + + function maxDeposit() external pure override returns (uint256) { + return type(uint256).max; + } + + function maxWithdraw() external view override returns (uint256) { + return _min(nst.balanceOf(address(anst)), assetBalance()); + } + + function redeemable() external view override returns (address) { + return address(anst); + } + + // --- Collect any rewards --- + function collect(address gift) external returns (uint256 amt) { + require(king != address(0), "D3MAaveV3NoSupplyCapTypePool/king-not-set"); + + address[] memory assets = new address[](1); + assets[0] = address(anst); + + RewardsClaimerLike rewardsClaimer = RewardsClaimerLike(anst.getIncentivesController()); + + amt = rewardsClaimer.claimRewards(assets, type(uint256).max, king, gift); + emit Collect(king, gift, amt); + } +} diff --git a/src/tests/pools/D3MAaveV3NSTNoSupplyCapTypePool.t.sol b/src/tests/pools/D3MAaveV3NSTNoSupplyCapTypePool.t.sol new file mode 100644 index 00000000..32830061 --- /dev/null +++ b/src/tests/pools/D3MAaveV3NSTNoSupplyCapTypePool.t.sol @@ -0,0 +1,332 @@ +// SPDX-FileCopyrightText: © 2022 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.14; + +import "./D3MPoolBase.t.sol"; + +import { D3MAaveV3NSTNoSupplyCapTypePool, PoolLike } from "../../pools/D3MAaveV3NSTNoSupplyCapTypePool.sol"; + +interface RewardsClaimerLike { + function getRewardsBalance(address[] calldata assets, address user) external view returns (uint256); +} + +contract AToken is TokenMock { + address public rewardsClaimer; + + constructor(uint256 decimals_) TokenMock(decimals_) { + rewardsClaimer = address(new FakeRewardsClaimer()); + } + + function scaledBalanceOf(address who) external view returns (uint256) { + return balanceOf[who]; + } + + function getIncentivesController() external view returns (address) { + return rewardsClaimer; + } +} + +contract FakeRewardsClaimer { + struct ClaimCall { + address[] assets; + uint256 amt; + address dst; + address reward; + } + ClaimCall public lastClaim; + + function claimRewards(address[] calldata assets, uint256 amt, address dst, address reward) external returns (uint256) { + lastClaim = ClaimCall( + assets, + amt, + dst, + reward + ); + return amt; + } + + function getAssetsFromClaim() external view returns (address[] memory) { + return lastClaim.assets; + } +} + +contract FakeLendingPool { + + // Need to use a struct as too many variables to return on the stack + struct ReserveData { + //stores the reserve configuration + uint256 configuration; + //the liquidity index. Expressed in ray + uint128 liquidityIndex; + //the current supply rate. Expressed in ray + uint128 currentLiquidityRate; + //variable borrow index. Expressed in ray + uint128 variableBorrowIndex; + //the current variable borrow rate. Expressed in ray + uint128 currentVariableBorrowRate; + //the current stable borrow rate. Expressed in ray + uint128 currentStableBorrowRate; + //timestamp of last update + uint40 lastUpdateTimestamp; + //the id of the reserve. Represents the position in the list of the active reserves + uint16 id; + //aToken address + address aTokenAddress; + //stableDebtToken address + address stableDebtTokenAddress; + //variableDebtToken address + address variableDebtTokenAddress; + //address of the interest rate strategy + address interestRateStrategyAddress; + //the current treasury balance, scaled + uint128 accruedToTreasury; + //the outstanding unbacked aTokens minted through the bridging feature + uint128 unbacked; + //the outstanding debt borrowed against this asset in isolation mode + uint128 isolationModeTotalDebt; + } + + address public anst; + address public nst; + + struct DepositCall { + address asset; + uint256 amt; + address forWhom; + uint16 code; + } + DepositCall public lastDeposit; + + struct WithdrawCall { + address asset; + uint256 amt; + address dst; + } + WithdrawCall public lastWithdraw; + + constructor(address anst_, address nst_) { + anst = anst_; + nst = nst_; + } + + function getReserveData(address) external view returns( + ReserveData memory result + ) { + result.aTokenAddress = anst; + result.stableDebtTokenAddress = address(2); + result.variableDebtTokenAddress = address(3); + result.interestRateStrategyAddress = address(4); + } + + function supply(address asset, uint256 amt, address forWhom, uint16 code) external { + lastDeposit = DepositCall( + asset, + amt, + forWhom, + code + ); + TokenMock(anst).mint(forWhom, amt); + } + + function withdraw(address asset, uint256 amt, address dst) external { + lastWithdraw = WithdrawCall( + asset, + amt, + dst + ); + TokenMock(asset).transfer(dst, amt); + } + + function getReserveNormalizedIncome(address asset) external pure returns (uint256) { + asset; + return 10 ** 27; + } +} + +contract DaiJoinMock { + + TokenMock public dai; + + constructor(TokenMock dai_) { + dai = dai_; + } + + function join(address usr, uint256 amt) external { + dai.transferFrom(usr, address(this), amt); + } + + function exit(address usr, uint256 amt) external { + dai.transfer(usr, amt); + } + +} + +contract NstJoinMock { + + TokenMock public nst; + + constructor(TokenMock nst_) { + nst = nst_; + } + + function join(address usr, uint256 amt) external { + nst.transferFrom(usr, address(this), amt); + } + + function exit(address usr, uint256 amt) external { + nst.transfer(usr, amt); + } + +} + +contract D3MAaveV3NSTNoSupplyCapTypePoolTest is D3MPoolBaseTest { + + AToken anst; + FakeLendingPool aavePool; + DaiJoinMock daiJoin; + TokenMock nst; + NstJoinMock nstJoin; + + D3MAaveV3NSTNoSupplyCapTypePool pool; + + function setUp() public { + baseInit("D3MAaveV3NoSupplyCapTypePool"); + + nst = new TokenMock(18); + anst = new AToken(18); + daiJoin = new DaiJoinMock(dai); + nstJoin = new NstJoinMock(nst); + anst.mint(address(this), 1_000_000 ether); + aavePool = new FakeLendingPool(address(anst), address(nst)); + anst.rely(address(aavePool)); + + dai.mint(address(daiJoin), 1_000_000 ether); + nst.mint(address(nstJoin), 1_000_000 ether); + + setPoolContract(pool = new D3MAaveV3NSTNoSupplyCapTypePool("", address(hub), address(nstJoin), address(daiJoin), address(aavePool))); + } + + function test_constructor_sets_values() public { + assertEq(address(pool.nstJoin()), address(nstJoin)); + assertEq(address(pool.nst()), address(nst)); + assertEq(address(pool.daiJoin()), address(daiJoin)); + assertEq(address(pool.dai()), address(dai)); + } + + function test_can_file_king() public { + assertEq(pool.king(), address(0)); + + pool.file("king", address(123)); + + assertEq(pool.king(), address(123)); + } + + function test_cannot_file_king_no_auth() public { + pool.deny(address(this)); + assertRevert(address(pool), abi.encodeWithSignature("file(bytes32,address)", bytes32("king"), address(123)), "D3MAaveV3NoSupplyCapTypePool/not-authorized"); + } + + function test_cannot_file_king_vat_caged() public { + vat.cage(); + assertRevert(address(pool), abi.encodeWithSignature("file(bytes32,address)", bytes32("king"), address(123)), "D3MAaveV3NoSupplyCapTypePool/no-file-during-shutdown"); + } + + function test_deposit_calls_lending_pool_deposit() public { + TokenMock(address(anst)).rely(address(aavePool)); + dai.mint(address(pool), 1); + vm.prank(address(hub)); pool.deposit(1); + (address asset, uint256 amt, address dst, uint256 code) = FakeLendingPool(address(aavePool)).lastDeposit(); + assertEq(asset, address(nst)); + assertEq(amt, 1); + assertEq(dst, address(pool)); + assertEq(code, 0); + } + + function test_collect_claims_for_king() public { + address king = address(123); + address rewardsClaimer = anst.getIncentivesController(); + pool.file("king", king); + + pool.collect(address(456)); + + (uint256 amt, address dst, address reward) = FakeRewardsClaimer(rewardsClaimer).lastClaim(); + address[] memory assets = FakeRewardsClaimer(rewardsClaimer).getAssetsFromClaim(); + + assertEq(address(anst), assets[0]); + assertEq(amt, type(uint256).max); + assertEq(dst, king); + assertEq(reward, address(456)); + } + + function test_collect_no_king() public { + assertEq(pool.king(), address(0)); + assertRevert(address(pool), abi.encodeWithSignature("collect(address)", address(0)), "D3MAaveV3NoSupplyCapTypePool/king-not-set"); + } + + function test_redeemable_returns_anst() public { + assertEq(pool.redeemable(), address(anst)); + } + + function test_exit_anst() public { + uint256 tokens = anst.totalSupply(); + anst.transfer(address(pool), tokens); + assertEq(anst.balanceOf(address(this)), 0); + assertEq(anst.balanceOf(address(pool)), tokens); + + end.setArt(tokens); + vm.prank(address(hub)); pool.exit(address(this), tokens); + + assertEq(anst.balanceOf(address(this)), tokens); + assertEq(anst.balanceOf(address(pool)), 0); + } + + function test_quit_moves_balance() public { + uint256 tokens = anst.totalSupply(); + anst.transfer(address(pool), tokens); + assertEq(anst.balanceOf(address(this)), 0); + assertEq(anst.balanceOf(address(pool)), tokens); + + pool.quit(address(this)); + + assertEq(anst.balanceOf(address(this)), tokens); + assertEq(anst.balanceOf(address(pool)), 0); + } + + function test_assetBalance_gets_anst_balanceOf_pool() public { + uint256 tokens = anst.totalSupply(); + assertEq(pool.assetBalance(), 0); + assertEq(anst.balanceOf(address(pool)), 0); + + anst.transfer(address(pool), tokens); + + assertEq(pool.assetBalance(), tokens); + assertEq(anst.balanceOf(address(pool)), tokens); + } + + function test_maxWithdraw_gets_available_assets_nstBal() public { + uint256 tokens = anst.totalSupply(); + anst.transfer(address(pool), tokens); + assertEq(nst.balanceOf(address(anst)), 0); + assertEq(anst.balanceOf(address(pool)), tokens); + + assertEq(pool.maxWithdraw(), 0); + } + + function test_maxDeposit_returns_max_uint() public { + assertEq(pool.maxDeposit(), type(uint256).max); + } +}