diff --git a/src/interfaces/IAssetPool.sol b/src/interfaces/IAssetPool.sol index 65febef..dd4b309 100644 --- a/src/interfaces/IAssetPool.sol +++ b/src/interfaces/IAssetPool.sol @@ -1,73 +1,56 @@ // SPDX-License-Identifier: AGPL-3.0 +// author: bhargavaparoksham pragma solidity ^0.8.20; -import {IAssetPoolAddressesProvider} from './IAssetPoolAddressesProvider.sol'; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {IXToken} from "./IXToken.sol"; +import {ILPRegistry} from "./ILPRegistry.sol"; interface IAssetPool { - /** - * @dev Emitted on deposit() - * @param user The address initiating the deposit - * @param onBehalfOf The beneficiary of the deposit, receiving the aTokens - * @param amount The amount deposited - **/ - event Deposit( - address user, - address indexed onBehalfOf, - uint256 amount - ); - - /** - * @dev Emitted on withdraw() - * @param user The address initiating the withdrawal, owner of xTokens - * @param to Address that will receive the underlying - * @param amount The amount to be withdrawn - **/ - event Withdraw(address indexed user, address indexed to, uint256 amount); - - - /** - * @dev Emitted when the pause is triggered. - */ - event Paused(); - - /** - * @dev Emitted when the pause is lifted. - */ - event Unpaused(); - - /** - * @dev Deposits an `amount` of stable asset, receiving in return overlying xTokens. - * - E.g. User deposits 100 USDC and gets in return 100 xAsset - * @param amount The amount to be deposited - * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user - * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens - * is a different wallet - **/ - function deposit( - uint256 amount, - address onBehalfOf - ) external; - - /** - * @dev Withdraws an `amount` of stable asset, burning the equivalent xTokens owned - * E.g. User has 100 xAsset, calls withdraw() and receives 100 USDC, burning the 100 xAsset - * @param amount The underlying amount to be withdrawn - * - Send the value type(uint256).max in order to withdraw the whole xToken balance - * @param to Address that will receive the underlying, same as msg.sender if the user - * wants to receive it on his own wallet, or a different address if the beneficiary is a - * different wallet - * @return The final amount withdrawn - **/ - - function withdraw( - uint256 amount, - address to - ) external returns (uint256); - - function getAddressesProvider() external view returns (IAssetPoolAddressesProvider); - - function setPause(bool val) external; - - function paused() external view returns (bool); + enum CycleState { + REBALANCING, // LPs can rebalance + IN_CYCLE // Normal cycle operation + } + + event DepositReceived(address indexed user, uint256 amount, uint256 cycleNumber); + event WithdrawalRequested(address indexed user, uint256 xTokenAmount, uint256 cycleNumber); + event XTokensClaimed(address indexed user, uint256 amount, uint256 cycleNumber); + event DepositTokensClaimed(address indexed user, uint256 depositTokenAmount, uint256 cycleNumber); + event CycleStarted(uint256 cycleNumber, uint256 timestamp); + event CycleStateUpdated(CycleState newState); + event Rebalanced(address indexed lp, uint256 lpAdded, uint256 lpWithdrawn); + event RebalancingCompleted(uint256 cycleNumber); + + error InvalidState(); + error NotLP(); + error InvalidAmount(); + error InsufficientBalance(); + error NotInRebalancingPeriod(); + error RebalancingAlreadyDone(); + error NothingToClaim(); + error ZeroAddress(); + + function deposit(uint256 amount) external; + function requestWithdrawal(uint256 xTokenAmount) external; + function claimXTokens() external; + function claimDepositTokens() external; + function rebalance(uint256 lpAdded, uint256 lpWithdrawn) external; + function checkAndStartNewCycle() external; + + function assetToken() external view returns (IXToken); + function depositToken() external view returns (IERC20); + function lpRegistry() external view returns (ILPRegistry); + function assetSymbol() external view returns (string memory); + function cycleLength() external view returns (uint256); + function rebalancingPeriod() external view returns (uint256); + function currentCycleStart() external view returns (uint256); + function currentCycleNumber() external view returns (uint256); + function currentState() external view returns (CycleState); + function unclaimedDeposits(address user) external view returns (uint256); + function unclaimedWithdrawals(address user) external view returns (uint256); + function lastDepositCycle(address user) external view returns (uint256); + function lastWithdrawalCycle(address user) external view returns (uint256); + function cycleRebalanced(address lp) external view returns (bool); + function rebalancedLPCount() external view returns (uint256); } \ No newline at end of file diff --git a/src/interfaces/IAssetPoolAddressesProvider.sol b/src/interfaces/IAssetPoolAddressesProvider.sol deleted file mode 100644 index 14d41c6..0000000 --- a/src/interfaces/IAssetPoolAddressesProvider.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 - -pragma solidity ^0.8.20; - -/** - * @title IAssetPoolAddressesProvider contract - * @dev Main registry of addresses part of or connected to the protocol, including permissioned roles - * - Acting also as factory of proxies and admin of those, so with right to change its implementations - * - Owned by the Own Governance - * @author Own Protocol - **/ -interface IAssetPoolAddressesProvider { - event MarketIdSet(string newMarketId); - event AssetPoolUpdated(address indexed newAddress); - event ConfigurationAdminUpdated(address indexed newAddress); - event EmergencyAdminUpdated(address indexed newAddress); - event AssetPoolConfiguratorUpdated(address indexed newAddress); - event AssetPoolCollateralManagerUpdated(address indexed newAddress); - event PriceOracleUpdated(address indexed newAddress); - event AssetRateOracleUpdated(address indexed newAddress); - event ProxyCreated(bytes32 id, address indexed newAddress); - event AddressSet(bytes32 id, address indexed newAddress, bool hasProxy); - - function getMarketId() external view returns (string memory); - - function setMarketId(string calldata marketId) external; - - function setAddress(bytes32 id, address newAddress) external; - - function setAddressAsProxy(bytes32 id, address impl) external; - - function getAddress(bytes32 id) external view returns (address); - - function getAssetPool() external view returns (address); - - function setAssetPoolImpl(address pool) external; - - function getAssetPoolConfigurator() external view returns (address); - - function setAssetPoolConfiguratorImpl(address configurator) external; - - function getAssetPoolCollateralManager() external view returns (address); - - function setAssetPoolCollateralManager(address manager) external; - - function getPoolAdmin() external view returns (address); - - function setPoolAdmin(address admin) external; - - function getEmergencyAdmin() external view returns (address); - - function setEmergencyAdmin(address admin) external; - - function getPriceOracle() external view returns (address); - - function setPriceOracle(address priceOracle) external; - - function getAssetRateOracle() external view returns (address); - - function setAssetRateOracle(address AssetRateOracle) external; -} \ No newline at end of file diff --git a/src/protocol/AssetPool.sol b/src/protocol/AssetPool.sol index ae7df93..817924b 100644 --- a/src/protocol/AssetPool.sol +++ b/src/protocol/AssetPool.sol @@ -3,95 +3,153 @@ pragma solidity ^0.8.20; -import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "openzeppelin-contracts/contracts/access/Ownable.sol"; -import "./xToken.sol"; -import "./AssetOracle.sol"; - - -contract AssetPool is Ownable { - xToken public tokenX; - IERC20 public tokenY; - AssetOracle public oracle; - - uint256 public totalDeposits; // Total USDC deposited - uint256 public totalScaledDeposits; // Total scaled USDC equivalent - - event Deposited(address indexed user, uint256 amount, uint256 xTokenMinted); - event Withdrawn(address indexed user, uint256 amount, uint256 xTokenBurned); - event Rebalanced(uint256 lpAdded, uint256 lpWithdrawn); - - mapping(address => uint256) public scaledBalances; +import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {IAssetPool} from "../interfaces/IAssetPool.sol"; +import {IXToken} from "../interfaces/IXToken.sol"; +import {ILPRegistry} from "../interfaces/ILPRegistry.sol"; +import {xToken} from "./xToken.sol"; + +contract AssetPool is IAssetPool, Ownable { + IXToken public assetToken; + IERC20 public depositToken; + ILPRegistry public immutable lpRegistry; + string public assetSymbol; + + uint256 public immutable cycleLength; + uint256 public immutable rebalancingPeriod; + uint256 public currentCycleStart; + uint256 public currentCycleNumber; + CycleState public currentState; + + mapping(address => uint256) public unclaimedDeposits; + mapping(address => uint256) public unclaimedWithdrawals; + mapping(address => uint256) public lastDepositCycle; + mapping(address => uint256) public lastWithdrawalCycle; + mapping(address => bool) public cycleRebalanced; + uint256 public rebalancedLPCount; constructor( - address _tokenY, + string memory _assetSymbol, + string memory _assetTokenName, + string memory _assetTokenSymbol, + address _depositToken, address _oracle, - string memory _xtokenName, - string memory _xtokenSymbol - ) Ownable(msg.sender) { - tokenY = IERC20(_tokenY); - oracle = AssetOracle(_oracle); - tokenX = new xToken(_xtokenName, _xtokenSymbol, address(oracle)); - + uint256 _cycleLength, + uint256 _rebalancingPeriod, + address _owner, + address _lpRegistry + ) Ownable(_owner) { + if (_depositToken == address(0) || _oracle == address(0) || + _lpRegistry == address(0) || _owner == address(0)) revert ZeroAddress(); + + assetSymbol = _assetSymbol; + depositToken = IERC20(_depositToken); + assetToken = new xToken(_assetTokenName, _assetTokenSymbol, _oracle); + cycleLength = _cycleLength; + rebalancingPeriod = _rebalancingPeriod; + currentState = CycleState.IN_CYCLE; + lpRegistry = ILPRegistry(_lpRegistry); } - function _getScaledBalance(uint256 amount, uint256 price) internal pure returns (uint256) { - // Price is in cents, so divide by 100 to convert to a scaled factor - return (amount * 1e18) / (price * 1e16); + modifier onlyLP() { + if (!lpRegistry.isLP(address(this), msg.sender)) revert NotLP(); + _; } function deposit(uint256 amount) external { - require(amount > 0, "Amount must be greater than zero"); - uint256 price = oracle.assetPrice(); - require(price > 0, "Invalid asset price"); - - tokenY.transferFrom(msg.sender, address(this), amount); - - uint256 scaledAmount = _getScaledBalance(amount, price); - totalDeposits += amount; - totalScaledDeposits += scaledAmount; - - scaledBalances[msg.sender] += scaledAmount; - tokenX.mint(msg.sender, scaledAmount); - - emit Deposited(msg.sender, amount, scaledAmount); + if (amount == 0) revert InvalidAmount(); + + depositToken.transferFrom(msg.sender, address(this), amount); + unclaimedDeposits[msg.sender] += amount; + lastDepositCycle[msg.sender] = currentCycleNumber; + + emit DepositReceived(msg.sender, amount, currentCycleNumber); } - function withdraw(uint256 xtokenAmount) external { - require(xtokenAmount > 0, "Amount must be greater than zero"); - require(tokenX.balanceOf(msg.sender) >= xtokenAmount, "Insufficient xToken balance"); - - uint256 price = oracle.assetPrice(); - require(price > 0, "Invalid asset price"); - - uint256 usdcAmount = (xtokenAmount * (price * 1e16)) / 1e18; - require(usdcAmount <= tokenY.balanceOf(address(this)), "Insufficient USDC liquidity"); - - tokenX.burn(msg.sender, xtokenAmount); - scaledBalances[msg.sender] -= xtokenAmount; - totalDeposits -= usdcAmount; - totalScaledDeposits -= xtokenAmount; + function requestWithdrawal(uint256 xTokenAmount) external { + if (xTokenAmount == 0) revert InvalidAmount(); + if (assetToken.balanceOf(msg.sender) < xTokenAmount) revert InsufficientBalance(); + + assetToken.transferFrom(msg.sender, address(this), xTokenAmount); + unclaimedWithdrawals[msg.sender] += xTokenAmount; + lastWithdrawalCycle[msg.sender] = currentCycleNumber; + + emit WithdrawalRequested(msg.sender, xTokenAmount, currentCycleNumber); + } - tokenY.transfer(msg.sender, usdcAmount); + function claimXTokens() external { + uint256 depositCycle = lastDepositCycle[msg.sender]; + if (depositCycle == 0 || depositCycle >= currentCycleNumber) revert NothingToClaim(); + if (currentState != CycleState.IN_CYCLE) revert InvalidState(); + + uint256 amount = unclaimedDeposits[msg.sender]; + if (amount == 0) revert NothingToClaim(); + + unclaimedDeposits[msg.sender] = 0; + assetToken.mint(msg.sender, amount); + + emit XTokensClaimed(msg.sender, amount, currentCycleNumber); + } - emit Withdrawn(msg.sender, usdcAmount, xtokenAmount); + function claimDepositTokens() external { + uint256 withdrawalCycle = lastWithdrawalCycle[msg.sender]; + if (withdrawalCycle == 0 || withdrawalCycle >= currentCycleNumber) revert NothingToClaim(); + if (currentState != CycleState.IN_CYCLE) revert InvalidState(); + + uint256 xTokenAmount = unclaimedWithdrawals[msg.sender]; + if (xTokenAmount == 0) revert NothingToClaim(); + + uint256 depositTokenAmount = calculateDepositTokenAmount(xTokenAmount); + + unclaimedWithdrawals[msg.sender] = 0; + assetToken.burn(address(this), xTokenAmount); + depositToken.transfer(msg.sender, depositTokenAmount); + + emit DepositTokensClaimed(msg.sender, depositTokenAmount, currentCycleNumber); } - function rebalance(uint256 lpAdded, uint256 lpWithdrawn) external onlyOwner { - require(lpAdded > 0 || lpWithdrawn > 0, "Invalid rebalance amounts"); + function rebalance(uint256 lpAdded, uint256 lpWithdrawn) external onlyLP { + if (currentState != CycleState.REBALANCING) revert NotInRebalancingPeriod(); + if (cycleRebalanced[msg.sender]) revert RebalancingAlreadyDone(); + if (block.timestamp > currentCycleStart + rebalancingPeriod) revert NotInRebalancingPeriod(); if (lpAdded > 0) { - tokenY.transferFrom(msg.sender, address(this), lpAdded); - totalDeposits += lpAdded; + depositToken.transferFrom(msg.sender, address(this), lpAdded); } - if (lpWithdrawn > 0) { - require(lpWithdrawn <= tokenY.balanceOf(address(this)), "Insufficient USDC liquidity"); - tokenY.transfer(msg.sender, lpWithdrawn); - totalDeposits -= lpWithdrawn; + depositToken.transfer(msg.sender, lpWithdrawn); } - emit Rebalanced(lpAdded, lpWithdrawn); + cycleRebalanced[msg.sender] = true; + rebalancedLPCount++; + + emit Rebalanced(msg.sender, lpAdded, lpWithdrawn); + + if (rebalancedLPCount == lpRegistry.getLPCount(address(this))) { + currentState = CycleState.IN_CYCLE; + rebalancedLPCount = 0; + emit CycleStateUpdated(CycleState.IN_CYCLE); + emit RebalancingCompleted(currentCycleNumber); + } } + function checkAndStartNewCycle() external { + if (currentState == CycleState.IN_CYCLE && + block.timestamp >= currentCycleStart + cycleLength) { + + currentCycleNumber++; + currentCycleStart = block.timestamp; + currentState = CycleState.REBALANCING; + rebalancedLPCount = 0; + + emit CycleStarted(currentCycleNumber, block.timestamp); + emit CycleStateUpdated(CycleState.REBALANCING); + } } + + function calculateDepositTokenAmount(uint256 xTokenAmount) internal view returns (uint256) { + uint256 price = assetToken.oracle().assetPrice(); + return (xTokenAmount * price * 1e16) / 1e18; + } +} \ No newline at end of file