Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions deployments/8453/fx/fxUSDC.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"fxUSDC": "0x857011642C31C611b6A11dE6F6c802b6Dd776d3E",
"fxUSDCImp": "0xAA6b1c93aF26636a894dCF9B73FDc287E1C76501"
}
5 changes: 5 additions & 0 deletions deployments/901/fx/fxUSDC.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"fxUSDC": "0xdf986C23f298AfaDeea0b739167Cc2Eac5F9417e",
"fxUSDCImp": "0xc4650837767557B487e91173B653cDcF68672F00",
"fxUSDCAsset": "0x1ddefDd26a10eFf75301d71c65D3d0066F00A4AA"
}
5 changes: 5 additions & 0 deletions deployments/957/fx/fxUSDC.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"fxUSDC": "0xb82E56B142CA4D32BdeE04313139F26e81cE92D2",
"fxUSDCImp": "0xEb4975c0B9f850b292Eb452D274fb9381B21Cb91",
"fxUSDCAsset": "0xD696A90f262324375310992aD73A38E0917DE4cd"
}
5 changes: 4 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
sepolia = "https://sepolia.infura.io/v3/26251a7744c548a3adbc17880fc70764"

[profile.CORE]
src = "lib/v2-core/src"
58 changes: 58 additions & 0 deletions scripts/deploy-fxUSDC.s.sol
Original file line number Diff line number Diff line change
@@ -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));
}
}
105 changes: 105 additions & 0 deletions src/fx/FXToken.sol
Original file line number Diff line number Diff line change
@@ -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);
}
6 changes: 3 additions & 3 deletions test/ForkBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
132 changes: 132 additions & 0 deletions test/fx/FXToken.sol
Original file line number Diff line number Diff line change
@@ -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);
}



}
Loading