diff --git a/.gitmodules b/.gitmodules index ff98ee869..5c71a385d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -37,4 +37,12 @@ [submodule "packages/contracts/lib/chainlink-brownie-contracts"] path = packages/contracts/lib/chainlink-brownie-contracts url = https://github.com/smartcontractkit/chainlink-brownie-contracts - branch= main \ No newline at end of file + branch= main +[submodule "packages/contracts/lib/aave-v3-core"] + path = packages/contracts/lib/aave-v3-core + url = https://github.com/aave/aave-v3-core + branch= master +[submodule "packages/contracts/lib/aave-v3-periphery"] + path = packages/contracts/lib/aave-v3-periphery + url = https://github.com/aave/aave-v3-periphery + branch= master diff --git a/cspell.json b/cspell.json index 40f83956d..75164182e 100644 --- a/cspell.json +++ b/cspell.json @@ -162,7 +162,13 @@ "blockhash", "Merkle", "UUPS", - "Initializable" + "Initializable", + "IAMO", + "timelock", + "AAve", + "AAveV3", + "IAAve", + "Cust" ], "flagWords": ["creditNFT", "CreditNFT"], "language": "en-US" diff --git a/packages/contracts/LICENSE_GPL b/packages/contracts/LICENSE_GPL new file mode 100644 index 000000000..3d2480a49 --- /dev/null +++ b/packages/contracts/LICENSE_GPL @@ -0,0 +1 @@ +Sections of this software is licensed under the GNU GPL-2.0-or-later. You can find the source code at: https://github.com/FraxFinance. diff --git a/packages/contracts/foundry.toml b/packages/contracts/foundry.toml index 8b516b440..5260b3552 100644 --- a/packages/contracts/foundry.toml +++ b/packages/contracts/foundry.toml @@ -19,6 +19,7 @@ force = false [rpc_endpoints] mainnet = "https://rpc.ankr.com/eth" +sepolia = "https://1rpc.io/sepolia" [profile.SMT.model_checker] contracts = { } diff --git a/packages/contracts/lib/aave-v3-core b/packages/contracts/lib/aave-v3-core new file mode 160000 index 000000000..b74526a7b --- /dev/null +++ b/packages/contracts/lib/aave-v3-core @@ -0,0 +1 @@ +Subproject commit b74526a7bc67a3a117a1963fc871b3eb8cea8435 diff --git a/packages/contracts/lib/aave-v3-periphery b/packages/contracts/lib/aave-v3-periphery new file mode 160000 index 000000000..72fdcca18 --- /dev/null +++ b/packages/contracts/lib/aave-v3-periphery @@ -0,0 +1 @@ +Subproject commit 72fdcca18838c2f4e05ecd25bbfb44f0db5383f7 diff --git a/packages/contracts/remappings.txt b/packages/contracts/remappings.txt index 638db8d7f..c50eb50a7 100644 --- a/packages/contracts/remappings.txt +++ b/packages/contracts/remappings.txt @@ -8,4 +8,6 @@ solidity-linked-list/=lib/solidity-linked-list @uniswap/v3-periphery/contracts/=lib/Uniswap/v3-periphery/contracts abdk/=lib/abdk-libraries-solidity/ operator-filter-registry/=lib/operator-filter-registry/src -@chainlink/=lib/chainlink-brownie-contracts/contracts/src/v0.8/ \ No newline at end of file +@chainlink/=lib/chainlink-brownie-contracts/contracts/src/v0.8/ +@aavev3-core/contracts/=lib/aave-v3-core/contracts +@aavev3-periphery/contracts/=lib/aave-v3-periphery/contracts \ No newline at end of file diff --git a/packages/contracts/src/dollar/amo/AaveAmo.sol b/packages/contracts/src/dollar/amo/AaveAmo.sol new file mode 100644 index 000000000..5b69a5076 --- /dev/null +++ b/packages/contracts/src/dollar/amo/AaveAmo.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.19; + +import {UbiquityAmoMinter} from "../core/UbiquityAmoMinter.sol"; +import {IAmo} from "../interfaces/IAmo.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IPool} from "@aavev3-core/contracts/interfaces/IPool.sol"; +import {IRewardsController} from "@aavev3-periphery/contracts/rewards/interfaces/IRewardsController.sol"; + +/** + * @title AaveAmo + * @notice AMO to interact with Aave V3: supply and manage rewards. + * @notice Can receive collateral from UbiquityAmoMinter and interact with Aave's V3 pool. + */ +contract AaveAmo is IAmo, Ownable { + using SafeERC20 for ERC20; + + /// @notice UbiquityAmoMinter instance + UbiquityAmoMinter public amoMinter; + + /// @notice Aave V3 pool instance + IPool public immutable aavePool; + + /// @notice Aave rewards controller + IRewardsController public immutable aaveRewardsController; + + /* ========== CONSTRUCTOR ========== */ + + /** + * @notice Initializes the contract with necessary parameters + * @param _ownerAddress Address of the contract owner + * @param _amoMinterAddress Address of the Ubiquity Amo minter + * @param _aavePool Address of the Aave pool + * @param _aaveRewardsController Address of the Aave rewards controller + */ + constructor( + address _ownerAddress, + address _amoMinterAddress, + address _aavePool, + address _aaveRewardsController + ) { + require(_ownerAddress != address(0), "Owner address cannot be zero"); + require( + _amoMinterAddress != address(0), + "Amo minter address cannot be zero" + ); + require(_aavePool != address(0), "Aave pool address cannot be zero"); + require( + _aaveRewardsController != address(0), + "Aave rewards controller address cannot be zero" + ); + + // Set contract owner + transferOwnership(_ownerAddress); + + // Set the Amo minter + amoMinter = UbiquityAmoMinter(_amoMinterAddress); + + // Set the Aave pool + aavePool = IPool(_aavePool); + + // Set the Aave rewards controller + aaveRewardsController = IRewardsController(_aaveRewardsController); + } + + /* ========== Aave V3 + REWARDS ========== */ + + /** + * @notice Deposits collateral to Aave pool + * @param collateralAddress Address of the collateral ERC20 + * @param amount Amount of collateral to deposit + */ + function aaveDepositCollateral( + address collateralAddress, + uint256 amount + ) public onlyOwner { + ERC20 token = ERC20(collateralAddress); + token.safeApprove(address(aavePool), amount); + aavePool.deposit(collateralAddress, amount, address(this), 0); + + emit CollateralDeposited(collateralAddress, amount); + } + + /** + * @notice Withdraws collateral from Aave pool + * @param collateralAddress Address of the collateral ERC20 + * @param aTokenAmount Amount of collateral to withdraw + */ + function aaveWithdrawCollateral( + address collateralAddress, + uint256 aTokenAmount + ) public onlyOwner { + aavePool.withdraw(collateralAddress, aTokenAmount, address(this)); + + emit CollateralWithdrawn(collateralAddress, aTokenAmount); + } + + /** + * @notice Claims all rewards available from the list of assets provided, will fail if balance on asset is zero + * @param assets Array of aTokens/sTokens/vTokens addresses to claim rewards from + */ + function claimAllRewards(address[] memory assets) external { + // Claim all rewards for the collected tokens + aaveRewardsController.claimAllRewards(assets, address(this)); + + emit RewardsClaimed(); + } + + /* ========== RESTRICTED GOVERNANCE FUNCTIONS ========== */ + + /** + * @notice Returns collateral back to the AMO minter + * @param collateralAmount Amount of collateral to return, pass 0 to return all collateral + */ + function returnCollateralToMinter( + uint256 collateralAmount + ) public override onlyOwner { + ERC20 collateralToken = amoMinter.collateralToken(); + + if (collateralAmount == 0) { + collateralAmount = collateralToken.balanceOf(address(this)); + } + + // Approve and return collateral + collateralToken.approve(address(amoMinter), collateralAmount); + amoMinter.receiveCollateralFromAmo(collateralAmount); + + emit CollateralReturnedToMinter(collateralAmount); + } + + /** + * @notice Sets the AMO minter address + * @param _amoMinterAddress New address of the AMO minter + */ + function setAmoMinter( + address _amoMinterAddress + ) external override onlyOwner { + amoMinter = UbiquityAmoMinter(_amoMinterAddress); + + emit AmoMinterSet(_amoMinterAddress); + } + + /** + * @notice Recovers any ERC20 tokens held by the contract + * @param tokenAddress Address of the token to recover + * @param tokenAmount Amount of tokens to recover + */ + function recoverERC20( + address tokenAddress, + uint256 tokenAmount + ) external onlyOwner { + ERC20(tokenAddress).safeTransfer(msg.sender, tokenAmount); + + emit ERC20Recovered(tokenAddress, tokenAmount); + } + + /** + * @notice Executes arbitrary calls from this contract + * @param _to Address to call + * @param _value Value to send + * @param _data Data to execute + * @return success, result Returns whether the call succeeded and the returned data + */ + function execute( + address _to, + uint256 _value, + bytes calldata _data + ) external onlyOwner returns (bool, bytes memory) { + (bool success, bytes memory result) = _to.call{value: _value}(_data); + + emit ExecuteCalled(_to, _value, _data); + return (success, result); + } + + /* ========== EVENTS ========== */ + + event CollateralDeposited( + address indexed collateralAddress, + uint256 amount + ); + event CollateralWithdrawn( + address indexed collateralAddress, + uint256 amount + ); + event CollateralReturnedToMinter(uint256 amount); + event RewardsClaimed(); + event AmoMinterSet(address indexed newMinter); + event ERC20Recovered(address tokenAddress, uint256 tokenAmount); + event ExecuteCalled(address indexed to, uint256 value, bytes data); +} diff --git a/packages/contracts/src/dollar/core/UbiquityAmoMinter.sol b/packages/contracts/src/dollar/core/UbiquityAmoMinter.sol new file mode 100644 index 000000000..2b97a0a54 --- /dev/null +++ b/packages/contracts/src/dollar/core/UbiquityAmoMinter.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.19; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IUbiquityPool} from "../interfaces/IUbiquityPool.sol"; + +/** + * @title UbiquityAmoMinter + * @notice Contract responsible for managing collateral borrowing from Ubiquity's Pool to AMOs. + * @notice Allows owner to move Dollar collateral to AMOs, enabling yield generation. + * @notice It keeps track of borrowed collateral balances per Amo and the total borrowed sum. + */ +contract UbiquityAmoMinter is Ownable { + using SafeERC20 for ERC20; + + /// @notice Collateral token used by the AMO minter + ERC20 public immutable collateralToken; + + /// @notice Ubiquity pool interface + IUbiquityPool public pool; + + /// @notice Collateral-related properties + address public immutable collateralAddress; + uint256 public immutable collateralIndex; // Index of the collateral in the pool + uint256 public immutable missingDecimals; + int256 public collateralBorrowCap = int256(100_000e18); + + /// @notice Mapping for tracking borrowed collateral balances per AMO + mapping(address => int256) public collateralBorrowedBalances; + + /// @notice Sum of all collateral borrowed across Amos + int256 public collateralTotalBorrowedBalance = 0; + + /// @notice Mapping to track active AMOs + mapping(address => bool) public Amos; + + /* ========== CONSTRUCTOR ========== */ + + /** + * @notice Initializes the Amo minter contract + * @param _ownerAddress Address of the contract owner + * @param _collateralAddress Address of the collateral token + * @param _collateralIndex Index of the collateral in the pool + * @param _poolAddress Address of the Ubiquity pool + */ + constructor( + address _ownerAddress, + address _collateralAddress, + uint256 _collateralIndex, + address _poolAddress + ) { + require(_ownerAddress != address(0), "Owner address cannot be zero"); + require(_poolAddress != address(0), "Pool address cannot be zero"); + + // Set the owner + transferOwnership(_ownerAddress); + + // Pool related + pool = IUbiquityPool(_poolAddress); + + // Collateral related + collateralAddress = _collateralAddress; + collateralIndex = _collateralIndex; + collateralToken = ERC20(_collateralAddress); + missingDecimals = uint256(18) - collateralToken.decimals(); + + emit OwnershipTransferred(_ownerAddress); + emit PoolSet(_poolAddress); + } + + /* ========== MODIFIERS ========== */ + + /** + * @notice Ensures the caller is a valid AMO + * @param amoAddress Address of the AMO to check + */ + modifier validAmo(address amoAddress) { + require(Amos[amoAddress], "Invalid Amo"); + _; + } + + /* ========== AMO MANAGEMENT FUNCTIONS ========== */ + + /** + * @notice Enables an AMO + * @param amo Address of the AMO to enable + */ + function enableAmo(address amo) external onlyOwner { + Amos[amo] = true; + } + + /** + * @notice Disables an AMO + * @param amo Address of the AMO to disable + */ + function disableAmo(address amo) external onlyOwner { + Amos[amo] = false; + } + + /* ========== COLLATERAL FUNCTIONS ========== */ + + /** + * @notice Transfers collateral to the specified AMO + * @param destinationAmo Address of the AMO to receive collateral + * @param collateralAmount Amount of collateral to transfer + */ + function giveCollateralToAmo( + address destinationAmo, + uint256 collateralAmount + ) external onlyOwner validAmo(destinationAmo) { + require( + collateralToken.balanceOf(address(pool)) >= collateralAmount, + "Insufficient balance" + ); + + int256 collateralAmount_i256 = int256(collateralAmount); + + require( + (collateralTotalBorrowedBalance + collateralAmount_i256) <= + collateralBorrowCap, + "Borrow cap exceeded" + ); + + collateralBorrowedBalances[destinationAmo] += collateralAmount_i256; + collateralTotalBorrowedBalance += collateralAmount_i256; + + // Borrow collateral from the pool + pool.amoMinterBorrow(collateralAmount); + + // Transfer collateral to the AMO + collateralToken.safeTransfer(destinationAmo, collateralAmount); + + emit CollateralGivenToAmo(destinationAmo, collateralAmount); + } + + /** + * @notice Receives collateral back from an AMO + * @param collateralAmount Amount of collateral being returned + */ + function receiveCollateralFromAmo( + uint256 collateralAmount + ) external validAmo(msg.sender) { + int256 collateralAmount_i256 = int256(collateralAmount); + + // Update the collateral balances + collateralBorrowedBalances[msg.sender] -= collateralAmount_i256; + collateralTotalBorrowedBalance -= collateralAmount_i256; + + // Transfer collateral back to the pool + collateralToken.safeTransferFrom( + msg.sender, + address(pool), + collateralAmount + ); + + emit CollateralReceivedFromAmo(msg.sender, collateralAmount); + } + + /* ========== RESTRICTED GOVERNANCE FUNCTIONS ========== */ + + /** + * @notice Updates the collateral borrow cap + * @param _collateralBorrowCap New collateral borrow cap + */ + function setCollateralBorrowCap( + uint256 _collateralBorrowCap + ) external onlyOwner { + collateralBorrowCap = int256(_collateralBorrowCap); + emit CollateralBorrowCapSet(_collateralBorrowCap); + } + + /** + * @notice Updates the pool address + * @param _poolAddress New pool address + */ + function setPool(address _poolAddress) external onlyOwner { + pool = IUbiquityPool(_poolAddress); + emit PoolSet(_poolAddress); + } + + /* =========== VIEWS ========== */ + + /** + * @notice Returns the total value of borrowed collateral + * @return Total balance of collateral borrowed + */ + function collateralDollarBalance() external view returns (uint256) { + return uint256(collateralTotalBorrowedBalance); + } + + /* ========== EVENTS ========== */ + + event CollateralGivenToAmo( + address destinationAmo, + uint256 collateralAmount + ); + event CollateralReceivedFromAmo( + address sourceAmo, + uint256 collateralAmount + ); + event CollateralBorrowCapSet(uint256 newCollateralBorrowCap); + event PoolSet(address newPoolAddress); + event OwnershipTransferred(address newOwner); +} diff --git a/packages/contracts/src/dollar/interfaces/IAaveAmo.sol b/packages/contracts/src/dollar/interfaces/IAaveAmo.sol new file mode 100644 index 000000000..025e11c7b --- /dev/null +++ b/packages/contracts/src/dollar/interfaces/IAaveAmo.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IAaveAmo { + /** + * @notice Deposits collateral into the Aave pool + * @param collateralAddress Address of the collateral ERC20 token + * @param amount Amount of collateral to deposit + */ + function aaveDepositCollateral( + address collateralAddress, + uint256 amount + ) external; + + /** + * @notice Withdraws collateral from the Aave pool + * @param collateralAddress Address of the collateral ERC20 token + * @param aTokenAmount Amount of aTokens (collateral) to withdraw + */ + function aaveWithdrawCollateral( + address collateralAddress, + uint256 aTokenAmount + ) external; + + /** + * @notice Borrows an asset from the Aave pool + * @param asset Address of the asset to borrow + * @param borrowAmount Amount of the asset to borrow + * @param interestRateMode Interest rate mode: 1 for stable, 2 for variable + */ + function aaveBorrow( + address asset, + uint256 borrowAmount, + uint256 interestRateMode + ) external; + + /** + * @notice Repays a borrowed asset to the Aave pool + * @param asset Address of the asset to repay + * @param repayAmount Amount of the asset to repay + * @param interestRateMode Interest rate mode: 1 for stable, 2 for variable + */ + function aaveRepay( + address asset, + uint256 repayAmount, + uint256 interestRateMode + ) external; + + /** + * @notice Claims all rewards from the provided assets + * @param assets Array of aTokens/sTokens/vTokens addresses to claim rewards from + */ + function claimAllRewards(address[] memory assets) external; + + /** + * @notice Returns collateral back to the AMO minter + * @param collateralAmount Amount of collateral to return + */ + function returnCollateralToMinter(uint256 collateralAmount) external; + + /** + * @notice Sets the address of the AMO minter + * @param _amoMinterAddress New address of the AMO minter + */ + function setAmoMinter(address _amoMinterAddress) external; + + /** + * @notice Recovers any ERC20 tokens held by the contract + * @param tokenAddress Address of the token to recover + * @param tokenAmount Amount of tokens to recover + */ + function recoverERC20(address tokenAddress, uint256 tokenAmount) external; + + /** + * @notice Executes an arbitrary call from the contract + * @param _to Address to call + * @param _value Value to send with the call + * @param _data Data to execute in the call + * @return success Boolean indicating whether the call succeeded + * @return result Bytes data returned from the call + */ + function execute( + address _to, + uint256 _value, + bytes calldata _data + ) external returns (bool, bytes memory); + + /** + * @notice Emitted when collateral is deposited into the Aave pool + * @param collateralAddress Address of the collateral token + * @param amount Amount of collateral deposited + */ + event CollateralDeposited( + address indexed collateralAddress, + uint256 amount + ); + + /** + * @notice Emitted when collateral is withdrawn from the Aave pool + * @param collateralAddress Address of the collateral token + * @param amount Amount of collateral withdrawn + */ + event CollateralWithdrawn( + address indexed collateralAddress, + uint256 amount + ); + + /** + * @notice Emitted when an asset is borrowed from the Aave pool + * @param asset Address of the asset borrowed + * @param amount Amount of asset borrowed + * @param interestRateMode Interest rate mode used for the borrow (1 for stable, 2 for variable) + */ + event Borrowed( + address indexed asset, + uint256 amount, + uint256 interestRateMode + ); + + /** + * @notice Emitted when a borrowed asset is repaid to the Aave pool + * @param asset Address of the asset repaid + * @param amount Amount of asset repaid + * @param interestRateMode Interest rate mode used for the repay (1 for stable, 2 for variable) + */ + event Repaid( + address indexed asset, + uint256 amount, + uint256 interestRateMode + ); + + /** + * @notice Emitted when collateral is returned to the AMO minter + * @param amount Amount of collateral returned + */ + event CollateralReturnedToMinter(uint256 amount); + + /** + * @notice Emitted when rewards are claimed + */ + event RewardsClaimed(); + + /** + * @notice Emitted when the AMO minter address is set + * @param newMinter Address of the new AMO minter + */ + event AmoMinterSet(address indexed newMinter); + + /** + * @notice Emitted when ERC20 tokens are recovered from the contract + * @param tokenAddress Address of the recovered token + * @param tokenAmount Amount of tokens recovered + */ + event ERC20Recovered(address tokenAddress, uint256 tokenAmount); + + /** + * @notice Emitted when an arbitrary call is executed from the contract + * @param to Address of the call target + * @param value Value sent with the call + * @param data Data sent with the call + */ + event ExecuteCalled(address indexed to, uint256 value, bytes data); +} diff --git a/packages/contracts/src/dollar/interfaces/IAmo.sol b/packages/contracts/src/dollar/interfaces/IAmo.sol new file mode 100644 index 000000000..8dca94c7d --- /dev/null +++ b/packages/contracts/src/dollar/interfaces/IAmo.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IAmo { + /** + * @notice Returns collateral back to the AMO minter + * @param collateralAmount Amount of collateral to return + */ + function returnCollateralToMinter(uint256 collateralAmount) external; + + /** + * @notice Sets the address of the AMO minter + * @param _amoMinterAddress New address of the AMO minter + */ + function setAmoMinter(address _amoMinterAddress) external; +} diff --git a/packages/contracts/src/dollar/interfaces/IUbiquityAmoMinter.sol b/packages/contracts/src/dollar/interfaces/IUbiquityAmoMinter.sol new file mode 100644 index 000000000..2587402bd --- /dev/null +++ b/packages/contracts/src/dollar/interfaces/IUbiquityAmoMinter.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.19; + +interface IUbiquityAmoMinter { + /** + * @notice Enables an AMO for collateral transfers + * @param amo Address of the AMO to enable + */ + function enableAmo(address amo) external; + + /** + * @notice Disables an AMO, preventing further collateral transfers + * @param amo Address of the AMO to disable + */ + function disableAmo(address amo) external; + + /** + * @notice Transfers collateral to a specified AMO + * @param destinationAmo Address of the AMO to receive collateral + * @param collateralAmount Amount of collateral to transfer + */ + function giveCollateralToAmo( + address destinationAmo, + uint256 collateralAmount + ) external; + + /** + * @notice Receives collateral back from an AMO + * @param collateralAmount Amount of collateral being returned + */ + function receiveCollateralFromAmo(uint256 collateralAmount) external; + + /** + * @notice Updates the maximum allowable borrowed collateral + * @param _collateralBorrowCap New collateral borrow cap value + */ + function setCollateralBorrowCap(uint256 _collateralBorrowCap) external; + + /** + * @notice Updates the address of the Ubiquity pool + * @param _poolAddress New pool address + */ + function setPool(address _poolAddress) external; + + /** + * @notice Returns the total balance of collateral borrowed by all AMOs + * @return Total balance of collateral borrowed + */ + function collateralDollarBalance() external view returns (uint256); + + /** + * @notice Emitted when collateral is given to an AMO + * @param destinationAmo Address of the AMO receiving the collateral + * @param collateralAmount Amount of collateral transferred + */ + event CollateralGivenToAmo( + address destinationAmo, + uint256 collateralAmount + ); + + /** + * @notice Emitted when collateral is returned from an AMO + * @param sourceAmo Address of the AMO returning the collateral + * @param collateralAmount Amount of collateral returned + */ + event CollateralReceivedFromAmo( + address sourceAmo, + uint256 collateralAmount + ); + + /** + * @notice Emitted when the collateral borrow cap is updated + * @param newCollateralBorrowCap The updated collateral borrow cap + */ + event CollateralBorrowCapSet(uint256 newCollateralBorrowCap); + + /** + * @notice Emitted when the Ubiquity pool address is updated + * @param newPoolAddress The updated pool address + */ + event PoolSet(address newPoolAddress); + + /** + * @notice Emitted when ownership of the contract is transferred + * @param newOwner Address of the new contract owner + */ + event OwnershipTransferred(address newOwner); +} diff --git a/packages/contracts/test/amo/AaveAmo.t.sol b/packages/contracts/test/amo/AaveAmo.t.sol new file mode 100644 index 000000000..6b7b5d53e --- /dev/null +++ b/packages/contracts/test/amo/AaveAmo.t.sol @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import {DiamondTestSetup} from "../diamond/DiamondTestSetup.sol"; +import {UbiquityAmoMinter} from "../../src/dollar/core/UbiquityAmoMinter.sol"; +import {AaveAmo} from "../../src/dollar/amo/AaveAmo.sol"; +import {MockERC20} from "../../src/dollar/mocks/MockERC20.sol"; +import {MockChainLinkFeed} from "../../src/dollar/mocks/MockChainLinkFeed.sol"; +import {IPool} from "@aavev3-core/contracts/interfaces/IPool.sol"; +import {IAToken} from "@aavev3-core/contracts/interfaces/IAToken.sol"; +import {IVariableDebtToken} from "@aavev3-core/contracts/interfaces/IVariableDebtToken.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract AaveAmoTest is DiamondTestSetup { + UbiquityAmoMinter amoMinter; + AaveAmo aaveAmo; + address rewardsController = + address(0x4DA5c4da71C5a167171cC839487536d86e083483); // Aave Rewards Controller + address collateralOwner = + address(0xC959483DBa39aa9E78757139af0e9a2EDEb3f42D); // Aave Sepolia Faucet + MockERC20 collateralToken = + MockERC20(0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357); // DAI-TestnetMintableERC20-Aave Sepolia + MockChainLinkFeed collateralTokenPriceFeed = + MockChainLinkFeed(0x9aF11c35c5d3Ae182C0050438972aac4376f9516); // DAI-TestnetPriceAggregator-Aave Sepolia + IAToken aToken = IAToken(0x29598b72eb5CeBd806C5dCD549490FdA35B13cD8); // DAI-AToken-Aave Sepolia + IVariableDebtToken vToken = + IVariableDebtToken(0x22675C506A8FC26447aFFfa33640f6af5d4D4cF0); // DAI-VariableDebtToken-Aave Sepolia + + // Constants for the test + address constant newAmoMinterAddress = address(5); // mock new Amo minter address + address constant nonAmo = address(9999); // Address representing a non-Amo entity + uint256 constant interestRateMode = 2; // Variable interest rate mode in Aave + + // Mocking the Aave Pool + IPool private constant aavePool = + IPool(0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951); // Aave V3 Sepolia Pool + + function setUp() public override { + vm.createSelectFork(vm.rpcUrl("sepolia")); + super.setUp(); + + // Deploy UbiquityAmoMinter contract + amoMinter = new UbiquityAmoMinter( + owner, + address(collateralToken), + 0, + address(ubiquityPoolFacet) + ); + + // Deploy AaveAmo contract + aaveAmo = new AaveAmo( + owner, + address(amoMinter), + address(aavePool), + address(rewardsController) + ); + + // Enable AaveAmo as a valid Amo + vm.prank(owner); + amoMinter.enableAmo(address(aaveAmo)); + + vm.startPrank(admin); + + // Add collateral token to the pool + uint256 poolCeiling = 500_000e18; + ubiquityPoolFacet.addCollateralToken( + address(collateralToken), + address(collateralTokenPriceFeed), + poolCeiling + ); + + // Enable collateral and register Amo Minter + ubiquityPoolFacet.toggleCollateral(0); + ubiquityPoolFacet.addAmoMinter(address(amoMinter)); + + vm.stopPrank(); + } + + /* ========== Aave Amo SETUP TESTS ========== */ + + function testAaveAmoSetup_ShouldSet_owner() public { + // Verify the owner was set correctly + assertEq(aaveAmo.owner(), owner); + } + + function testAaveAmoSetup_ShouldSet_amoMinter() public { + // Verify the Amo minter was set correctly + assertEq(address(aaveAmo.amoMinter()), address(amoMinter)); + } + + function testAaveAmoSetup_ShouldSet_aavePool() public { + // Verify the Aave pool was set correctly + assertEq(address(aaveAmo.aavePool()), address(aavePool)); + } + + function testAaveAmoSetup_ShouldSet_aaveRewardsController() public { + // Verify the Aave rewards controller was set correctly + assertEq( + address(aaveAmo.aaveRewardsController()), + address(rewardsController) + ); + } + + function testConstructor_ShouldRevertWhenOwnerIsZeroAddress() public { + // Test with zero address for owner + vm.expectRevert("Owner address cannot be zero"); + new AaveAmo( + address(0), // Invalid owner address + address(amoMinter), + address(aavePool), + address(rewardsController) + ); + } + + function testConstructor_ShouldRevertWhenAmoMinterIsZeroAddress() public { + // Test with zero address for Amo minter + vm.expectRevert("Amo minter address cannot be zero"); + new AaveAmo( + owner, + address(0), // Invalid Amo minter address + address(aavePool), + address(rewardsController) + ); + } + + function testConstructor_ShouldRevertWhenAavePoolIsZeroAddress() public { + // Test with zero address for Aave pool + vm.expectRevert("Aave pool address cannot be zero"); + new AaveAmo( + owner, + address(amoMinter), + address(0), // Invalid Aave pool address + address(rewardsController) + ); + } + + function testConstructor_ShouldRevertWhenAaveRewardsControllerIsZeroAddress() + public + { + // Test with zero address for Aave + vm.expectRevert("Aave rewards controller address cannot be zero"); + new AaveAmo( + owner, + address(amoMinter), + address(aavePool), + address(0) // Invalid Aave rewards controller address + ); + } + + /* ========== Aave Amo COLLATERAL TESTS ========== */ + + function testAaveDepositCollateral_ShouldDepositSuccessfully() public { + uint256 depositAmount = 1000e18; + + // Mints collateral to Amo + vm.prank(collateralOwner); + collateralToken.mint(address(aaveAmo), depositAmount); + + // Owner deposits collateral to Aave Pool + vm.prank(owner); + aaveAmo.aaveDepositCollateral(address(collateralToken), depositAmount); + + // Check if the deposit was successful + assertApproxEqAbs( + aToken.balanceOf(address(aaveAmo)), + depositAmount, + 1e2 + ); // little error this is due to interest rate + assertEq(collateralToken.balanceOf(address(aaveAmo)), 0); + } + + function testAaveWithdrawCollateral_ShouldWithdrawSuccessfully() public { + uint256 depositAmount = 1000e18; + + // Mints collateral to Amo + vm.prank(collateralOwner); + collateralToken.mint(address(aaveAmo), depositAmount); + + // Owner deposits collateral to Aave Pool + vm.prank(owner); + aaveAmo.aaveDepositCollateral(address(collateralToken), depositAmount); + + // Check balances before withdrawal + assertApproxEqAbs( + aToken.balanceOf(address(aaveAmo)), + depositAmount, + 1e2 + ); // little error this is due to interest rate + assertEq(collateralToken.balanceOf(address(aaveAmo)), 0); + + uint256 withdrawAmount = aToken.balanceOf(address(aaveAmo)); + + // Owner withdraws collateral from Aave Pool + vm.prank(owner); + aaveAmo.aaveWithdrawCollateral( + address(collateralToken), + withdrawAmount + ); + assertEq(aToken.balanceOf(address(aaveAmo)), 0); + assertEq(collateralToken.balanceOf(address(aaveAmo)), withdrawAmount); + } + + function testAaveDeposit_ShouldRevertIfNotOwner() public { + uint256 depositAmount = 1e18; + + // Attempting to deposit as a non-owner should revert + vm.prank(nonAmo); + vm.expectRevert("Ownable: caller is not the owner"); + aaveAmo.aaveDepositCollateral(address(collateralToken), depositAmount); + } + + function testAaveWithdraw_ShouldRevertIfNotOwner() public { + uint256 withdrawAmount = 1e18; + + // Attempting to withdraw as a non-owner should revert + vm.prank(nonAmo); + vm.expectRevert("Ownable: caller is not the owner"); + aaveAmo.aaveWithdrawCollateral( + address(collateralToken), + withdrawAmount + ); + } + + /* ========== Aave Amo MINTER TESTS ========== */ + + function testReturnCollateralToMinter_ShouldWork() public { + uint256 returnAmount = 1000e18; + + // Mints collateral to Amo + vm.prank(collateralOwner); + collateralToken.mint(address(aaveAmo), returnAmount); + + // Owner returns collateral to the Amo Minter + vm.prank(owner); + aaveAmo.returnCollateralToMinter(returnAmount); + + // Verify pool received collateral + assertEq( + collateralToken.balanceOf(address(ubiquityPoolFacet)), + returnAmount + ); + } + + function testReturnCollateralToMinter_ShouldRevertIfNotOwner() public { + uint256 returnAmount = 1000e18; + + // Revert if a non-owner tries to return collateral + vm.prank(nonAmo); + vm.expectRevert("Ownable: caller is not the owner"); + aaveAmo.returnCollateralToMinter(returnAmount); + } + + function testSetAmoMinter_ShouldWorkWhenCalledByOwner() public { + // Set new Amo minter address + vm.prank(owner); + aaveAmo.setAmoMinter(newAmoMinterAddress); + + // Verify the new Amo minter address was set + assertEq(address(aaveAmo.amoMinter()), newAmoMinterAddress); + } + + function testSetAmoMinter_ShouldRevertIfNotOwner() public { + // Attempting to set a new Amo minter address as a non-owner should revert + vm.prank(nonAmo); + vm.expectRevert("Ownable: caller is not the owner"); + aaveAmo.setAmoMinter(newAmoMinterAddress); + } + + /* =========== Aave Amo REWARDS TESTS =========== */ + + function testClaimAllRewards_ShouldClaimRewardsSuccessfully() public { + uint256 depositAmount = 1000e18; + + // Mints collateral to Amo + vm.prank(collateralOwner); + collateralToken.mint(address(aaveAmo), depositAmount); + + // Owner deposits collateral to Aave Pool + vm.prank(owner); + aaveAmo.aaveDepositCollateral(address(collateralToken), depositAmount); + + // Specify assets to claim rewards for + address[] memory assets = new address[](1); + assets[0] = aavePool + .getReserveData(address(collateralToken)) + .aTokenAddress; + + // Claim rewards from Aave + vm.prank(owner); + aaveAmo.claimAllRewards(assets); + + // Verify the rewards were claimed successfully + assertTrue(true); + } + + /* ========== Aave Amo EMERGENCY TESTS ========== */ + + function testRecoverERC20_ShouldTransferERC20ToOwner() public { + uint256 tokenAmount = 1000e18; + + // Mint some tokens to AaveAmo + MockERC20 mockToken = new MockERC20("Mock Token", "MTK", 18); + mockToken.mint(address(aaveAmo), tokenAmount); + + // Recover tokens as the owner + vm.prank(owner); + aaveAmo.recoverERC20(address(mockToken), tokenAmount); + + // Check if the tokens were transferred to the owner + assertEq(mockToken.balanceOf(owner), tokenAmount); + } + + function testRecoverERC20_ShouldRevertIfNotOwner() public { + uint256 tokenAmount = 1000e18; + + // Revert if non-owner attempts to recover tokens + vm.prank(nonAmo); + vm.expectRevert("Ownable: caller is not the owner"); + aaveAmo.recoverERC20(address(collateralToken), tokenAmount); + } + + function testExecute_ShouldExecuteCallSuccessfully() public { + // Example of executing a simple call + vm.prank(owner); + (bool success, ) = aaveAmo.execute(owner, 0, ""); + + // Verify the call executed successfully + assertTrue(success); + } + + function testExecute_ShouldRevertIfNotOwner() public { + // Attempting to call execute as a non-owner should revert + vm.prank(nonAmo); + vm.expectRevert("Ownable: caller is not the owner"); + aaveAmo.execute(owner, 0, ""); + } +} diff --git a/packages/contracts/test/amo/UbiquityAmoMinter.t.sol b/packages/contracts/test/amo/UbiquityAmoMinter.t.sol new file mode 100644 index 000000000..4613e0d79 --- /dev/null +++ b/packages/contracts/test/amo/UbiquityAmoMinter.t.sol @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import {DiamondTestSetup} from "../diamond/DiamondTestSetup.sol"; +import {UbiquityAmoMinter} from "../../src/dollar/core/UbiquityAmoMinter.sol"; +import {AaveAmo} from "../../src/dollar/amo/AaveAmo.sol"; +import {MockERC20} from "../../src/dollar/mocks/MockERC20.sol"; +import {IUbiquityPool} from "../../src/dollar/interfaces/IUbiquityPool.sol"; +import {MockChainLinkFeed} from "../../src/dollar/mocks/MockChainLinkFeed.sol"; + +contract UbiquityAmoMinterTest is DiamondTestSetup { + UbiquityAmoMinter amoMinter; + AaveAmo aaveAmo; + MockERC20 collateralToken; + MockChainLinkFeed collateralTokenPriceFeed; + + address newPoolAddress = address(4); // mock new pool address + address nonAmo = address(9999); + + function setUp() public override { + super.setUp(); + + // Initialize mock collateral token and price feed + collateralToken = new MockERC20("Mock Collateral", "MCT", 18); + collateralTokenPriceFeed = new MockChainLinkFeed(); + + // Deploy UbiquityAmoMinter contract + amoMinter = new UbiquityAmoMinter( + owner, + address(collateralToken), // Collateral token address + 0, // Collateral index + address(ubiquityPoolFacet) // Pool address + ); + + // Deploy AaveAmo contract + aaveAmo = new AaveAmo( + owner, + address(amoMinter), + address(1), + address(2) + ); + + // Enable AaveAmo as a valid Amo + vm.prank(owner); + amoMinter.enableAmo(address(aaveAmo)); + + vm.startPrank(admin); // Prank as admin for pool setup + + // Add collateral token to the pool with a ceiling + uint256 poolCeiling = 500_000e18; + ubiquityPoolFacet.addCollateralToken( + address(collateralToken), + address(collateralTokenPriceFeed), + poolCeiling + ); + + // Enable collateral and register Amo Minter + ubiquityPoolFacet.toggleCollateral(0); + ubiquityPoolFacet.addAmoMinter(address(amoMinter)); + + // Mint collateral to the pool + collateralToken.mint(address(ubiquityPoolFacet), 500_000e18); + + vm.stopPrank(); + } + + function testConstructor_ShouldInitializeCorrectly() public { + // Deploy a new instance of the UbiquityAmoMinter contract + UbiquityAmoMinter newAmoMinter = new UbiquityAmoMinter( + owner, + address(collateralToken), // Collateral token address + 0, // Collateral index + address(ubiquityPoolFacet) // Pool address + ); + + // Verify the owner is set correctly + assertEq(newAmoMinter.owner(), owner); + + // Verify the collateral token is set correctly + assertEq( + address(newAmoMinter.collateralToken()), + address(collateralToken) + ); + + // Verify the collateral index is set correctly + assertEq(newAmoMinter.collateralIndex(), 0); + + // Verify the pool address is set correctly + assertEq(address(newAmoMinter.pool()), address(ubiquityPoolFacet)); + + // Verify the missing decimals calculation + assertEq( + newAmoMinter.missingDecimals(), + uint256(18) - collateralToken.decimals() + ); + } + + function testConstructor_ShouldRevertIfOwnerIsZero() public { + // Ensure the constructor reverts if the owner address is zero + vm.expectRevert("Owner address cannot be zero"); + new UbiquityAmoMinter( + address(0), + address(collateralToken), // Collateral token address + 0, // Collateral index + address(ubiquityPoolFacet) // Pool address + ); + } + + function testConstructor_ShouldRevertIfPoolAddressIsZero() public { + // Ensure the constructor reverts if the pool address is zero + vm.expectRevert("Pool address cannot be zero"); + new UbiquityAmoMinter( + owner, + address(collateralToken), // Collateral token address + 0, // Collateral index + address(0) // Pool address + ); + } + + /* ========== Tests for Amo management ========== */ + + function testEnableAmo_ShouldWorkWhenCalledByOwner() public { + // Test enabling a new Amo + address newAmo = address(10); + vm.prank(owner); + amoMinter.enableAmo(newAmo); + + // Check if the new Amo is enabled + assertEq(amoMinter.Amos(newAmo), true); + } + + function testDisableAmo_ShouldWorkWhenCalledByOwner() public { + // Test disabling the AaveAmo + vm.prank(owner); + amoMinter.disableAmo(address(aaveAmo)); + + // Check if the Amo is disabled + assertEq(amoMinter.Amos(address(aaveAmo)), false); + } + + function testEnableAmo_ShouldRevertWhenCalledByNonOwner() public { + // Ensure only the owner can enable Amos + address newAmo = address(10); + vm.prank(nonAmo); + vm.expectRevert("Ownable: caller is not the owner"); + amoMinter.enableAmo(newAmo); + } + + function testDisableAmo_ShouldRevertWhenCalledByNonOwner() public { + // Ensure only the owner can disable Amos + vm.prank(nonAmo); + vm.expectRevert("Ownable: caller is not the owner"); + amoMinter.disableAmo(address(aaveAmo)); + } + + /* ========== Tests for giveCollateralToAmo ========== */ + + function testGiveCollatToAmo_ShouldWorkWhenCalledByOwner() public { + uint256 collatAmount = 1000e18; + + // Owner gives collateral to the AaveAmo + vm.prank(owner); + amoMinter.giveCollateralToAmo(address(aaveAmo), collatAmount); + + // Verify the balances + assertEq( + amoMinter.collateralBorrowedBalances(address(aaveAmo)), + int256(collatAmount) + ); + assertEq( + amoMinter.collateralTotalBorrowedBalance(), + int256(collatAmount) + ); + } + + function testGiveCollatToAmo_ShouldRevertWhenNotValidAmo() public { + uint256 collatAmount = 1000e18; + + // Ensure giving collateral to a non-Amo address reverts + vm.prank(owner); + vm.expectRevert("Invalid Amo"); + amoMinter.giveCollateralToAmo(nonAmo, collatAmount); + } + + function testGiveCollatToAmo_ShouldRevertWhenExceedingBorrowCap() public { + uint256 collatAmount = 200000e18; // Exceeds the default borrow cap of 100_000 + + // Ensure exceeding the borrow cap reverts + vm.prank(owner); + vm.expectRevert("Borrow cap exceeded"); + amoMinter.giveCollateralToAmo(address(aaveAmo), collatAmount); + } + + /* ========== Tests for receiveCollateralFromAmo ========== */ + + // This function is actually intended to be called by the Amo, but we can test it by calling it directly + function testReceiveCollatFromAmo_ShouldWorkWhenCalledByValidAmo() public { + uint256 collatAmount = 1000e18; + + uint256 poolBalance = collateralToken.balanceOf( + address(ubiquityPoolFacet) + ); + + // First, give collateral to the Amo + vm.prank(owner); + amoMinter.giveCollateralToAmo(address(aaveAmo), collatAmount); + + // Amo returns collateral + vm.startPrank(address(aaveAmo)); + collateralToken.approve(address(amoMinter), collatAmount); + amoMinter.receiveCollateralFromAmo(collatAmount); + vm.stopPrank(); + + // Verify the balances + assertEq(amoMinter.collateralBorrowedBalances(address(aaveAmo)), 0); + assertEq(amoMinter.collateralTotalBorrowedBalance(), 0); + assertEq(collateralToken.balanceOf(address(aaveAmo)), 0); + assertEq(collateralToken.balanceOf(address(amoMinter)), 0); + assertEq( + poolBalance, + collateralToken.balanceOf(address(ubiquityPoolFacet)) + ); + } + + function testReceiveCollatFromAmo_ShouldRevertWhenNotValidAmo() public { + uint256 collatAmount = 1000e18; + + // Ensure non-Amo cannot return collateral + vm.prank(nonAmo); + vm.expectRevert("Invalid Amo"); + amoMinter.receiveCollateralFromAmo(collatAmount); + } + + /* ========== Tests for setCollateralBorrowCap ========== */ + + function testSetCollatBorrowCap_ShouldWorkWhenCalledByOwner() public { + uint256 newCap = 5000000e6; // new cap + + // Owner sets new collateral borrow cap + vm.prank(owner); + amoMinter.setCollateralBorrowCap(newCap); + + // Verify the collateral borrow cap was updated + assertEq(amoMinter.collateralBorrowCap(), int256(newCap)); + } + + function testSetCollatBorrowCap_ShouldRevertWhenCalledByNonOwner() public { + uint256 newCap = 5000000e6; // new cap + + // Ensure non-owner cannot set the cap + vm.prank(address(1234)); + vm.expectRevert("Ownable: caller is not the owner"); + amoMinter.setCollateralBorrowCap(newCap); + } + + /* ========== Tests for setPool ========== */ + + function testSetPool_ShouldWorkWhenCalledByOwner() public { + // Owner sets new pool + vm.prank(owner); + amoMinter.setPool(newPoolAddress); + + // Verify the pool address was updated + assertEq(address(amoMinter.pool()), newPoolAddress); + } + + function testSetPool_ShouldRevertWhenCalledByNonOwner() public { + // Ensure non-owner cannot set the pool + vm.prank(address(1234)); + vm.expectRevert("Ownable: caller is not the owner"); + amoMinter.setPool(newPoolAddress); + } +}