diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 5deca91..77b2ef2 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -28,18 +28,18 @@ jobs: with: version: nightly - - name: Run Forge build - run: | - forge --version - forge build --sizes - id: build + # - name: Run Forge build + # run: | + # forge --version + # forge build nest --sizes + # id: build - - name: Run Forge format - run: | - forge fmt - id: format + # - name: Run Forge format + # run: | + # forge fmt nest --check + # id: format # - name: Run Forge tests # run: | - # forge test -vvv + # forge test nest -vvv # id: test diff --git a/nest/script/DeployNestContracts.s.sol b/nest/script/DeployNestContracts.s.sol new file mode 100644 index 0000000..0fda1ff --- /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..b0fbf39 --- /dev/null +++ b/nest/src/FakeComponentToken.sol @@ -0,0 +1,211 @@ +// 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, + IComponentToken +{ + + // 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"); + + // 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 CurrencyToken to pay to receive the same amount of FakeComponentToken + */ + function buy(IERC20 currencyToken_, uint256 amount) public returns (uint256) { + 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); + + return amount; + } + + /** + * @notice Sell FakeComponentToken to receive CurrencyToken + * @param currencyToken_ CurrencyToken received in exchange for the FakeComponentToken + * @param amount Amount of CurrencyToken to receive in exchange for the FakeComponentToken + */ + function sell(IERC20 currencyToken_, uint256 amount) public returns (uint256) { + 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); + + return amount; + } + + // Admin Setter Functions + + /** + * @notice Set the CurrencyToken used to mint and burn the FakeComponentToken + * @param currencyToken New CurrencyToken + */ + function setCurrencyToken(IERC20 currencyToken) public onlyRole(DEFAULT_ADMIN_ROLE) { + FakeComponentTokenStorage storage $ = _getFakeComponentTokenStorage(); + $.currencyToken = currencyToken; + } + + // Getter 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..d5933f1 --- /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(IERC20 currencyToken, uint256 currencyTokenAmount) external returns (uint256 componentTokenAmount); + function sell(IERC20 currencyToken, uint256 currencyTokenAmount) external returns (uint256 componentTokenAmount); + +}