// 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 {IAccount} from "lib/staked-celo/contracts/interfaces/IAccount.sol"; import {IManager} from "./IManager.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 "lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; import "lib/openzeppelin-contracts-upgradeable/contracts/security/PausableUpgradeable.sol"; import "lib/openzeppelin-contracts-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; import {MathUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/utils/math/MathUpgradeable.sol"; /// @title SpiralsStCeloVault /// @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 SpiralsStCeloVault is ERC4626Upgradeable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable { event CeloDeposit(address indexed sender, uint256 indexed amount); IERC20Upgradeable internal c_stCeloToken; IManager internal c_stCeloManager; IAccount internal c_stCeloAccount; function initialize( address _stCeloTokenAddress, address _stCeloManagerAddress, address _stCeloAccountAddress ) public initializer { __ERC4626_init(IERC20MetadataUpgradeable(_stCeloTokenAddress)); __ERC20_init("Spirals Celo Vault Token", "spCELO"); __Ownable_init(); __Pausable_init(); __ReentrancyGuard_init(); setDependencies( _stCeloTokenAddress, _stCeloManagerAddress, _stCeloAccountAddress ); } receive() external payable { emit CeloDeposit(msg.sender, msg.value); } function setDependencies( address _stCeloTokenAddress, address _stCeloManagerAddress, address _stCeloAccountAddress ) public onlyOwner { c_stCeloToken = IERC20Upgradeable(_stCeloTokenAddress); require(c_stCeloToken.totalSupply() > 0, "INVALID_TOKEN_ADDRESS"); c_stCeloManager = IManager(_stCeloManagerAddress); require(c_stCeloManager.toCelo(1) >= 0, "INVALID_MANAGER_ADDRESS"); c_stCeloAccount = IAccount(_stCeloAccountAddress); require(c_stCeloAccount.getTotalCelo() >= 0, "INVALID_ACCOUNT_ADDRESS"); } function stake() public payable virtual { // Convert CELO to stCELO on contract. c_stCeloManager.deposit{value: msg.value}(); // Mint spCELO shares. _deposit( address(this), msg.sender, msg.value, previewDeposit(msg.value) ); } function unstake(uint256 _spCeloAmount) public virtual { uint256 stCeloAmount = _convertToAssets( _spCeloAmount, MathUpgradeable.Rounding.Down ); _burn(msg.sender, _spCeloAmount); c_stCeloManager.withdraw(stCeloAmount); // arrive after 3 days // TODO: can we automate this? } /** * @dev Deposit/mint common workflow. Slighly modified to handle edge * case where caller is the contract itself (avoid ERC20 transfer). */ function _deposit( address caller, address receiver, uint256 assets, uint256 shares ) internal override { // If _asset is ERC777, `transferFrom` can trigger a reenterancy BEFORE the transfer happens through the // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer, // calls the vault, which is assumed not malicious. // // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the // assets are transfered and before the shares are minted, which is a valid state. // slither-disable-next-line reentrancy-no-eth if (caller != address(this)) { // Skip if _from and _to are the same. SafeERC20Upgradeable.safeTransferFrom( c_stCeloToken, caller, address(this), assets ); } _mint(receiver, shares); emit Deposit(caller, receiver, assets, shares); } function _convertToShares(uint256 assets, MathUpgradeable.Rounding) internal view override returns (uint256 shares) { return c_stCeloManager.toCelo(assets); } function _convertToAssets(uint256 shares, MathUpgradeable.Rounding) internal view override returns (uint256 assets) { return c_stCeloManager.toStakedCelo(shares); } }