// 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 {Manager} from "lib/staked-celo/contracts/Manager.sol"; import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.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 { address internal c_stCeloToken = 0xC668583dcbDc9ae6FA3CE46462758188adfdfC24; address internal c_stCeloManager = 0x0239b96D10a434a56CC9E09383077A0490cF9398; address internal c_stCeloAccount = 0x4aAD04D41FD7fd495503731C5a2579e19054C432; function initialize() public initializer { __ERC4626_init(IERC20MetadataUpgradeable(c_stCeloToken)); __ERC20_init("Spirals Celo Vault Token", "spCELO"); __Ownable_init(); __Pausable_init(); __ReentrancyGuard_init(); } receive() external payable {} function stake() public payable virtual { // Convert CELO to stCELO on contract. Manager(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); Manager(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( IERC20Upgradeable(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 Manager(c_stCeloManager).toCelo(assets); } function _convertToAssets(uint256 shares, MathUpgradeable.Rounding) internal view override returns (uint256 assets) { return Manager(c_stCeloManager).toStakedCelo(shares); } }