From 794aeff73bc8639aaa7247b4334bd3f3f965a95c Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Thu, 12 Sep 2024 15:11:06 -0700 Subject: [PATCH 01/17] [NES-186] make AggregateToken.sol --- nest/src/AggregateToken.sol | 171 ++++++++++++++++++++++++ nest/src/Counter.sol | 14 -- nest/src/FakeComponentToken.sol | 137 +++++++++++++++++++ nest/src/NestStaking.sol | 13 ++ nest/src/interfaces/IAggregateToken.sol | 6 + nest/src/interfaces/IComponentToken.sol | 9 ++ 6 files changed, 336 insertions(+), 14 deletions(-) create mode 100644 nest/src/AggregateToken.sol delete mode 100644 nest/src/Counter.sol create mode 100644 nest/src/FakeComponentToken.sol create mode 100644 nest/src/NestStaking.sol create mode 100644 nest/src/interfaces/IAggregateToken.sol create mode 100644 nest/src/interfaces/IComponentToken.sol 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 From 29c57bd6764b953b1057d6b725c771fe11f8e01e Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 00:08:15 -0700 Subject: [PATCH 02/17] [ARC-204] set up CI for contracts --- .github/pull_request_template.md | 9 +++++++ .../workflows/continuous-integration.yml | 12 +++++++--- nest/foundry.toml | 22 +++++++++++++++++ nest/script/Counter.s.sol | 12 ---------- nest/src/Counter.sol | 14 ----------- nest/test/Counter.t.sol | 24 ------------------- 6 files changed, 40 insertions(+), 53 deletions(-) create mode 100644 .github/pull_request_template.md rename nest/.github/workflows/test.yml => .github/workflows/continuous-integration.yml (78%) delete mode 100644 nest/script/Counter.s.sol delete mode 100644 nest/src/Counter.sol delete mode 100644 nest/test/Counter.t.sol diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..e032b53 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,9 @@ +## What's new in this PR? + +In bullet point format, please describe what's new in this PR. + +## Why? + +What problem does this solve? +Why is this important? +What's the context? diff --git a/nest/.github/workflows/test.yml b/.github/workflows/continuous-integration.yml similarity index 78% rename from nest/.github/workflows/test.yml rename to .github/workflows/continuous-integration.yml index 9282e82..62dfad3 100644 --- a/nest/.github/workflows/test.yml +++ b/.github/workflows/continuous-integration.yml @@ -1,6 +1,12 @@ -name: test +name: Continuous Integration -on: workflow_dispatch +on: + push: + branches: + - main + pull_request: + branches: + - main env: FOUNDRY_PROFILE: ci @@ -10,7 +16,7 @@ jobs: strategy: fail-fast: true - name: Foundry project + name: Build and Test Contracts runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/nest/foundry.toml b/nest/foundry.toml index 25b918f..8e9095d 100644 --- a/nest/foundry.toml +++ b/nest/foundry.toml @@ -1,6 +1,28 @@ [profile.default] +solc = "0.8.25" +evm_version = "cancun" src = "src" out = "out" libs = ["lib"] +ffi = true +ast = true +build_info = true +extra_output = ["storageLayout"] + +[fmt] +single_line_statement_blocks = "multi" +multiline_func_header = "params_first" +sort_imports = true +contract_new_lines = true +bracket_spacing = true +int_types = "long" +quote_style = "double" +number_underscore = "thousands" +wrap_comments = true # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + +remappings = [ + "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/", + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", +] diff --git a/nest/script/Counter.s.sol b/nest/script/Counter.s.sol deleted file mode 100644 index df9ee8b..0000000 --- a/nest/script/Counter.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; - -contract CounterScript is Script { - function setUp() public {} - - function run() public { - vm.broadcast(); - } -} 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/test/Counter.t.sol b/nest/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/nest/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} From 8ce88382c8d8e18da5ab53c63dbd17a83172a71f Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 00:13:00 -0700 Subject: [PATCH 03/17] add template for creating new Foundry projects --- foundry-template/.gitignore | 14 ++++++++++++++ foundry-template/README.md | 9 +++++++++ foundry-template/foundry.toml | 28 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 foundry-template/.gitignore create mode 100644 foundry-template/README.md create mode 100644 foundry-template/foundry.toml diff --git a/foundry-template/.gitignore b/foundry-template/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/foundry-template/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/foundry-template/README.md b/foundry-template/README.md new file mode 100644 index 0000000..e38039d --- /dev/null +++ b/foundry-template/README.md @@ -0,0 +1,9 @@ +## Foundry Template Setup + +Copy this folder to start a new Foundry project, then run the following to initialize the submodules: + +```bash +$ forge install foundry-rs/forge-std +$ forge install OpenZeppelin/openzeppelin-foundry-upgrades +$ forge install OpenZeppelin/openzeppelin-contracts-upgradeable +``` diff --git a/foundry-template/foundry.toml b/foundry-template/foundry.toml new file mode 100644 index 0000000..8e9095d --- /dev/null +++ b/foundry-template/foundry.toml @@ -0,0 +1,28 @@ +[profile.default] +solc = "0.8.25" +evm_version = "cancun" +src = "src" +out = "out" +libs = ["lib"] +ffi = true +ast = true +build_info = true +extra_output = ["storageLayout"] + +[fmt] +single_line_statement_blocks = "multi" +multiline_func_header = "params_first" +sort_imports = true +contract_new_lines = true +bracket_spacing = true +int_types = "long" +quote_style = "double" +number_underscore = "thousands" +wrap_comments = true + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + +remappings = [ + "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/", + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", +] From b077905bd70bca74c8928c4be31a5235285b26fc Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 00:16:46 -0700 Subject: [PATCH 04/17] [NES-188] make ComponentToken.sol --- nest/script/DeployNestContracts.s.sol | 28 ++++ nest/src/FakeComponentToken.sol | 204 ++++++++++++++++++++++++ nest/src/interfaces/IComponentToken.sol | 11 ++ 3 files changed, 243 insertions(+) create mode 100644 nest/script/DeployNestContracts.s.sol create mode 100644 nest/src/FakeComponentToken.sol create mode 100644 nest/src/interfaces/IComponentToken.sol diff --git a/nest/script/DeployNestContracts.s.sol b/nest/script/DeployNestContracts.s.sol new file mode 100644 index 0000000..93e2162 --- /dev/null +++ b/nest/script/DeployNestContracts.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import "forge-std/Script.sol"; + +import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; + +import { FakeComponentToken } from "../src/FakeComponentToken.sol"; + +contract DeployNestContracts is Script { + + address private constant ARC_ADMIN_ADDRESS = 0x1c9d94FAD4ccCd522804a955103899e0D6A4405a; + address private constant USDC_ADDRESS = 0x849c25e6cCB03cdc23ba91d92440dA7bC8486be2; + + function run() external { + vm.startBroadcast(ARC_ADMIN_ADDRESS); + + address fakeComponentTokenProxy = Upgrades.deployUUPSProxy( + "FakeComponentToken.sol", + abi.encodeCall(FakeComponentToken.initialize, (msg.sender, "Banana", "BAN", IERC20(USDC_ADDRESS), 18)) + ); + console.log("FakeComponentToken deployed to:", fakeComponentTokenProxy); + + vm.stopBroadcast(); + } + +} diff --git a/nest/src/FakeComponentToken.sol b/nest/src/FakeComponentToken.sol new file mode 100644 index 0000000..e698951 --- /dev/null +++ b/nest/src/FakeComponentToken.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { AccessControlUpgradeable } from "openzeppelin-contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { Initializable } from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { ERC20Upgradeable } from "openzeppelin-contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol"; + +import { IComponentToken } from "./interfaces/IComponentToken.sol"; + +/** + * @title FakeComponentToken + * @author Eugene Y. Q. Shen + * @notice Fake example of a ComponentToken that could be used in an AggregateToken when testing. + * Users can buy and sell one FakeComponentToken by exchanging it with one CurrencyToken at any time. + * @custom:oz-upgrades-from FakeComponentToken + */ +contract FakeComponentToken is Initializable, AccessControlUpgradeable, UUPSUpgradeable, ERC20Upgradeable { + + // Storage + + /// @custom:storage-location erc7201:plume.storage.FakeComponentToken + struct FakeComponentTokenStorage { + /// @dev CurrencyToken used to mint and burn the FakeComponentToken + IERC20 currencyToken; + /// @dev Number of decimals of the FakeComponentToken + uint8 decimals; + } + + // keccak256(abi.encode(uint256(keccak256("plume.storage.FakeComponentToken")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant FAKE_COMPONENT_TOKEN_STORAGE_LOCATION = + 0x2c4e9dd7fc35b7006b8a84e1ac11ecc9e53a0dd5c8824b364abab355c5037600; + + function _getFakeComponentTokenStorage() private pure returns (FakeComponentTokenStorage storage $) { + assembly { + $.slot := FAKE_COMPONENT_TOKEN_STORAGE_LOCATION + } + } + + // Constants + + /// @notice Role for the upgrader of the FakeComponentToken + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADE_ROLE"); + + // Base at which we do all calculations to minimize rounding losses + uint256 private constant _BASE = 1e18; + + // Events + + /** + * @notice Emitted when a user buys FakeComponentToken using CurrencyToken + * @param user Address of the user who bought the FakeComponentToken + * @param currencyToken CurrencyToken used to buy the FakeComponentToken + * @param currencyTokenAmount Amount of CurrencyToken paid + * @param componentTokenAmount Amount of FakeComponentToken received + */ + event ComponentTokenBought( + address indexed user, IERC20 indexed currencyToken, uint256 currencyTokenAmount, uint256 componentTokenAmount + ); + + /** + * @notice Emitted when a user sells FakeComponentToken to receive CurrencyToken + * @param user Address of the user who sold the FakeComponentToken + * @param currencyToken CurrencyToken received in exchange for the FakeComponentToken + * @param currencyTokenAmount Amount of CurrencyToken received + * @param componentTokenAmount Amount of FakeComponentToken sold + */ + event ComponentTokenSold( + address indexed user, IERC20 indexed currencyToken, uint256 currencyTokenAmount, uint256 componentTokenAmount + ); + + // Errors + + /** + * @notice Indicates a failure because the given CurrencyToken does not match actual CurrencyToken + * @param invalidCurrencyToken CurrencyToken that does not match the actual CurrencyToken + * @param currencyToken Actual CurrencyToken used to mint and burn the FakeComponentToken + */ + error InvalidCurrencyToken(IERC20 invalidCurrencyToken, IERC20 currencyToken); + + /** + * @notice Indicates a failure because the FakeComponentToken does not have enough CurrencyToken + * @param currencyToken CurrencyToken used to mint and burn the FakeComponentToken + * @param amount Amount of CurrencyToken required in the failed transfer + */ + error CurrencyTokenInsufficientBalance(IERC20 currencyToken, uint256 amount); + + /** + * @notice Indicates a failure because the user does not have enough CurrencyToken + * @param currencyToken CurrencyToken used to mint and burn the FakeComponentToken + * @param user Address of the user who is selling the CurrencyToken + * @param amount Amount of CurrencyToken required in the failed transfer + */ + error UserCurrencyTokenInsufficientBalance(IERC20 currencyToken, address user, uint256 amount); + + // Initializer + + /** + * @notice Initialize the FakeComponentToken + * @param owner Address of the owner of the FakeComponentToken + * @param name Name of the FakeComponentToken + * @param symbol Symbol of the FakeComponentToken + * @param currencyToken CurrencyToken used to mint and burn the FakeComponentToken + * @param decimals_ Number of decimals of the FakeComponentToken + */ + function initialize( + address owner, + string memory name, + string memory symbol, + IERC20 currencyToken, + uint8 decimals_ + ) public initializer { + __ERC20_init(name, symbol); + __AccessControl_init(); + __UUPSUpgradeable_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, owner); + _grantRole(UPGRADER_ROLE, owner); + + FakeComponentTokenStorage storage $ = _getFakeComponentTokenStorage(); + $.currencyToken = currencyToken; + $.decimals = decimals_; + } + + // Override Functions + + /** + * @notice Revert when `msg.sender` is not authorized to upgrade the contract + * @param newImplementation Address of the new implementation + */ + function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) { } + + /// @notice Number of decimals of the FakeComponentToken + function decimals() public view override returns (uint8) { + FakeComponentTokenStorage storage $ = _getFakeComponentTokenStorage(); + return $.decimals; + } + + // User Functions + + /** + * @notice Buy FakeComponentToken using CurrencyToken + * @dev The user must approve the contract to spend the CurrencyToken + * @param currencyToken_ CurrencyToken used to buy the FakeComponentToken + * @param amount Amount of FakeComponentToken to buy using the same amount of CurrencyToken + */ + function buy(IERC20 currencyToken_, uint256 amount) public { + FakeComponentTokenStorage storage $ = _getFakeComponentTokenStorage(); + IERC20 currencyToken = $.currencyToken; + + if (currencyToken_ != currencyToken) { + revert InvalidCurrencyToken(currencyToken_, currencyToken); + } + if (!currencyToken.transferFrom(msg.sender, address(this), amount)) { + revert UserCurrencyTokenInsufficientBalance(currencyToken, msg.sender, amount); + } + + _mint(msg.sender, amount); + + emit ComponentTokenBought(msg.sender, currencyToken, amount, amount); + } + + /** + * @notice Sell FakeComponentToken to receive CurrencyToken + * @param currencyToken_ CurrencyToken received in exchange for the FakeComponentToken + * @param amount Amount of FakeComponentToken to sell to receive the same amount of CurrencyToken + */ + function sell(IERC20 currencyToken_, uint256 amount) public { + FakeComponentTokenStorage storage $ = _getFakeComponentTokenStorage(); + IERC20 currencyToken = $.currencyToken; + + if (currencyToken_ != currencyToken) { + revert InvalidCurrencyToken(currencyToken_, currencyToken); + } + if (!currencyToken.transfer(msg.sender, amount)) { + revert CurrencyTokenInsufficientBalance(currencyToken, amount); + } + + _burn(msg.sender, amount); + + emit ComponentTokenSold(msg.sender, currencyToken, amount, amount); + } + + // Admin Functions + + /** + * @notice Set the CurrencyToken used to mint and burn the FakeComponentToken + * @param currencyAddress Address of the CurrencyToken + */ + function setCurrencyToken(IERC20 currencyAddress) public onlyRole(DEFAULT_ADMIN_ROLE) { + FakeComponentTokenStorage storage $ = _getFakeComponentTokenStorage(); + $.currencyToken = currencyAddress; + } + + // View Functions + + /// @notice CurrencyToken used to mint and burn the FakeComponentToken + function getCurrencyToken() public view returns (IERC20) { + FakeComponentTokenStorage storage $ = _getFakeComponentTokenStorage(); + return $.currencyToken; + } + +} diff --git a/nest/src/interfaces/IComponentToken.sol b/nest/src/interfaces/IComponentToken.sol new file mode 100644 index 0000000..463741d --- /dev/null +++ b/nest/src/interfaces/IComponentToken.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol"; + +interface IComponentToken is IERC20 { + + function buy(address currencyAddress, uint256 amount) external; + function sell(address currencyAddress, uint256 amount) external; + +} From 2afbb3dd7fb5b4abc7131d1213f30b1ba547be17 Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 00:29:39 -0700 Subject: [PATCH 05/17] remove comments in foundry.toml --- foundry-template/foundry.toml | 2 -- nest/foundry.toml | 2 -- 2 files changed, 4 deletions(-) diff --git a/foundry-template/foundry.toml b/foundry-template/foundry.toml index 8e9095d..b5d5cbd 100644 --- a/foundry-template/foundry.toml +++ b/foundry-template/foundry.toml @@ -20,8 +20,6 @@ quote_style = "double" number_underscore = "thousands" wrap_comments = true -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options - remappings = [ "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/", "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", diff --git a/nest/foundry.toml b/nest/foundry.toml index 8e9095d..b5d5cbd 100644 --- a/nest/foundry.toml +++ b/nest/foundry.toml @@ -20,8 +20,6 @@ quote_style = "double" number_underscore = "thousands" wrap_comments = true -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options - remappings = [ "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/", "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", From af8961b935d6b5349536ac47b00148ce6c1aaac5 Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 00:30:19 -0700 Subject: [PATCH 06/17] make ComponentToken.sol compile --- nest/script/DeployNestContracts.s.sol | 2 +- nest/src/FakeComponentToken.sol | 10 +++++----- nest/src/interfaces/IComponentToken.sol | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/nest/script/DeployNestContracts.s.sol b/nest/script/DeployNestContracts.s.sol index 93e2162..0fda1ff 100644 --- a/nest/script/DeployNestContracts.s.sol +++ b/nest/script/DeployNestContracts.s.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.25; import "forge-std/Script.sol"; -import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; import { FakeComponentToken } from "../src/FakeComponentToken.sol"; diff --git a/nest/src/FakeComponentToken.sol b/nest/src/FakeComponentToken.sol index e698951..127e271 100644 --- a/nest/src/FakeComponentToken.sol +++ b/nest/src/FakeComponentToken.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -import { AccessControlUpgradeable } from "openzeppelin-contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import { Initializable } from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; -import { UUPSUpgradeable } from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { ERC20Upgradeable } from "openzeppelin-contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IComponentToken } from "./interfaces/IComponentToken.sol"; diff --git a/nest/src/interfaces/IComponentToken.sol b/nest/src/interfaces/IComponentToken.sol index 463741d..3cbc753 100644 --- a/nest/src/interfaces/IComponentToken.sol +++ b/nest/src/interfaces/IComponentToken.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IComponentToken is IERC20 { From 485d90f1997879d6cc1171491d988c7acd88945c Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 01:42:48 -0700 Subject: [PATCH 07/17] remove base and change currencyAddress to currencyToken --- nest/src/FakeComponentToken.sol | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/nest/src/FakeComponentToken.sol b/nest/src/FakeComponentToken.sol index 127e271..1d2119d 100644 --- a/nest/src/FakeComponentToken.sol +++ b/nest/src/FakeComponentToken.sol @@ -43,9 +43,6 @@ contract FakeComponentToken is Initializable, AccessControlUpgradeable, UUPSUpgr /// @notice Role for the upgrader of the FakeComponentToken bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADE_ROLE"); - // Base at which we do all calculations to minimize rounding losses - uint256 private constant _BASE = 1e18; - // Events /** @@ -186,11 +183,11 @@ contract FakeComponentToken is Initializable, AccessControlUpgradeable, UUPSUpgr /** * @notice Set the CurrencyToken used to mint and burn the FakeComponentToken - * @param currencyAddress Address of the CurrencyToken + * @param currencyToken New CurrencyToken */ - function setCurrencyToken(IERC20 currencyAddress) public onlyRole(DEFAULT_ADMIN_ROLE) { + function setCurrencyToken(IERC20 currencyToken) public onlyRole(DEFAULT_ADMIN_ROLE) { FakeComponentTokenStorage storage $ = _getFakeComponentTokenStorage(); - $.currencyToken = currencyAddress; + $.currencyToken = currencyToken; } // View Functions From 67b0b234b06d84f16ebccf8643e3ff2ffb922346 Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 02:03:46 -0700 Subject: [PATCH 08/17] return the amount of component token bought / sold --- nest/src/FakeComponentToken.sol | 16 ++++++++++------ nest/src/interfaces/IComponentToken.sol | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/nest/src/FakeComponentToken.sol b/nest/src/FakeComponentToken.sol index 1d2119d..4136927 100644 --- a/nest/src/FakeComponentToken.sol +++ b/nest/src/FakeComponentToken.sol @@ -140,9 +140,9 @@ contract FakeComponentToken is Initializable, AccessControlUpgradeable, UUPSUpgr * @notice Buy FakeComponentToken using CurrencyToken * @dev The user must approve the contract to spend the CurrencyToken * @param currencyToken_ CurrencyToken used to buy the FakeComponentToken - * @param amount Amount of FakeComponentToken to buy using the same amount of CurrencyToken + * @param amount Amount of CurrencyToken to pay to receive the same amount of FakeComponentToken */ - function buy(IERC20 currencyToken_, uint256 amount) public { + function buy(IERC20 currencyToken_, uint256 amount) public returns (uint256) { FakeComponentTokenStorage storage $ = _getFakeComponentTokenStorage(); IERC20 currencyToken = $.currencyToken; @@ -156,14 +156,16 @@ contract FakeComponentToken is Initializable, AccessControlUpgradeable, UUPSUpgr _mint(msg.sender, amount); emit ComponentTokenBought(msg.sender, currencyToken, amount, amount); + + return amount; } /** * @notice Sell FakeComponentToken to receive CurrencyToken * @param currencyToken_ CurrencyToken received in exchange for the FakeComponentToken - * @param amount Amount of FakeComponentToken to sell to receive the same amount of CurrencyToken + * @param amount Amount of CurrencyToken to receive in exchange for the FakeComponentToken */ - function sell(IERC20 currencyToken_, uint256 amount) public { + function sell(IERC20 currencyToken_, uint256 amount) public returns (uint256) { FakeComponentTokenStorage storage $ = _getFakeComponentTokenStorage(); IERC20 currencyToken = $.currencyToken; @@ -177,9 +179,11 @@ contract FakeComponentToken is Initializable, AccessControlUpgradeable, UUPSUpgr _burn(msg.sender, amount); emit ComponentTokenSold(msg.sender, currencyToken, amount, amount); + + return amount; } - // Admin Functions + // Admin Setter Functions /** * @notice Set the CurrencyToken used to mint and burn the FakeComponentToken @@ -190,7 +194,7 @@ contract FakeComponentToken is Initializable, AccessControlUpgradeable, UUPSUpgr $.currencyToken = currencyToken; } - // View Functions + // Getter View Functions /// @notice CurrencyToken used to mint and burn the FakeComponentToken function getCurrencyToken() public view returns (IERC20) { diff --git a/nest/src/interfaces/IComponentToken.sol b/nest/src/interfaces/IComponentToken.sol index 3cbc753..d5933f1 100644 --- a/nest/src/interfaces/IComponentToken.sol +++ b/nest/src/interfaces/IComponentToken.sol @@ -5,7 +5,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IComponentToken is IERC20 { - function buy(address currencyAddress, uint256 amount) external; - function sell(address currencyAddress, uint256 amount) external; + function buy(IERC20 currencyToken, uint256 currencyTokenAmount) external returns (uint256 componentTokenAmount); + function sell(IERC20 currencyToken, uint256 currencyTokenAmount) external returns (uint256 componentTokenAmount); } From 28543f5a1a77dc836189834239edff405004b480 Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 02:18:33 -0700 Subject: [PATCH 09/17] extend IComponentToken --- nest/src/FakeComponentToken.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nest/src/FakeComponentToken.sol b/nest/src/FakeComponentToken.sol index 4136927..79ced95 100644 --- a/nest/src/FakeComponentToken.sol +++ b/nest/src/FakeComponentToken.sol @@ -16,7 +16,7 @@ import { IComponentToken } from "./interfaces/IComponentToken.sol"; * Users can buy and sell one FakeComponentToken by exchanging it with one CurrencyToken at any time. * @custom:oz-upgrades-from FakeComponentToken */ -contract FakeComponentToken is Initializable, AccessControlUpgradeable, UUPSUpgradeable, ERC20Upgradeable { +contract FakeComponentToken is Initializable, AccessControlUpgradeable, UUPSUpgradeable, ERC20Upgradeable, IComponentToken { // Storage From 549c8da981b8129d1a4ddd46cf0362c8a6825056 Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 02:29:01 -0700 Subject: [PATCH 10/17] workflow --- .github/workflows/continuous-integration.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 5deca91..df16b90 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -31,15 +31,15 @@ jobs: - name: Run Forge build run: | forge --version - forge build --sizes + forge build nest --sizes id: build - name: Run Forge format run: | - forge fmt + forge fmt nest id: format # - name: Run Forge tests # run: | - # forge test -vvv + # forge test nest -vvv # id: test From 12bc4fa6aa88ea120a43028737ac118c1e79b307 Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 04:46:41 -0700 Subject: [PATCH 11/17] update workflow --- .github/workflows/continuous-integration.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index df16b90..7a9b426 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -28,11 +28,11 @@ jobs: with: version: nightly - - name: Run Forge build - run: | - forge --version - forge build nest --sizes - id: build + # - name: Run Forge build + # run: | + # forge --version + # forge build nest --sizes + # id: build - name: Run Forge format run: | From c58632381e3d2814f3a2fcf6053dd3633ba12562 Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 04:49:50 -0700 Subject: [PATCH 12/17] forge fmt check --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 7a9b426..510ad55 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -36,7 +36,7 @@ jobs: - name: Run Forge format run: | - forge fmt nest + forge fmt nest --check id: format # - name: Run Forge tests From 91fbb310c9eebd4054c23145541811878a7c21fe Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 04:52:29 -0700 Subject: [PATCH 13/17] forge fmt --- nest/src/FakeComponentToken.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nest/src/FakeComponentToken.sol b/nest/src/FakeComponentToken.sol index 79ced95..b0fbf39 100644 --- a/nest/src/FakeComponentToken.sol +++ b/nest/src/FakeComponentToken.sol @@ -16,7 +16,13 @@ import { IComponentToken } from "./interfaces/IComponentToken.sol"; * Users can buy and sell one FakeComponentToken by exchanging it with one CurrencyToken at any time. * @custom:oz-upgrades-from FakeComponentToken */ -contract FakeComponentToken is Initializable, AccessControlUpgradeable, UUPSUpgradeable, ERC20Upgradeable, IComponentToken { +contract FakeComponentToken is + Initializable, + AccessControlUpgradeable, + UUPSUpgradeable, + ERC20Upgradeable, + IComponentToken +{ // Storage From fcdeb166e2d28ab03075ae8c31b21dfcf2b22297 Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 04:55:29 -0700 Subject: [PATCH 14/17] update aggregate token --- nest/script/DeployNestContracts.s.sol | 19 ++ nest/src/AggregateToken.sol | 373 ++++++++++++++++++------ nest/src/NestStaking.sol | 13 - nest/src/interfaces/IAggregateToken.sol | 15 +- 4 files changed, 307 insertions(+), 113 deletions(-) delete mode 100644 nest/src/NestStaking.sol diff --git a/nest/script/DeployNestContracts.s.sol b/nest/script/DeployNestContracts.s.sol index 0fda1ff..823b7be 100644 --- a/nest/script/DeployNestContracts.s.sol +++ b/nest/script/DeployNestContracts.s.sol @@ -6,6 +6,7 @@ import "forge-std/Script.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import { AggregateToken } from "../src/AggregateToken.sol"; import { FakeComponentToken } from "../src/FakeComponentToken.sol"; contract DeployNestContracts is Script { @@ -22,6 +23,24 @@ contract DeployNestContracts is Script { ); console.log("FakeComponentToken deployed to:", fakeComponentTokenProxy); + address aggregateTokenProxy = Upgrades.deployUUPSProxy( + "AggregateToken.sol", + abi.encodeCall( + AggregateToken.initialize, + ( + msg.sender, + "Apple", + "AAPL", + IERC20(USDC_ADDRESS), + 18, + 15e17, + 12e17, + "https://assets.plumenetwork.xyz/metadata/mineral-vault.json" + ) + ) + ); + console.log("AggregateToken deployed to:", aggregateTokenProxy); + vm.stopBroadcast(); } diff --git a/nest/src/AggregateToken.sol b/nest/src/AggregateToken.sol index 7a6a156..c99a37b 100644 --- a/nest/src/AggregateToken.sol +++ b/nest/src/AggregateToken.sol @@ -1,171 +1,350 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IAggregateToken } from "./interfaces/IAggregateToken.sol"; 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 + * @author Eugene Y. Q. Shen + * @notice ERC20 token that represents a basket of ComponentTokens + * @dev Invariant: the total value of all AggregateTokens minted is approximately + * equal to the total value of all of its constituent ComponentTokens + * @custom:oz-upgrades-from AggregateToken */ -contract AggregateToken is IComponentToken, ERC20Upgradeable { - IERC20 public currencyToken; - IComponentToken[] public componentTokens; - string public tokenURI; +contract AggregateToken is + Initializable, + AccessControlUpgradeable, + UUPSUpgradeable, + ERC20Upgradeable, + IAggregateToken +{ + + // Storage + + /// @custom:storage-location erc7201:plume.storage.AggregateToken + struct AggregateTokenStorage { + /// @dev Mapping of all ComponentTokens that have ever been added to the AggregateToken + mapping(IComponentToken componentToken => bool exists) componentTokenMap; + /// @dev List of all ComponentTokens that have ever been added to the AggregateToken + IComponentToken[] componentTokenList; + /// @dev CurrencyToken used to mint and burn the AggregateToken + IERC20 currencyToken; + /// @dev Number of decimals of the AggregateToken + uint8 decimals; + /// @dev Price at which users can buy the AggregateToken using CurrencyToken, times the base + uint256 askPrice; + /// @dev Price at which users can sell the AggregateToken to receive CurrencyToken, times the base + uint256 bidPrice; + /// @dev URI for the AggregateToken metadata + string tokenURI; + } - // Base at which we do calculations in order to minimize rounding differences - uint256 _BASE = 10 ** 18; + // keccak256(abi.encode(uint256(keccak256("plume.storage.AggregateToken")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant AGGREGATE_TOKEN_STORAGE_LOCATION = + 0xd3be8f8d43881152ac95daeff8f4c57e01616286ffd74814a5517f422a6b6200; - // 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; + function _getAggregateTokenStorage() private pure returns (AggregateTokenStorage storage $) { + assembly { + $.slot := AGGREGATE_TOKEN_STORAGE_LOCATION + } + } - uint8 private _currencyDecimals; - uint8 private _decimals; + // Constants + + /// @notice Role for the upgrader of the AggregateToken + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADE_ROLE"); + + // Base that is used to divide all price inputs in order to represent e.g. 1.000001 as 1000001e12 + uint256 private constant _BASE = 1e18; // 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 + * @notice Emitted when a user buys AggregateToken using CurrencyToken + * @param user Address of the user who bought the AggregateToken + * @param currencyToken CurrencyToken used to buy the AggregateToken + * @param currencyTokenAmount Amount of CurrencyToken paid + * @param aggregateTokenAmount Amount of AggregateToken received + */ + event AggregateTokenBought( + address indexed user, IERC20 indexed currencyToken, uint256 currencyTokenAmount, uint256 aggregateTokenAmount + ); + + /** + * @notice Emitted when a user sells AggregateToken to receive CurrencyToken + * @param user Address of the user who sold the AggregateToken + * @param currencyToken CurrencyToken received in exchange for the AggregateToken + * @param currencyTokenAmount Amount of CurrencyToken received + * @param aggregateTokenAmount Amount of AggregateToken sold + */ + event AggregateTokenSold( + address indexed user, IERC20 indexed currencyToken, uint256 currencyTokenAmount, uint256 aggregateTokenAmount + ); + + /** + * @notice Emitted when the admin buys ComponentToken using CurrencyToken + * @param admin Address of the admin who bought the ComponentToken + * @param currencyToken CurrencyToken used to buy the ComponentToken + * @param currencyTokenAmount Amount of CurrencyToken paid + * @param componentTokenAmount Amount of ComponentToken received + */ + event ComponentTokenBought( + address indexed admin, IERC20 indexed currencyToken, uint256 currencyTokenAmount, uint256 componentTokenAmount + ); + + /** + * @notice Emitted when the admin sells ComponentToken to receive CurrencyToken + * @param admin Address of the admin who sold the ComponentToken + * @param currencyToken CurrencyToken received in exchange for the ComponentToken + * @param currencyTokenAmount Amount of CurrencyToken received + * @param componentTokenAmount Amount of ComponentToken sold + */ + event ComponentTokenSold( + address indexed admin, IERC20 indexed currencyToken, uint256 currencyTokenAmount, uint256 componentTokenAmount + ); + + // Errors + + /** + * @notice Indicates a failure because the given CurrencyToken does not match actual CurrencyToken + * @param invalidCurrencyToken CurrencyToken that does not match the actual CurrencyToken + * @param currencyToken Actual CurrencyToken used to mint and burn the AggregateToken + */ + error InvalidCurrencyToken(IERC20 invalidCurrencyToken, IERC20 currencyToken); + + /** + * @notice Indicates a failure because the AggregateToken does not have enough CurrencyToken + * @param currencyToken CurrencyToken used to mint and burn the AggregateToken + * @param amount Amount of CurrencyToken required in the failed transfer */ - event Buy(address indexed user, uint256 currencyTokenAmount, uint256 aggregateTokenAmount); + error CurrencyTokenInsufficientBalance(IERC20 currencyToken, uint256 amount); /** - * @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 + * @notice Indicates a failure because the user does not have enough CurrencyToken + * @param currencyToken CurrencyToken used to mint and burn the AggregateToken + * @param user Address of the user who is selling the CurrencyToken + * @param amount Amount of CurrencyToken required in the failed transfer */ - event Sell(address indexed user, uint256 currencyTokenAmount, uint256 aggregateTokenAmount); + error UserCurrencyTokenInsufficientBalance(IERC20 currencyToken, address user, uint256 amount); + + // Initializer + /** + * @notice Initialize the AggregateToken + * @param owner Address of the owner of the AggregateToken + * @param name Name of the AggregateToken + * @param symbol Symbol of the AggregateToken + * @param currencyToken CurrencyToken used to mint and burn the AggregateToken + * @param decimals_ Number of decimals of the AggregateToken + * @param askPrice Price at which users can buy the AggregateToken using CurrencyToken, times the base + * @param bidPrice Price at which users can sell the AggregateToken to receive CurrencyToken, times the base + * @param tokenURI URI of the AggregateToken metadata + */ function initialize( + address owner, string memory name, string memory symbol, - uint8 __decimals, - string memory _tokenURI, - address _currencyToken, - address[] _componentTokens, - uint256 _askPrice, - uint256 _bidPrice + IERC20 currencyToken, + uint8 decimals_, + uint256 askPrice, + uint256 bidPrice, + string memory tokenURI ) 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; + __ERC20_init(name, symbol); + __AccessControl_init(); + __UUPSUpgradeable_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, owner); + _grantRole(UPGRADER_ROLE, owner); + + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + $.currencyToken = currencyToken; + $.decimals = decimals_; + $.askPrice = askPrice; + $.bidPrice = bidPrice; + $.tokenURI = tokenURI; } // Override Functions /** - * @notice Returns the number of decimals of the aggregateToken + * @notice Revert when `msg.sender` is not authorized to upgrade the contract + * @param newImplementation Address of the new implementation */ + function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) { } + + /// @notice Number of decimals of the AggregateToken function decimals() public view override returns (uint8) { - return _decimals; + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + 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 + * @notice Buy AggregateToken using CurrencyToken + * @dev The user must approve the contract to spend the CurrencyToken + * @param currencyToken_ CurrencyToken used to buy the AggregateToken + * @param currencyTokenAmount Amount of CurrencyToken to pay for the AggregateToken */ - function buy( - uint256 currencyTokenAmount - ) public { - // TODO: figure decimals math - uint256 aggregateTokenAmount = currencyTokenAmount * _BASE / askPrice; + function buy(IERC20 currencyToken_, uint256 currencyTokenAmount) public returns (uint256 aggregateTokenAmount) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + IERC20 currencyToken = $.currencyToken; + + if (currencyToken_ != currencyToken) { + revert InvalidCurrencyToken(currencyToken_, currencyToken); + } + if (!currencyToken.transferFrom(msg.sender, address(this), currencyTokenAmount)) { + revert UserCurrencyTokenInsufficientBalance(currencyToken, msg.sender, currencyTokenAmount); + } + + 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); + emit AggregateTokenBought(msg.sender, currencyToken, currencyTokenAmount, aggregateTokenAmount); } /** - * @notice Sell the aggregateToken to receive currencyToken - * @param currencyTokenAmount Amount of currencyToken to receive for the aggregateToken + * @notice Sell AggregateToken to receive CurrencyToken + * @param currencyToken_ CurrencyToken received in exchange for the AggregateToken + * @param currencyTokenAmount Amount of CurrencyToken to receive in exchange for the AggregateToken */ - function sell( - uint256 currencyTokenAmount - ) public { - // TODO: figure decimals math - uint256 aggregateTokenAmount = currencyTokenAmount * _BASE / bidPrice; + function sell(IERC20 currencyToken_, uint256 currencyTokenAmount) public returns (uint256 aggregateTokenAmount) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + IERC20 currencyToken = $.currencyToken; + + if (currencyToken_ != currencyToken) { + revert InvalidCurrencyToken(currencyToken_, currencyToken); + } + if (!currencyToken.transfer(msg.sender, currencyTokenAmount)) { + revert CurrencyTokenInsufficientBalance(currencyToken, currencyTokenAmount); + } + + 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); + emit AggregateTokenSold(msg.sender, currencyToken, currencyTokenAmount, aggregateTokenAmount); } + // Admin Functions + /** - * @notice + * @notice Buy ComponentToken using CurrencyToken + * @dev Will revert if the AggregateToken does not have enough CurrencyToken to buy the ComponentToken + * @param componentToken ComponentToken to buy + * @param currencyTokenAmount Amount of CurrencyToken to pay to receive the ComponentToken */ - function claim(uint256 amount) public { - // TODO - rebasing vs. streaming - } + function buyComponentToken( + IComponentToken componentToken, + uint256 currencyTokenAmount + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + IERC20 currencyToken = $.currencyToken; + + if (!$.componentTokenMap[componentToken]) { + $.componentTokenMap[componentToken] = true; + $.componentTokenList.push(componentToken); + } + uint256 componentTokenAmount = componentToken.buy(currencyToken, currencyTokenAmount); - function claimAll() public { - uint256 amount = claimableAmount(msg.sender); - claim(amount); + emit ComponentTokenBought(msg.sender, $.currencyToken, currencyTokenAmount, componentTokenAmount); } - // Admin Functions + /** + * @notice Sell ComponentToken to receive CurrencyToken + * @dev Will revert if the ComponentToken does not have enough CurrencyToken to sell to the AggregateToken + * @param componentToken ComponentToken to sell + * @param currencyTokenAmount Amount of CurrencyToken to receive in exchange for the ComponentToken + */ + function sellComponentToken( + IComponentToken componentToken, + uint256 currencyTokenAmount + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + IERC20 currencyToken = $.currencyToken; - function buyComponentToken(address token, uint256 amount) public onlyOwner { - // TODO verify it's allowed - IComponentToken(token).buy(amount); - } + uint256 componentTokenAmount = componentToken.sell(currencyToken, currencyTokenAmount); - function sellComponentToken(address token, uint256 amount) public onlyOwner { - IComponentToken(token).sell(amount); + emit ComponentTokenSold(msg.sender, currencyToken, currencyTokenAmount, componentTokenAmount); } // Admin Setter Functions - function setTokenURI(string memory uri) public onlyOwner { - tokenURI = uri; + /** + * @notice Set the CurrencyToken used to mint and burn the AggregateToken + * @param currencyToken New CurrencyToken + */ + function setCurrencyToken(IERC20 currencyToken) public onlyRole(DEFAULT_ADMIN_ROLE) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + $.currencyToken = currencyToken; } - function addAllowedComponentToken( - address token, - ) public onlyOwner { - componentTokens.push(IComponentToken(token)); + /** + * @notice Set the price at which users can buy the AggregateToken using CurrencyToken + * @param askPrice New ask price + */ + function setAskPrice(uint256 askPrice) public onlyRole(DEFAULT_ADMIN_ROLE) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + $.askPrice = askPrice; } - function removeAllowedComponentToken( - address token, - ) public onlyOwner { - componentTokens.push(IComponentToken(token)); + /** + * @notice Set the price at which users can sell the AggregateToken to receive CurrencyToken + * @param bidPrice New bid price + */ + function setBidPrice(uint256 bidPrice) public onlyRole(DEFAULT_ADMIN_ROLE) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + $.bidPrice = bidPrice; } - function setAskPrice(uint256 price) public onlyOwner { - askPrice = price; + /** + * @notice Set the URI for the AggregateToken metadata + * @param tokenURI New token URI + */ + function setTokenURI(string memory tokenURI) public onlyRole(DEFAULT_ADMIN_ROLE) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + $.tokenURI = tokenURI; } - - function setBidPrice(uint256 price) public onlyOwner { - bidPrice = price; + + // Getter View Functions + + /// @notice CurrencyToken used to mint and burn the AggregateToken + function getCurrencyToken() public view returns (IERC20) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + return $.currencyToken; } - // View Functions + /// @notice Price at which users can buy the AggregateToken using CurrencyToken, times the base + function getAskPrice() public view returns (uint256) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + return $.askPrice; + } - function claimableAmount(address user) public view returns (uint256 amount) { - amount = 0; + /// @notice Price at which users can sell the AggregateToken to receive CurrencyToken, times the base + function getBidPrice() public view returns (uint256) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + return $.bidPrice; } - function allowedComponentTokens() public view returns (address[] memory) { + /// @notice URI for the AggregateToken metadata + function getTokenURI() public view returns (string memory) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + return $.tokenURI; + } + /// @notice Get all ComponentTokens that have ever been added to the AggregateToken + function getComponentTokenList() public view returns (IComponentToken[] memory) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + return $.componentTokenList; } -} \ No newline at end of file + +} diff --git a/nest/src/NestStaking.sol b/nest/src/NestStaking.sol deleted file mode 100644 index e8e8a2f..0000000 --- a/nest/src/NestStaking.sol +++ /dev/null @@ -1,13 +0,0 @@ -// 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 index c3c90a3..bd82955 100644 --- a/nest/src/interfaces/IAggregateToken.sol +++ b/nest/src/interfaces/IAggregateToken.sol @@ -1,6 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -interface IComponentToken { - function setTokenURI(string memory uri) external; -} \ No newline at end of file +import { IComponentToken } from "./IComponentToken.sol"; + +interface IAggregateToken is IComponentToken { + + function buyComponentToken(IComponentToken componentToken, uint256 currencyTokenAmount) external; + function sellComponentToken(IComponentToken componentToken, uint256 currencyTokenAmount) external; + function getAskPrice() external view returns (uint256); + function getBidPrice() external view returns (uint256); + function getTokenURI() external view returns (string memory); + function getComponentTokenList() external view returns (IComponentToken[] memory); + +} From bf7bfd9ba36add04351f4f558abb3f77e281e45a Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 05:46:24 -0700 Subject: [PATCH 15/17] add approvals --- nest/script/DeployNestContracts.s.sol | 6 ++++-- nest/src/AggregateToken.sol | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/nest/script/DeployNestContracts.s.sol b/nest/script/DeployNestContracts.s.sol index 823b7be..92a8856 100644 --- a/nest/script/DeployNestContracts.s.sol +++ b/nest/script/DeployNestContracts.s.sol @@ -19,7 +19,9 @@ contract DeployNestContracts is Script { address fakeComponentTokenProxy = Upgrades.deployUUPSProxy( "FakeComponentToken.sol", - abi.encodeCall(FakeComponentToken.initialize, (msg.sender, "Banana", "BAN", IERC20(USDC_ADDRESS), 18)) + abi.encodeCall( + FakeComponentToken.initialize, (ARC_ADMIN_ADDRESS, "Banana", "BAN", IERC20(USDC_ADDRESS), 18) + ) ); console.log("FakeComponentToken deployed to:", fakeComponentTokenProxy); @@ -28,7 +30,7 @@ contract DeployNestContracts is Script { abi.encodeCall( AggregateToken.initialize, ( - msg.sender, + ARC_ADMIN_ADDRESS, "Apple", "AAPL", IERC20(USDC_ADDRESS), diff --git a/nest/src/AggregateToken.sol b/nest/src/AggregateToken.sol index c99a37b..d20c068 100644 --- a/nest/src/AggregateToken.sol +++ b/nest/src/AggregateToken.sol @@ -254,7 +254,10 @@ contract AggregateToken is $.componentTokenMap[componentToken] = true; $.componentTokenList.push(componentToken); } + + currencyToken.approve(address(componentToken), currencyTokenAmount); uint256 componentTokenAmount = componentToken.buy(currencyToken, currencyTokenAmount); + componentToken.approve(address(componentToken), 0); emit ComponentTokenBought(msg.sender, $.currencyToken, currencyTokenAmount, componentTokenAmount); } @@ -341,6 +344,15 @@ contract AggregateToken is return $.tokenURI; } + /** + * @notice Check if the given ComponentToken has ever been added to the AggregateToken + * @param componentToken ComponentToken to check + */ + function getComponentToken(IComponentToken componentToken) public view returns (bool) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + return $.componentTokenMap[componentToken]; + } + /// @notice Get all ComponentTokens that have ever been added to the AggregateToken function getComponentTokenList() public view returns (IComponentToken[] memory) { AggregateTokenStorage storage $ = _getAggregateTokenStorage(); From 229a156afa43e9d60f4b9ebfcd1d8a6b03fb1c0b Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 06:21:09 -0700 Subject: [PATCH 16/17] comment out workflows --- .github/workflows/continuous-integration.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 510ad55..77b2ef2 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -34,10 +34,10 @@ jobs: # forge build nest --sizes # id: build - - name: Run Forge format - run: | - forge fmt nest --check - id: format + # - name: Run Forge format + # run: | + # forge fmt nest --check + # id: format # - name: Run Forge tests # run: | From 9e19b20f38cd22030e2c21adafb56245440e35f1 Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Fri, 13 Sep 2024 06:46:28 -0700 Subject: [PATCH 17/17] add currency token to the component token list --- nest/src/AggregateToken.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nest/src/AggregateToken.sol b/nest/src/AggregateToken.sol index d20c068..7d6e6a4 100644 --- a/nest/src/AggregateToken.sol +++ b/nest/src/AggregateToken.sol @@ -165,6 +165,8 @@ contract AggregateToken is _grantRole(UPGRADER_ROLE, owner); AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + $.componentTokenMap[currencyToken] = true; + $.componentTokenList.push(currencyToken); $.currencyToken = currencyToken; $.decimals = decimals_; $.askPrice = askPrice;