// SPDX-License-Identifier: Apache-2.0 // https://docs.soliditylang.org/en/v0.8.10/style-guide.html pragma solidity ^0.8.10; import {IAccount} from "lib/staked-celo/contracts/interfaces/IAccount.sol"; import {IManager} from "./IManager.sol"; import {IManaged} from "./IManaged.sol"; import {ILockedGold} from "./ILockedGold.sol"; import {IRegistry} from "./IRegistry.sol"; import {StakedERC4626Upgradeable} from "./StakedERC4626Upgradeable.sol"; import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IERC20Upgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; import {ERC4626Upgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol"; import {IERC20MetadataUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; import {SafeERC20Upgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/utils/SafeERC20Upgradeable.sol"; import {MathUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/utils/math/MathUpgradeable.sol"; /// @title SpiralsCeloVault /// @author @douglasqian @no40 /// @notice This is a modification of the EIP-4626 tokenized vault standard /// for yield-bearing tokens where the yield accrued on stCELO is held within /// the vault instead of being distributed to depositors. /// /// Invariant: /// Manager().toCelo(x) == Manager().toCelo(y) /// where Manager() is the stCELO Manager.sol contract /// x is the initial amount of stCELO deposited /// y is the amount of stCELO at the time of withdrawal /// /// *This invariant holds regardless of how much stCELO's value has appreciated /// against CELO, but note that this does assume stCELO will increase in value /// relative to CELO. /// contract SpiralsCeloVault is StakedERC4626Upgradeable { error OutstandingWithdrawal(uint256 value, uint256 ts); error NoWithdrawalReady(uint256 readyTimestamp); event Receive(address indexed sender, uint256 indexed amount); event Claim(address indexed receiver, uint256 indexed amount); event DependenciesUpdated( address indexed token, address indexed manager, address indexed account, address registry ); IERC20Upgradeable internal c_stCeloToken; IManager internal c_stCeloManager; IAccount internal c_stCeloAccount; IRegistry internal c_celoRegistry; struct WithdrawalInfo { uint256 value; uint256 timestamp; } mapping(address => WithdrawalInfo) internal withdrawals; function initialize( address _stCeloTokenAddress, address _stCeloManagerAddress, address _stCeloAccountAddress, address _celoRegistryAddress ) external initializer { __Ownable_init(); __Pausable_init(); __ReentrancyGuard_init(); // Ensures that `_owner` is set. setDependencies( _stCeloTokenAddress, _stCeloManagerAddress, _stCeloAccountAddress, _celoRegistryAddress ); // Ensures that `_stCeloTokenAddress` has been sanitized. __ERC20_init("Spirals Celo Vault Token", "spCELO"); __ERC4626_init(IERC20MetadataUpgradeable(address(getGoldToken()))); __StakedERC4626_init(IERC20MetadataUpgradeable(_stCeloTokenAddress)); } receive() external payable { emit Receive(msg.sender, msg.value); } /** * @notice Sets dependencies on contract (stCELO contract addresses). */ function setDependencies( address _stCeloTokenAddress, address _stCeloManagerAddress, address _stCeloAccountAddress, address _celoRegistryAddress ) public onlyOwner { require( IManaged(_stCeloTokenAddress).manager() == _stCeloManagerAddress, "NON_MATCHING_STCELO_MANAGER" ); require( IManaged(_stCeloAccountAddress).manager() == _stCeloManagerAddress, "NON_MATCHING_ACCOUNT_MANAGER" ); require( IRegistry(_celoRegistryAddress).getAddressForString("Validators") != address(0), "INVALID_REGISTRY_ADDRESS" ); c_stCeloToken = IERC20Upgradeable(_stCeloTokenAddress); c_stCeloManager = IManager(_stCeloManagerAddress); c_stCeloAccount = IAccount(_stCeloAccountAddress); c_celoRegistry = IRegistry(_celoRegistryAddress); emit DependenciesUpdated( _stCeloTokenAddress, _stCeloManagerAddress, _stCeloAccountAddress, _celoRegistryAddress ); } /** * @dev Deposit CELO into stCELO Manager. */ function stake(uint256 assets) internal virtual override { // TODO-slither might be no-op, but slither doesn't like this // https://github.com/crytic/slither/wiki/Detector-Documentation#functions-that-send-ether-to-arbitrary-destinations c_stCeloManager.deposit{value: assets}(); } function _checkCanWithdraw( address, address, address receiver, uint256, uint256 ) internal virtual override { if (hasOutstandingWithdrawal(receiver)) { WithdrawalInfo memory withdrawalInfo = withdrawals[receiver]; revert OutstandingWithdrawal( withdrawalInfo.value, withdrawalInfo.timestamp ); } } /** * @dev Initiates CELO unstaking on stCELO contracts. Note that CELO * will not be in vault immediately after (3 day lockup period) but is * expect to land in vault automatically. */ function _withdrawFromStakedAsset(uint256 stakedAssets) internal virtual override returns (uint256 assets) { c_stCeloManager.withdraw(stakedAssets); return c_stCeloManager.toCelo(stakedAssets); } /** * @dev Can't immediately send unlocked CELO to whoever is trying to withdraw * from this vault. */ function _sendAssetToReceiver( address, address receiver, uint256 assets ) internal virtual override { withdrawals[receiver].value = assets; withdrawals[receiver].timestamp = block.timestamp + getLockedGold().unlockingPeriod(); } /** * @notice Allows user to claim CELO withdrawan after unlocking period * into an arbitrary address. */ function claim() external virtual whenNotPaused nonReentrant { if (!hasWithdrawalReady(msg.sender)) { revert NoWithdrawalReady(withdrawals[msg.sender].timestamp); } uint256 celoToWithdraw = withdrawals[msg.sender].value; withdrawals[msg.sender].value = 0; // do this before transfer to protect against re-entrancy SafeERC20Upgradeable.safeTransfer( getGoldToken(), msg.sender, celoToWithdraw ); withdrawals[msg.sender].timestamp = 0; emit Claim(msg.sender, celoToWithdraw); } /** * @dev Returns true if current user's pending withdrawal is ready. */ function hasWithdrawalReady(address _address) public view returns (bool) { uint256 ts = withdrawals[_address].timestamp; // TODO-slither https://github.com/crytic/slither/wiki/Detector-Documentation#block-timestamp // Could use block.number, but less precise 5s block time is an estimate return ts != 0 && block.timestamp >= ts; } /** * @dev Returns true if the current user has an oustanding withdrawal. */ function hasOutstandingWithdrawal(address _address) public view returns (bool) { return withdrawals[_address].timestamp != 0; } /** * @dev CELO -> spCELO */ function _convertToShares(uint256 assets, MathUpgradeable.Rounding) internal pure override returns (uint256 shares) { return assets; } /** * @dev spCELO -> CELO */ function _convertToAssets(uint256 shares, MathUpgradeable.Rounding) internal pure override returns (uint256 assets) { return shares; } /** * @dev stCELO -> CELO */ function convertStakedToAssets(uint256 stakedAssets) public view virtual override returns (uint256) { return c_stCeloManager.toCelo(stakedAssets); } /** * @dev CELO -> stCELO */ function convertAssetsToStaked(uint256 assets) public view virtual override returns (uint256) { return c_stCeloManager.toStakedCelo(assets); } /// @dev Returns a LockedGold.sol interface for interacting with the smart contract. function getLockedGold() internal view returns (ILockedGold) { address lockedGoldAddr = IRegistry(c_celoRegistry) .getAddressForStringOrDie("LockedGold"); return ILockedGold(lockedGoldAddr); } /// @dev Returns a GoldToken.sol interface for interacting with the smart contract. function getGoldToken() public view returns (IERC20Upgradeable) { address goldTokenAddr = IRegistry(c_celoRegistry) .getAddressForStringOrDie("GoldToken"); return IERC20Upgradeable(goldTokenAddr); } /// @dev Returns a GoldToken.sol interface for interacting with the smart contract. function getGoldTokenV2() public view returns (address) { address goldTokenAddr = IRegistry(c_celoRegistry) .getAddressForStringOrDie("GoldToken"); return goldTokenAddr; } /// @dev Returns a GoldToken.sol interface for interacting with the smart contract. function getGoldTokenV3() public view returns (IERC20) { address goldTokenAddr = IRegistry(c_celoRegistry) .getAddressForStringOrDie("GoldToken"); return IERC20(goldTokenAddr); } }