diff --git a/nest/src/AggregateToken.sol b/nest/src/AggregateToken.sol new file mode 100644 index 0000000..7a6a156 --- /dev/null +++ b/nest/src/AggregateToken.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { IComponentToken } from "./interfaces/IComponentToken.sol"; +import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol"; + +/** + * @title AggregateToken + * @dev ERC20 token that represents a basket of other ERC20 tokens + * Invariant: the total value of all AggregateTokens minted is equal to the total value of all of its component tokens + */ +contract AggregateToken is IComponentToken, ERC20Upgradeable { + IERC20 public currencyToken; + IComponentToken[] public componentTokens; + string public tokenURI; + + // Base at which we do calculations in order to minimize rounding differences + uint256 _BASE = 10 ** 18; + + // Price at which the vault manager is willing to sell the aggregate token, times the base + uint256 askPrice; + /* Price at which the vault manager is willing to buy back the aggregate token, times the base + * This is always smaller than the ask price, so if the vault manager never changes either price, + * then they will always be able to buy back all outstanding AggregateTokens at a profit + */ + uint256 bidPrice; + + uint8 private _currencyDecimals; + uint8 private _decimals; + + // Events + + /** + * @dev Emitted when a user buys aggregateToken using currencyToken + * @param user Address of the user who buys the aggregateToken + * @param currencyTokenAmount Amount of currencyToken paid + * @param aggregateTokenAmount Amount of aggregateToken bought + */ + event Buy(address indexed user, uint256 currencyTokenAmount, uint256 aggregateTokenAmount); + + /** + * @dev Emitted when a user sells aggregateToken for currencyToken + * @param user Address of the user who sells the aggregateToken + * @param currencyTokenAmount Amount of currencyToken received + * @param aggregateTokenAmount Amount of aggregateToken sold + */ + event Sell(address indexed user, uint256 currencyTokenAmount, uint256 aggregateTokenAmount); + + function initialize( + string memory name, + string memory symbol, + uint8 __decimals, + string memory _tokenURI, + address _currencyToken, + address[] _componentTokens, + uint256 _askPrice, + uint256 _bidPrice + ) public initializer { + ERC20__init(name, symbol); + _decimals = __decimals; + tokenURI = _tokenURI; + currencyToken = IERC20(_currencyToken); + _currencyDecimals = currencyToken.decimals(); + componentTokens = _componentTokens; // TODO initialize the array + askPrice = _askPrice; + bidPrice = _bidPrice; + } + + // Override Functions + + /** + * @notice Returns the number of decimals of the aggregateToken + */ + function decimals() public view override returns (uint8) { + return _decimals; + } + + // User Functions + + /** + * @notice Buy the aggregateToken using currencyToken + * @dev The user must approve the contract to spend the currencyToken + * @param currencyTokenAmount Amount of currencyToken to pay for the aggregateToken + */ + function buy( + uint256 currencyTokenAmount + ) public { + // TODO: figure decimals math + uint256 aggregateTokenAmount = currencyTokenAmount * _BASE / askPrice; + + require(currencyToken.transferFrom(msg.sender, address(this), currencyTokenAmount), "AggregateToken: failed to transfer currencyToken"); + _mint(msg.sender, aggregateTokenAmount); + + emit Buy(msg.sender, currencyTokenAmount, aggregateTokenAmount); + } + + /** + * @notice Sell the aggregateToken to receive currencyToken + * @param currencyTokenAmount Amount of currencyToken to receive for the aggregateToken + */ + function sell( + uint256 currencyTokenAmount + ) public { + // TODO: figure decimals math + uint256 aggregateTokenAmount = currencyTokenAmount * _BASE / bidPrice; + + require(currencyToken.transfer(msg.sender, currencyTokenAmount), "AggregateToken: failed to transfer currencyToken"); + _burn(msg.sender, aggregateTokenAmount); + + emit Sell(msg.sender, currencyTokenAmount, aggregateTokenAmount); + } + + /** + * @notice + */ + function claim(uint256 amount) public { + // TODO - rebasing vs. streaming + } + + function claimAll() public { + uint256 amount = claimableAmount(msg.sender); + claim(amount); + } + + // Admin Functions + + function buyComponentToken(address token, uint256 amount) public onlyOwner { + // TODO verify it's allowed + IComponentToken(token).buy(amount); + } + + function sellComponentToken(address token, uint256 amount) public onlyOwner { + IComponentToken(token).sell(amount); + } + + // Admin Setter Functions + + function setTokenURI(string memory uri) public onlyOwner { + tokenURI = uri; + } + + function addAllowedComponentToken( + address token, + ) public onlyOwner { + componentTokens.push(IComponentToken(token)); + } + + function removeAllowedComponentToken( + address token, + ) public onlyOwner { + componentTokens.push(IComponentToken(token)); + } + + function setAskPrice(uint256 price) public onlyOwner { + askPrice = price; + } + + function setBidPrice(uint256 price) public onlyOwner { + bidPrice = price; + } + + // View Functions + + function claimableAmount(address user) public view returns (uint256 amount) { + amount = 0; + } + + function allowedComponentTokens() public view returns (address[] memory) { + + } +} \ No newline at end of file diff --git a/nest/src/Counter.sol b/nest/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/nest/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/nest/src/FakeComponentToken.sol b/nest/src/FakeComponentToken.sol new file mode 100644 index 0000000..f1ed312 --- /dev/null +++ b/nest/src/FakeComponentToken.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { IComponentToken } from "./interfaces/IComponentToken.sol"; +import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol"; + +/** + * @title ComponentTokenExample + * @dev ERC20 token that represents a component of an AggregateToken + * Invariant: the total value of all ComponentTokens minted is equal to the total value of all of its component tokens + */ +contract ComponentToken is IComponentToken, ERC20Upgradeable { + IERC20 public currencyToken; + + // Base at which we do calculations in order to minimize rounding differences + uint256 _BASE = 10 ** 18; + + // Price at which the vault manager is willing to sell the aggregate token, times the base + uint256 askPrice; + /* Price at which the vault manager is willing to buy back the aggregate token, times the base + * This is always smaller than the ask price, so if the vault manager never changes either price, + * then they will always be able to buy back all outstanding ComponentTokens at a profit + */ + uint256 bidPrice; + + uint8 private _currencyDecimals; + uint8 private _decimals; + + // Events + + /** + * @dev Emitted when a user stakes currencyToken to receive aggregateToken in return + * @param user Address of the user who staked the currencyToken + * @param currencyTokenAmount Amount of currencyToken staked + * @param aggregateTokenAmount Amount of aggregateToken received + */ + event Buy(address indexed user, uint256 currencyTokenAmount, uint256 aggregateTokenAmount); + + /** + * @dev Emitted when a user unstakes aggregateToken to receive currencyToken in return + * @param user Address of the user who unstaked the aggregateToken + * @param currencyTokenAmount Amount of currencyToken received + * @param aggregateTokenAmount Amount of aggregateToken unstaked + */ + event Sell(address indexed user, uint256 currencyTokenAmount, uint256 aggregateTokenAmount); + + function initialize( + string memory name, + string memory symbol, + uint8 __decimals, + string memory _tokenURI, + address _currencyToken, + uint256 _askPrice, + uint256 _bidPrice + ) public initializer { + tokenURI = _tokenURI; + + currencyToken = IERC20(_currencyToken); + _currencyDecimals = currencyToken.decimals(); + askPrice = _askPrice; + bidPrice = _bidPrice; + _decimals = __decimals; + } + + // Override Functions + + /** + * @notice Returns the number of decimals of the aggregateToken + */ + function decimals() public view override returns (uint8) { + return _decimals; + } + + // User Functions + + /** + * @notice Stake the currencyToken to receive aggregateToken in return + * @dev The user must approve the contract to spend the currencyToken + * @param currencyTokenAmount Amount of currencyToken to stake + */ + function buy( + address currencyToken, + uint256 currencyTokenAmount + ) public { + /* + // TODO: figure decimals math + uint256 aggregateTokenAmount = currencyTokenAmount * _BASE / askPrice; + + require(currencyToken.transferFrom(msg.sender, address(this), currencyTokenAmount), "AggregateToken: failed to transfer currencyToken"); + _mint(msg.sender, aggregateTokenAmount); + + emit Staked(msg.sender, currencyTokenAmount, aggregateTokenAmount); + */ + + DEX.swap(p, address(this)); + + } + + /** + * @notice Unstake the aggregateToken to receive currencyToken in return + * @param currencyTokenAmount Amount of currencyToken to receive + */ + function ( + address currencyToken, + uint256 currencyTokenAmount + ) public { + // TODO: figure decimals math + uint256 aggregateTokenAmount = currencyTokenAmount * _BASE / bidPrice; + + require(currencyToken.transfer(msg.sender, currencyTokenAmount), "AggregateToken: failed to transfer currencyToken"); + _burn(msg.sender, aggregateTokenAmount); + + emit Unstaked(msg.sender, currencyTokenAmount, aggregateTokenAmount); + } + + /** + * @notice + */ + function claim(uint256 amount) public { + // TODO - rebasing vs. streaming + } + + function claimAll() public { + uint256 amount = claimableAmount(msg.sender); + claim(amount); + } + + // Admin Setter Functions + + function setAskPrice(uint256 price) public onlyOwner { + askPrice = price; + } + + function setBidPrice(uint256 price) public onlyOwner { + bidPrice = price; + } +} \ No newline at end of file diff --git a/nest/src/NestStaking.sol b/nest/src/NestStaking.sol new file mode 100644 index 0000000..e8e8a2f --- /dev/null +++ b/nest/src/NestStaking.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +contract NestStaking { + + function createAggregateToken( + string memory name, + string memory symbol, + uint8 decimals, + ) public returns (address) { + return address(0); + } +} diff --git a/nest/src/interfaces/IAggregateToken.sol b/nest/src/interfaces/IAggregateToken.sol new file mode 100644 index 0000000..c3c90a3 --- /dev/null +++ b/nest/src/interfaces/IAggregateToken.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +interface IComponentToken { + function setTokenURI(string memory uri) external; +} \ No newline at end of file diff --git a/nest/src/interfaces/IComponentToken.sol b/nest/src/interfaces/IComponentToken.sol new file mode 100644 index 0000000..49d2e2d --- /dev/null +++ b/nest/src/interfaces/IComponentToken.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +interface IComponentToken is IERC20 { + function buy(uint256 amount) external; + function sell(uint256 amount) external; + function claim(uint256 amount) external; + function claimAll() external; +} \ No newline at end of file