diff --git a/deployments/8453/fx/fxUSDC.json b/deployments/8453/fx/fxUSDC.json new file mode 100644 index 0000000..3a76e50 --- /dev/null +++ b/deployments/8453/fx/fxUSDC.json @@ -0,0 +1,4 @@ +{ + "fxUSDC": "0x857011642C31C611b6A11dE6F6c802b6Dd776d3E", + "fxUSDCImp": "0xAA6b1c93aF26636a894dCF9B73FDc287E1C76501" +} \ No newline at end of file diff --git a/deployments/901/fx/fxUSDC.json b/deployments/901/fx/fxUSDC.json new file mode 100644 index 0000000..1075de1 --- /dev/null +++ b/deployments/901/fx/fxUSDC.json @@ -0,0 +1,5 @@ +{ + "fxUSDC": "0xdf986C23f298AfaDeea0b739167Cc2Eac5F9417e", + "fxUSDCImp": "0xc4650837767557B487e91173B653cDcF68672F00", + "fxUSDCAsset": "0x1ddefDd26a10eFf75301d71c65D3d0066F00A4AA" +} \ No newline at end of file diff --git a/deployments/957/fx/fxUSDC.json b/deployments/957/fx/fxUSDC.json new file mode 100644 index 0000000..3cbf0a1 --- /dev/null +++ b/deployments/957/fx/fxUSDC.json @@ -0,0 +1,5 @@ +{ + "fxUSDC": "0xb82E56B142CA4D32BdeE04313139F26e81cE92D2", + "fxUSDCImp": "0xEb4975c0B9f850b292Eb452D274fb9381B21Cb91", + "fxUSDCAsset": "0xD696A90f262324375310992aD73A38E0917DE4cd" +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 798b799..aeb83fe 100644 --- a/foundry.toml +++ b/foundry.toml @@ -23,4 +23,7 @@ quote_style = 'double' [rpc_endpoints] conduit_prod = "https://l2-prod-testnet-0eakp60405.t.conduit.xyz" conduit_staging = "https://l2-staging-9ns7v94tpj.t.conduit.xyz" -sepolia = "https://sepolia.infura.io/v3/26251a7744c548a3adbc17880fc70764" \ No newline at end of file +sepolia = "https://sepolia.infura.io/v3/26251a7744c548a3adbc17880fc70764" + +[profile.CORE] +src = "lib/v2-core/src" \ No newline at end of file diff --git a/scripts/deploy-fxUSDC.s.sol b/scripts/deploy-fxUSDC.s.sol new file mode 100644 index 0000000..19081b3 --- /dev/null +++ b/scripts/deploy-fxUSDC.s.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; + +import "forge-std/console2.sol"; +import {Utils} from "./utils.sol"; +import "../src/periphery/LyraSettlementUtils.sol"; +import {BaseTSA} from "../src/tokenizedSubaccounts/BaseTSA.sol"; +import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; +import {CashAsset} from "v2-core/src/assets/CashAsset.sol"; +import {DutchAuction} from "v2-core/src/liquidation/DutchAuction.sol"; +import {ILiquidatableManager} from "v2-core/src/interfaces/ILiquidatableManager.sol"; +import {IMatching} from "../src/interfaces/IMatching.sol"; +import {IDepositModule} from "../src/interfaces/IDepositModule.sol"; +import {IWithdrawalModule} from "../src/interfaces/IWithdrawalModule.sol"; +import {ITradeModule} from "../src/interfaces/ITradeModule.sol"; +import {ISpotFeed} from "v2-core/src/interfaces/ISpotFeed.sol"; +import {IWrappedERC20Asset} from "v2-core/src/interfaces/IWrappedERC20Asset.sol"; +import "../src/tokenizedSubaccounts/CCTSA.sol"; +import "../src/tokenizedSubaccounts/PPTSA.sol"; +import "openzeppelin/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {TokenizedSubAccount} from "../src/tokenizedSubaccounts/TSA.sol"; +import "openzeppelin/proxy/transparent/ProxyAdmin.sol"; +import {TSAShareHandler} from "../src/tokenizedSubaccounts/TSAShareHandler.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {FXToken} from "../src/fx/FXToken.sol"; +import {WrappedERC20Asset} from "v2-core/src/assets/WrappedERC20Asset.sol"; + + +contract DeployFXUSDC is Utils { + /// @dev main function + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + string name = vm.envString("TOKEN_NAME"); + + address deployer = vm.addr(deployerPrivateKey); + console2.log("deployer: ", deployer); + + FXToken fxTokenImplementation = new FXToken(); + + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(fxTokenImplementation), + address(deployer), + abi.encodeWithSelector(fxTokenImplementation.initialize.selector, name, "fxUSDC", 6) + ); + + + FXToken fxUSDC = FXToken(address(proxy)); + + console2.log("fxUSDC: ", address(fxUSDC)); + console2.log("fxUSDCImp: ", address(fxTokenImplementation)); + + WrappedERC20Asset fxUSDCAsset = new WrappedERC20Asset(ISubAccounts(_loadConfig().subAccounts), fxUSDC); + + console2.log("fxUSDCAsset: ", address(fxUSDCAsset)); + } +} \ No newline at end of file diff --git a/src/fx/FXToken.sol b/src/fx/FXToken.sol new file mode 100644 index 0000000..71b1103 --- /dev/null +++ b/src/fx/FXToken.sol @@ -0,0 +1,105 @@ +pragma solidity ^0.8.27; + +import {AccessControlUpgradeable} from "openzeppelin-upgradeable/access/AccessControlUpgradeable.sol"; +import {ERC20Upgradeable, Initializable} from "openzeppelin-upgradeable/token/ERC20/ERC20Upgradeable.sol"; + + +contract FXToken is Initializable, ERC20Upgradeable, AccessControlUpgradeable { + bytes32 public constant MINTER_ROLE = keccak256("MINTER"); + bytes32 public constant BLOCK_MANAGER_ROLE = keccak256("BLOCK_MANAGER"); + // keccak256(abi.encode(uint256(keccak256("FxToken")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant FxTokenStorageLocation = 0xfb8997de7bd810675586dece12917931ae29ba246c9d4d120b17fca6e2b68f00; + + + /// @custom:storage-location erc7201:FxToken + struct FxTokenStorage { + uint8 decimals; + mapping(address user => bool blocked) isBlocked; + } + + function _getStorage() internal pure returns (FxTokenStorage storage s) { + bytes32 position = FxTokenStorageLocation; + assembly { + s.slot := position + } + } + + /////////// + // Setup // + /////////// + + constructor() { + _disableInitializers(); + } + + function initialize(string memory _name, string memory _symbol, uint _decimals) external initializer { + __ERC20_init_unchained(_name, _symbol); + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + + FxTokenStorage storage s = _getStorage(); + s.decimals = uint8(_decimals); + } + + //////////////// + // Block List // + //////////////// + + function setBlocked(address user, bool blocked) public onlyRole(BLOCK_MANAGER_ROLE) { + require(user != address(0), "FxToken: cannot block zero address"); + FxTokenStorage storage s = _getStorage(); + s.isBlocked[user] = blocked; + emit Blocked(user, blocked); + } + + function isBlocked(address user) public view returns (bool) { + return _getStorage().isBlocked[user]; + } + + /////////////// + // Mint/Burn // + /////////////// + + function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { + FxTokenStorage storage s = _getStorage(); + require(!s.isBlocked[msg.sender], "FxToken: minter is blocked"); + _mint(to, amount); + } + + function burn(address from, uint256 amount) public onlyRole(MINTER_ROLE) { + FxTokenStorage storage s = _getStorage(); + require(!s.isBlocked[msg.sender], "FxToken: minter is blocked"); + + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + // Skip the _update call to avoid checking blocked status + super._update(from, address(0), amount); + } + + ///////////////////// + // ERC20 Overrides // + ///////////////////// + + function _update(address from, address to, uint256 value) internal override { + FxTokenStorage storage s = _getStorage(); + require(!s.isBlocked[from], "FxToken: sender is blocked"); + require(!s.isBlocked[to], "FxToken: recipient is blocked"); + super._update(from, to, value); + } + + function _spendAllowance(address owner, address spender, uint256 value) internal override { + FxTokenStorage storage s = _getStorage(); + require(!s.isBlocked[spender], "FxToken: spender is blocked"); + super._spendAllowance(owner, spender, value); + } + + function decimals() public view virtual override returns (uint8) { + return _getStorage().decimals; + } + + //////////// + // Events // + //////////// + + event Blocked(address indexed user, bool blocked); +} diff --git a/test/ForkBase.t.sol b/test/ForkBase.t.sol index d3667d6..f3324a0 100644 --- a/test/ForkBase.t.sol +++ b/test/ForkBase.t.sol @@ -14,9 +14,9 @@ contract ForkBase is UtilBase, Test { } function _call(address target, bytes memory data) internal returns (bytes memory) { - console.log(target); - console.log(",0,"); - console.logBytes(data); +// console.log(target); +// console.log(",0,"); +// console.logBytes(data); (bool success, bytes memory result) = target.call(data); require(success, "call failed"); return result; diff --git a/test/fx/FXToken.sol b/test/fx/FXToken.sol new file mode 100644 index 0000000..b899af7 --- /dev/null +++ b/test/fx/FXToken.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "openzeppelin/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {FXToken} from "../../src/fx/FXToken.sol"; + +contract FXTokenTest is Test { + FXToken token; + + address admin = address(0x1); + address minter = address(0x2); + address blocker = address(0x2); + address alice = address(0xa); + address bob = address(0xb); + address charlie = address(0xc); + + function setUp() public { + // admin deploys, so becomes admin + vm.startPrank(admin); + FXToken fxTokenImplementation = new FXToken(); + + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(fxTokenImplementation), + address(alice), + abi.encodeWithSelector(fxTokenImplementation.initialize.selector, "fx USDC", "fxUSDC", 6) + ); + token = FXToken(address(proxy)); + + // Set roles + token.grantRole(token.MINTER_ROLE(), minter); + token.grantRole(token.BLOCK_MANAGER_ROLE(), blocker); + + vm.stopPrank(); + } + + function testInitialSetup() public view { + assertEq(token.name(), "fx USDC"); + assertEq(token.symbol(), "fxUSDC"); + assertEq(token.decimals(), 6); + assertTrue(token.hasRole(token.MINTER_ROLE(), minter)); + assertTrue(token.hasRole(token.BLOCK_MANAGER_ROLE(), blocker)); + } + + function testMint() public { + vm.prank(minter); + token.mint(alice, 100); + + assertEq(token.balanceOf(alice), 100); + } + + function testBurn() public { + vm.prank(minter); + token.mint(alice, 100); + + assertEq(token.balanceOf(alice), 100); + + vm.prank(minter); + token.burn(alice, 50); + + assertEq(token.balanceOf(alice), 50); + } + + function testBlockUser() public { + vm.prank(minter); + token.mint(bob, 100); + + vm.prank(blocker); + token.setBlocked(bob, true); + + assertTrue(token.isBlocked(bob)); + + // Minting to a blocked user should revert + vm.prank(minter); + vm.expectRevert("FxToken: recipient is blocked"); + token.mint(bob, 100); + + // Burning from a blocked user is allowed + vm.prank(minter); + token.burn(bob, 50); + + assertEq(token.balanceOf(bob), 50); + + vm.prank(minter); + token.mint(alice, 100); + + assertEq(token.balanceOf(alice), 100); + + vm.prank(alice); + vm.expectRevert("FxToken: recipient is blocked"); + token.transfer(bob, 50); + + // A blocked user cannot transfer tokens + vm.prank(bob); + vm.expectRevert("FxToken: sender is blocked"); + token.transfer(alice, 50); + + // A blocked user approving is allowed, but the spender cannot transfer + vm.prank(bob); + token.approve(alice, 50); + + vm.prank(alice); + vm.expectRevert("FxToken: sender is blocked"); + token.transferFrom(bob, alice, 50); + + // Cannot transferFrom to a blocked user + vm.prank(alice); + token.approve(charlie, 50); + + vm.prank(charlie); + vm.expectRevert("FxToken: recipient is blocked"); + token.transferFrom(alice, bob, 50); + + // Cannot spend allowance if spender is blocked + vm.prank(alice); + token.approve(bob, 50); + + vm.prank(bob); + vm.expectRevert("FxToken: spender is blocked"); + token.transferFrom(alice, charlie, 50); + } + + function testCannotBlockZeroAddress() public { + vm.prank(blocker); + vm.expectRevert("FxToken: cannot block zero address"); + token.setBlocked(address(0), true); + } + + + +} \ No newline at end of file