// SPDX-License-Identifier: Apache-2.0 // https://docs.soliditylang.org/en/v0.8.10/style-guide.html pragma solidity ^0.8.10; import "lib/forge-std/src/console.sol"; import {ImpactVaultManager} from "src/grants/ImpactVaultManager.sol"; import {StakedGrantManager} from "src/grants/StakedGrantManager.sol"; import {IManager} from "src/interfaces/IManager.sol"; import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {SpiralsRegistry} from "src/grants/SpiralsRegistry.sol"; import {IRegistry} from "src/interfaces/IRegistry.sol"; import {IMoolaLendingPool} from "src/interfaces/IMoolaLendingPool.sol"; /// @title ImpactVault /// /// @author @douglasqian contract ImpactVault is Ownable { using SafeERC20 for IERC20; /* * EVENTS */ event Transfer( address indexed token, address indexed receiver, uint256 indexed amount ); event Deposit( address indexed token, address indexed depositer, uint256 indexed amount ); event Withdraw( address indexed token, address indexed receiver, uint256 indexed amount ); /* * STATE VARIABLES */ bytes32 internal id; SpiralsRegistry public c_spiralsRegistry; // Incremented every time a deposit is made (units in CELO). // This is the max amount that the owner is entitled to withdrawing. // uint256 public celoPrincipalBalance; // All yield withdrawn in CELO up til this point. // uint256 public celoYieldWithdrawn; struct TokenInfo { uint256 principalBalance; uint256 yieldWithdrawn; } mapping(address => TokenInfo) public tokens; /* * CONSTRUCTORS */ constructor( bytes32 _id, address _owner, address _spiralsRegistryAddress ) { id = _id; c_spiralsRegistry = SpiralsRegistry(_spiralsRegistryAddress); transferOwnership(_owner); } /* * DEPOSIT - takes CELO deposits */ // @dev Allows this to work with the Celo gold token as well. function deposit(address _token, uint256 _amount) external onlySupportedTokens(_token) { IERC20(_token).transferFrom(msg.sender, address(this), _amount); _stake(_token, _amount); } function stake(address _token, uint256 _amount) external onlyStakedGrantManager onlySupportedTokens(_token) { _stake(_token, _amount); } function _stake(address _token, uint256 _amount) internal { if (_token == address(getGoldToken())) { getStakedCeloManager().deposit{value: _amount}(); } else if (_token == address(getStableToken())) { IMoolaLendingPool moolaLP = getMoolaCUSDLendingPool(); IERC20(_token).approve(address(moolaLP), _amount); moolaLP.deposit(_token, _amount, address(this), 0); } tokens[_token].principalBalance += _amount; emit Deposit(_token, msg.sender, _amount); } /* * WITHDRAW - transfer stCELO into owner's address to allow them to unstake */ function withdrawAllPrincipal(address _token) external onlyOwner onlySupportedTokens(_token) { uint256 principal = tokens[_token].principalBalance; withdrawPrincipal(_token, principal); } function withdrawPrincipal(address _token, uint256 _amount) public onlyOwner onlySupportedTokens(_token) { require( _amount <= tokens[_token].principalBalance, "WITHDRAW_EXCEED_BALANCE" ); tokens[_token].principalBalance -= _amount; _withdraw(_token, msg.sender, _amount); } function _withdraw( address _token, address _receiver, uint256 _amount ) internal { if (_token == address(getGoldToken())) { // Sends stCELO back to msg.sender uint256 stCeloAmount = getStakedCeloManager().toStakedCelo(_amount); getStakedCeloToken().transfer(_receiver, stCeloAmount); } else if (_token == address(getStableToken())) { // Sends cUSD back to msg.sender getMoolaCUSDLendingPool().withdraw(_token, _amount, _receiver); } emit Withdraw(_token, _receiver, _amount); } /* * YIELD - for managing yield accrued on smart contract */ function withdrawAllYield(address _token) external onlyManager onlySupportedTokens(_token) { withdrawYield(_token, getYield(_token)); } function withdrawYield(address _token, uint256 _amount) public onlyManager onlySupportedTokens(_token) { require(_amount <= getYield(_token), "WITHDRAWAL_EXCEEDS_YIELD"); _withdraw(_token, address(getStakedGrantManager()), _amount); tokens[_token].yieldWithdrawn += _amount; } /* * TRANSFER - transfer value from principal in staked asset to another vault */ function transferPrincipal( address _token, uint256 _amount, address _destVault ) external onlyTransferAuthority onlySupportedTokens(_token) { require(_amount <= getPrincipal(_token), "TRANSFER_EXCEED_PRINCIPAL"); tokens[_token].principalBalance -= _amount; if (_token == address(getGoldToken())) { // Convert to stCELO amount first and transfer stCELO to destVault uint256 stCeloAmount = getStakedCeloManager().toStakedCelo(_amount); getStakedCeloToken().transfer(_destVault, stCeloAmount); } else if (_token == address(getStableToken())) { // Transfer mcUSD directly since it's rebased so 1:1 with cUSD getMCUSDToken().transfer(_destVault, _amount); } emit Transfer(_token, _destVault, _amount); } function setPrincipalBalance(address _token, uint256 _newBalance) external onlyStakedGrantManager onlySupportedTokens(_token) { // TODO risky? this is needed in order to increase principal balance // of beneficiary vault in "StakedGrantManager.disburseFunds" tokens[_token].principalBalance = _newBalance; } /* * MODIFIERS */ modifier onlyManager() { require( msg.sender == address(getStakedGrantManager()), "NOT_STAKED_GRANT_MANAGER" ); _; } modifier onlyTransferAuthority() { require(hasTransferAuthority(msg.sender), "NO_TRANSFER_AUTHORITY"); _; } modifier onlyStakedGrantManager() { require(msg.sender == address(getStakedGrantManager())); _; } function hasTransferAuthority(address _address) internal view returns (bool) { return _address == owner() || _address == address(getStakedGrantManager()); } modifier onlySupportedTokens(address _token) { require(isSupportedToken(_token), "NOT_A_SUPPORTED_TOKEN"); _; } function isSupportedToken(address _token) public view returns (bool) { // TODO: make this more robust, check a mapping or array instead return _token == address(getGoldToken()) || _token == address(getStableToken()); } /* * HELPERS */ function getTotalBalance(address _token) public view onlySupportedTokens(_token) returns (uint256 balance) { if (_token == address(getGoldToken())) { uint256 stCeloBalance = getStakedCeloToken().balanceOf( address(this) ); balance = getStakedCeloManager().toCelo(stCeloBalance); } else if (_token == address(getStableToken())) { balance = getMCUSDToken().balanceOf(address(this)); } return balance; } function getPrincipal(address _token) public view returns (uint256 principal) { return tokens[_token].principalBalance; } function getYieldAllTime(address _token) public view returns (uint256 yield) { return getYield(_token) + tokens[_token].yieldWithdrawn; } /// @dev Could potentially underflow here due to rounding errors, so just /// flor at 0. function getYield(address _token) public view returns (uint256 yield) { uint256 principal = tokens[_token].principalBalance; uint256 total = getTotalBalance(_token); return total >= principal ? total - principal : 0; } /* * ADDRESS GETTERS */ function getGoldToken() internal view returns (IERC20) { return IERC20(getCeloRegistry().getAddressForStringOrDie("GoldToken")); } function getStableToken() internal view returns (IERC20) { return IERC20(getCeloRegistry().getAddressForStringOrDie("StableToken")); } function getStakedGrantManager() internal view returns (StakedGrantManager) { return StakedGrantManager( c_spiralsRegistry.getAddressForStringOrDie( "spirals.StakedGrantManager" ) ); } function getStakedCeloManager() internal view returns (IManager) { return IManager( c_spiralsRegistry.getAddressForStringOrDie("stCELO.Manager") ); } function getStakedCeloToken() internal view returns (IERC20) { return IERC20(c_spiralsRegistry.getAddressForStringOrDie("stCELO.Token")); } function getCeloRegistry() internal view returns (IRegistry) { return IRegistry( c_spiralsRegistry.getAddressForStringOrDie( "celo.SpiralsRegistry" ) ); } function getMoolaCUSDLendingPool() internal view returns (IMoolaLendingPool) { return IMoolaLendingPool( c_spiralsRegistry.getAddressForStringOrDie( "moola.CUSDLendingPool" ) ); } function getMCUSDToken() internal view returns (IERC20) { return IERC20(c_spiralsRegistry.getAddressForStringOrDie("moola.mcUSD")); } }