From d2eafc6dbcfed4bc584348e962250c663ae5cf7f Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 25 Mar 2025 22:30:02 +0900 Subject: [PATCH 1/6] feat: Add role for pausable and pause/unpause functions --- contracts/token/Credit.sol | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/contracts/token/Credit.sol b/contracts/token/Credit.sol index 281555d..43032d2 100644 --- a/contracts/token/Credit.sol +++ b/contracts/token/Credit.sol @@ -29,19 +29,18 @@ contract Credit is uint256[500] private __gap0; - error OnlyAdmin(); - error OnlyTransferAllowedRole(); error NoAdminExists(); - error OnlyOysterMarket(); - error NotEnoughUSDC(); + error OnlyAdmin(); error OnlyToEmergencyWithdrawRole(); + error OnlyTransferAllowedRole(); bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); // 0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6 bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); // 0x3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a848 bytes32 public constant TRANSFER_ALLOWED_ROLE = keccak256("TRANSFER_ALLOWED_ROLE"); // 0xed89ee80d998965e2804dad373576bf7ffc490ba5986d52deb7d526e93617101 bytes32 public constant REDEEMER_ROLE = keccak256("REDEEMER_ROLE"); // 0x44ac9762eec3a11893fefb11d028bb3102560094137c3ed4518712475b2577cc bytes32 public constant EMERGENCY_WITHDRAW_ROLE = keccak256("EMERGENCY_WITHDRAW_ROLE"); // 0x66f144ecd65ad16d38ecdba8687842af4bc05fde66fe3d999569a3006349785f - + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); // 0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a + modifier onlyAdmin() { require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), OnlyAdmin()); _; @@ -72,10 +71,18 @@ contract Credit is require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), OnlyAdmin()); } + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + function unpause() external onlyRole(PAUSER_ROLE) { + _unpause(); + } + //-------------------------------- Overrides end --------------------------------// /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address immutable USDC; + address public immutable USDC; uint256[500] private __gap1; @@ -92,7 +99,8 @@ contract Credit is __AccessControlEnumerable_init_unchained(); __ERC20_init_unchained("Oyster Credit", "CREDIT"); __UUPSUpgradeable_init_unchained(); - + __Pausable_init_unchained(); + _grantRole(DEFAULT_ADMIN_ROLE, _admin); } From 0c1d7b1ec450ad8a7cbd4897bcefc1a028c32e0b Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 25 Mar 2025 22:30:21 +0900 Subject: [PATCH 2/6] test: add tests for Credit contract --- test/token/Credit.ts | 393 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 test/token/Credit.ts diff --git a/test/token/Credit.ts b/test/token/Credit.ts new file mode 100644 index 0000000..5e70e22 --- /dev/null +++ b/test/token/Credit.ts @@ -0,0 +1,393 @@ +import { expect } from "chai"; +import { Signer } from "ethers"; +import { + ethers, + upgrades, +} from "hardhat"; + +import { + Credit, + Credit__factory, + Pond, + Pond__factory, +} from "../../typechain-types"; + +const creditAmount = (amount: number) => { + return ethers.utils.parseUnits(amount.toString(), "6"); +} + +describe("Credit", function () { + let signers: Signer[]; + let addrs: string[]; + let credit: Credit; + let usdc: Pond; + + let admin: Signer; + let user: Signer; + let user2: Signer; + + beforeEach(async function () { + // Signers + signers = await ethers.getSigners(); + admin = signers[0]; + user = signers[1]; + user2 = signers[2]; + + // Deploy Pond + const USDC = await ethers.getContractFactory("Pond"); + const usdcProxy = await upgrades.deployProxy(USDC, [], { + kind: "uups", + unsafeAllow: ["missing-initializer-call"], + initializer: false, + }); + usdc = Pond__factory.connect(usdcProxy.address, admin); + await usdc.initialize("USDC", "USDC"); + + // Deploy Credit + const Credit = await ethers.getContractFactory("Credit"); + const creditTokenContract = await upgrades.deployProxy(Credit, { + kind: "uups", + constructorArgs: [usdc.address], + initializer: false, + }); + credit = Credit__factory.connect(creditTokenContract.address, admin); + await credit.initialize(await admin.getAddress()); + }); + + describe("Access Control", function () { + it("should revert when 0 admins", async function () { + await expect(credit.connect(admin).revokeRole(await credit.DEFAULT_ADMIN_ROLE(), await admin.getAddress())).to.be.revertedWithCustomError(credit, "NoAdminExists"); + }); + }); + + describe("Getters", function () { + it("should get USDC", async function () { + expect(await credit.USDC()).to.equal(usdc.address); + }); + + it("should get decimals", async function () { + expect(await credit.decimals()).to.equal(6); + }); + + it("should get role", async function () { + expect(await credit.hasRole(await credit.DEFAULT_ADMIN_ROLE(), await admin.getAddress())).to.be.true; + }); + }); + + describe("Initialize", function () { + it("should deploy with initialization disabled", async function () { + await expect(credit.initialize(await admin.getAddress())).to.be.revertedWith("Initializable: contract is already initialized"); + }); + + it("should deploy as proxy and initialize", async function () { + const Credit = await ethers.getContractFactory("Credit"); + const creditTokenContract = await upgrades.deployProxy(Credit, { + kind: "uups", + constructorArgs: [usdc.address], + initializer: false, + }); + const credit = Credit__factory.connect(creditTokenContract.address, admin); + await credit.initialize(await admin.getAddress()); + + expect(await credit.USDC()).to.equal(usdc.address); + expect(await credit.hasRole(await credit.DEFAULT_ADMIN_ROLE(), await admin.getAddress())).to.be.true; + }); + }); + + describe("Mint/Burn", function () { + describe("Mint", function () { + it("should mint", async function () { + await credit.connect(admin).grantRole(await credit.MINTER_ROLE(), await admin.getAddress()); + await credit.connect(admin).grantRole(await credit.TRANSFER_ALLOWED_ROLE(), await admin.getAddress()); + await credit.connect(admin).mint(await admin.getAddress(), creditAmount(1000)); + expect(await credit.balanceOf(await admin.getAddress())).to.equal(creditAmount(1000)); + }); + + it("should revert when not minter", async function () { + await expect(credit.connect(user).mint(await user.getAddress(), creditAmount(1000))).to.be.reverted; + }); + + it("should revert when not transfer allowed", async function () { + await credit.connect(admin).grantRole(await credit.MINTER_ROLE(), await admin.getAddress()); + await expect(credit.connect(admin).mint(await user.getAddress(), creditAmount(1000))).to.be.reverted; + }); + }); + + describe("Burn", function () { + beforeEach(async function () { + // Grant `MINTER_ROLE` and `TRANSFER_ALLOWED_ROLE` to admin + await credit.connect(admin).grantRole(await credit.MINTER_ROLE(), await admin.getAddress()); + await credit.connect(admin).grantRole(await credit.TRANSFER_ALLOWED_ROLE(), await admin.getAddress()); + + // Mint 1000 Credit to admin + await credit.connect(admin).mint(await admin.getAddress(), creditAmount(1000)); + }); + + it("should burn", async function () { + // Transfer 1000 Credit to user + await credit.connect(admin).mint(await admin.getAddress(), creditAmount(1000)); + await credit.connect(admin).grantRole(await credit.TRANSFER_ALLOWED_ROLE(), await user.getAddress()); + await credit.connect(admin).transfer(await user.getAddress(), creditAmount(1000)); + + // Grant `BURNER_ROLE` to admin + await credit.connect(admin).grantRole(await credit.BURNER_ROLE(), await admin.getAddress()); + + // Burn 1000 Credit from user + await credit.connect(admin).burn(await user.getAddress(), creditAmount(1000)); + + // Check that the balance of the admin is 0 + expect(await credit.balanceOf(await user.getAddress())).to.equal(0); + }); + + it("should revert without BURNER_ROLE", async function () { + // Transfer 1000 Credit to user + await credit.connect(admin).mint(await admin.getAddress(), creditAmount(1000)); + await credit.connect(admin).grantRole(await credit.TRANSFER_ALLOWED_ROLE(), await user.getAddress()); + await credit.connect(admin).transfer(await user.getAddress(), creditAmount(1000)); + + // Burn 1000 Credit from user + const revertString = new RegExp(`AccessControl: account ${await admin.getAddress()} is missing role ${await credit.BURNER_ROLE()}`, 'i'); + await expect(credit.connect(admin).burn(await admin.getAddress(), creditAmount(1000))).to.be.revertedWith(revertString); + }); + + it("should revert when token holder does not have TRANSFER_ALLOWED_ROLE", async function () { + // Transfer 1000 Credit to user + await credit.connect(admin).mint(await admin.getAddress(), creditAmount(1000)); + // Note: only either of sender or recipient needs to have TRANSFER_ALLOWED_ROLE + // so in this case, admin has TRANSFER_ALLOWED_ROLE so no need for user + await credit.connect(admin).transfer(await user.getAddress(), creditAmount(1000)); + + // Grant `BURNER_ROLE` to admin + await credit.connect(admin).grantRole(await credit.BURNER_ROLE(), await admin.getAddress()); + + // Burn `1000` Credit from user + await expect(credit.connect(admin).burn(await user.getAddress(), creditAmount(1000))).to.be.revertedWithCustomError(credit, "OnlyTransferAllowedRole"); + }); + + it("should revert when token holder has insufficient balance", async function () { + // Transfer 1000 Credit to user + await credit.connect(admin).mint(await admin.getAddress(), creditAmount(1000)); + await credit.connect(admin).grantRole(await credit.TRANSFER_ALLOWED_ROLE(), await user.getAddress()); + await credit.connect(admin).transfer(await user.getAddress(), creditAmount(1000)); + + // Grant `BURNER_ROLE` to admin + await credit.connect(admin).grantRole(await credit.BURNER_ROLE(), await admin.getAddress()); + + // Revoke `TRANSFER_ALLOWED_ROLE` from user + await credit.connect(admin).revokeRole(await credit.TRANSFER_ALLOWED_ROLE(), await user.getAddress()); + + const revertString = "ERC20: burn amount exceeds balance"; + await expect(credit.connect(admin).burn(await admin.getAddress(), creditAmount(1001))).to.be.revertedWith(revertString); + }); + }); + }); + + describe("Redeem And Burn", function () { + beforeEach(async function () { + // Grant `MINTER_ROLE` and `TRANSFER_ALLOWED_ROLE` to admin + await credit.connect(admin).grantRole(await credit.MINTER_ROLE(), await admin.getAddress()); + await credit.connect(admin).grantRole(await credit.TRANSFER_ALLOWED_ROLE(), await admin.getAddress()); + + // Mint 1000 Credit to admin + await credit.connect(admin).mint(await admin.getAddress(), creditAmount(1000)); + + // Transfer 5000 USDC to admin + const usdcAmount = ethers.utils.parseUnits("5000", "6"); + await usdc.connect(admin).transfer(await admin.getAddress(), usdcAmount); + }); + + it("should redeem and burn", async function () { + // Transfer 5000 USDC to Credit contract + await usdc.connect(admin).transfer(credit.address, ethers.utils.parseUnits("5000", "6")); + + // Transfer 1000 Credit to user + await credit.connect(admin).transfer(await user.getAddress(), creditAmount(1000)); + + // Grant `TRANSFER_ALLOWED_ROLE` to user + await credit.connect(admin).grantRole(await credit.TRANSFER_ALLOWED_ROLE(), await user.getAddress()); + + // Grant `REDEEMER_ROLE` to user + await credit.connect(admin).grantRole(await credit.REDEEMER_ROLE(), await user.getAddress()); + + // Redeem and burn 1000 Credit from user + await credit.connect(user).redeemAndBurn(await user.getAddress(), creditAmount(1000)); + + // Check that Credit balance of the user is 0 + expect(await credit.balanceOf(await user.getAddress())).to.equal(0); + // Check that USDC balance of the Credit contract is 1000 + expect(await usdc.balanceOf(await user.getAddress())).to.equal(creditAmount(1000)); + // Check that USDC balance of Credit has decreased by 1000 (from 5000 to 4000) + expect(await usdc.balanceOf(credit.address)).to.equal(ethers.utils.parseUnits("4000", "6")); + }); + + it("should revert when redeemer does not have `REDEEMER_ROLE`", async function () { + // Transfer 5000 USDC to Credit contract + await usdc.connect(admin).transfer(credit.address, ethers.utils.parseUnits("5000", "6")); + + // Transfer 1000 Credit to user + await credit.connect(admin).transfer(await user.getAddress(), creditAmount(1000)); + + // Grant `TRANSFER_ALLOWED_ROLE` to user + await credit.connect(admin).grantRole(await credit.TRANSFER_ALLOWED_ROLE(), await user.getAddress()); + + //! Grant `REDEEMER_ROLE` to user + // await credit.connect(admin).grantRole(await credit.REDEEMER_ROLE(), await user.getAddress()); + + // Redeem and burn 1000 Credit from user + const revertString = new RegExp(`AccessControl: account ${await user.getAddress()} is missing role ${await credit.REDEEMER_ROLE()}`, 'i'); + await expect(credit.connect(user).redeemAndBurn(await user.getAddress(), creditAmount(1000))).to.be.revertedWith(revertString); + }); + + it("should revert when token holder does not have `TRANSFER_ALLOWED_ROLE`", async function () { + // Transfer 5000 USDC to Credit contract + await usdc.connect(admin).transfer(credit.address, ethers.utils.parseUnits("5000", "6")); + + // Transfer 1000 Credit to user + await credit.connect(admin).transfer(await user.getAddress(), creditAmount(1000)); + + //! Grant `TRANSFER_ALLOWED_ROLE` to user + // await credit.connect(admin).grantRole(await credit.TRANSFER_ALLOWED_ROLE(), await user.getAddress()); + + // Grant `REDEEMER_ROLE` to user + await credit.connect(admin).grantRole(await credit.REDEEMER_ROLE(), await user.getAddress()); + + // Redeem and burn 1000 Credit from user + await expect(credit.connect(user).redeemAndBurn(await user.getAddress(), creditAmount(1000))).to.be.revertedWithCustomError(credit, "OnlyTransferAllowedRole"); + }); + + it("should revert when token holder does not have enough Credit balance", async function () { + // Transfer 5000 USDC to Credit contract + await usdc.connect(admin).transfer(credit.address, ethers.utils.parseUnits("5000", "6")); + + //! Transfer 100 Credit to user + await credit.connect(admin).transfer(await user.getAddress(), creditAmount(100)); + + // Grant `TRANSFER_ALLOWED_ROLE` to user + await credit.connect(admin).grantRole(await credit.TRANSFER_ALLOWED_ROLE(), await user.getAddress()); + + // Grant `REDEEMER_ROLE` to user + await credit.connect(admin).grantRole(await credit.REDEEMER_ROLE(), await user.getAddress()); + + // Redeem and burn 1000 Credit from user + const revertString = "ERC20: burn amount exceeds balance"; + await expect(credit.connect(user).redeemAndBurn(await user.getAddress(), creditAmount(1000))).to.be.revertedWith(revertString); + }); + + it("should revert when Credit contract does not have enough USDC balance", async function () { + //! Transfer 500 USDC to Credit contract + await usdc.connect(admin).transfer(credit.address, ethers.utils.parseUnits("500", "6")); + + // Transfer 100 Credit to user + await credit.connect(admin).transfer(await user.getAddress(), creditAmount(100)); + + // Grant `TRANSFER_ALLOWED_ROLE` to user + await credit.connect(admin).grantRole(await credit.TRANSFER_ALLOWED_ROLE(), await user.getAddress()); + + // Grant `REDEEMER_ROLE` to user + await credit.connect(admin).grantRole(await credit.REDEEMER_ROLE(), await user.getAddress()); + + // Redeem and burn 1000 Credit from user + const revertString = "ERC20: transfer amount exceeds balance"; + await expect(credit.connect(user).redeemAndBurn(await user.getAddress(), creditAmount(1000))).to.be.revertedWith(revertString); + }); + }); + + describe("Pause/Unpause", function () { + beforeEach(async function () { + await credit.connect(admin).grantRole(await credit.PAUSER_ROLE(), await admin.getAddress()); + await credit.connect(admin).pause(); + }); + + it("should revert when calling mint", async function () { + await credit.connect(admin).grantRole(await credit.MINTER_ROLE(), await admin.getAddress()); + + const revertString = "Pausable: paused"; + await expect(credit.connect(admin).mint(await admin.getAddress(), creditAmount(1000))).to.be.revertedWith(revertString); + }); + + it("should revert when calling burn", async function () { + await credit.connect(admin).grantRole(await credit.BURNER_ROLE(), await admin.getAddress()); + + const revertString = "Pausable: paused"; + await expect(credit.connect(admin).burn(await admin.getAddress(), creditAmount(1000))).to.be.revertedWith(revertString); + }); + + it("should revert when calling redeemAndBurn", async function () { + await credit.connect(admin).grantRole(await credit.REDEEMER_ROLE(), await admin.getAddress()); + + const revertString = "Pausable: paused"; + await expect(credit.connect(admin).redeemAndBurn(await admin.getAddress(), creditAmount(1000))).to.be.revertedWith(revertString); + }); + + it("emergency withdraw should be possible", async function () { + const ADMIN_USDC_BALANCE_BEFORE = await usdc.balanceOf(await admin.getAddress()); + await credit.connect(admin).grantRole(await credit.EMERGENCY_WITHDRAW_ROLE(), await admin.getAddress()); + + // Transfer 5000 USDC to Credit contract + const USDC_AMOUNT = ethers.utils.parseUnits("5000", "6"); + await usdc.connect(admin).transfer(credit.address, USDC_AMOUNT); + + expect(await usdc.balanceOf(credit.address)).to.equal(USDC_AMOUNT); + expect(await usdc.balanceOf(await admin.getAddress())).to.equal(ADMIN_USDC_BALANCE_BEFORE.sub(USDC_AMOUNT)); + + // Emergency withdraw + await credit.connect(admin).emergencyWithdraw(usdc.address, await admin.getAddress(), USDC_AMOUNT); + + // Check that USDC balance of the admin is restored + expect(await usdc.balanceOf(await admin.getAddress())).to.equal(ADMIN_USDC_BALANCE_BEFORE); + // Check that USDC balance of the Credit contract is 0 + expect(await usdc.balanceOf(credit.address)).to.equal(0); + }); + }); + + describe("Emergency Withdraw", function () { + it("should revert when not admin", async function () { + await expect(credit.connect(user).emergencyWithdraw(usdc.address, await user.getAddress(), ethers.utils.parseUnits("5000", "6"))).to.be.revertedWithCustomError(credit, "OnlyAdmin"); + }); + }); + + describe("Emergency Withdraw", function () { + it("should withdraw USDC", async function () { + const ADMIN_USDC_BALANCE_BEFORE = await usdc.balanceOf(await admin.getAddress()); + + // Grant `EMERGENCY_WITHDRAW_ROLE` to admin + await credit.connect(admin).grantRole(await credit.EMERGENCY_WITHDRAW_ROLE(), await admin.getAddress()); + + // Transfer 5000 USDC to Credit contract + const USDC_AMOUNT = ethers.utils.parseUnits("5000", "6"); + await usdc.connect(admin).transfer(credit.address, USDC_AMOUNT); + + // Emergency withdraw + await credit.connect(admin).emergencyWithdraw(usdc.address, await admin.getAddress(), USDC_AMOUNT); + + // Check that USDC balance of the admin is restored + expect(await usdc.balanceOf(await admin.getAddress())).to.equal(ADMIN_USDC_BALANCE_BEFORE); + // Check that USDC balance of the Credit contract is 0 + expect(await usdc.balanceOf(credit.address)).to.equal(0); + }); + + it("should revert when not admin", async function () { + // Grant `EMERGENCY_WITHDRAW_ROLE` to admin + await credit.connect(admin).grantRole(await credit.EMERGENCY_WITHDRAW_ROLE(), await admin.getAddress()); + + // Transfer 5000 USDC to Credit contract + const USDC_AMOUNT = ethers.utils.parseUnits("5000", "6"); + await usdc.connect(admin).transfer(credit.address, USDC_AMOUNT); + + //! user calls Emergency withdraw + await expect(credit.connect(user).emergencyWithdraw(usdc.address, await admin.getAddress(), USDC_AMOUNT)).to.be.revertedWithCustomError(credit, "OnlyAdmin"); + }); + + it("should revert when recipient does not have `EMERGENCY_WITHDRAW_ROLE`", async function () { + // Grant `EMERGENCY_WITHDRAW_ROLE` to admin + await credit.connect(admin).grantRole(await credit.EMERGENCY_WITHDRAW_ROLE(), await admin.getAddress()); + + // Transfer 5000 USDC to Credit contract + const USDC_AMOUNT = ethers.utils.parseUnits("5000", "6"); + await usdc.connect(admin).transfer(credit.address, USDC_AMOUNT); + + // Emergency withdraw to user + await expect(credit.connect(admin).emergencyWithdraw(usdc.address, await user.getAddress(), USDC_AMOUNT)).to.be.revertedWithCustomError(credit, "OnlyToEmergencyWithdrawRole"); + }); + }); +}); From 2a4121ccd8d14c44d917140a56adec097da9340f Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 26 Mar 2025 10:24:55 +0900 Subject: [PATCH 3/6] move pause/unpause below --- contracts/token/Credit.sol | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/contracts/token/Credit.sol b/contracts/token/Credit.sol index 43032d2..fe28e7d 100644 --- a/contracts/token/Credit.sol +++ b/contracts/token/Credit.sol @@ -71,14 +71,6 @@ contract Credit is require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), OnlyAdmin()); } - function pause() external onlyRole(PAUSER_ROLE) { - _pause(); - } - - function unpause() external onlyRole(PAUSER_ROLE) { - _unpause(); - } - //-------------------------------- Overrides end --------------------------------// /// @custom:oz-upgrades-unsafe-allow state-variable-immutable @@ -127,6 +119,18 @@ contract Credit is //-------------------------------- Oyster Market end --------------------------------// + //-------------------------------- Pause/Unpause start --------------------------------// + + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + function unpause() external onlyRole(PAUSER_ROLE) { + _unpause(); + } + + //-------------------------------- Pause/Unpause end --------------------------------// + //-------------------------------- Emergency Withdraw start --------------------------------// function emergencyWithdraw(address _token, address _to, uint256 _amount) external onlyAdmin { From f5f24f05fdc03592044e972440ccedd839491412 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 26 Mar 2025 10:34:16 +0900 Subject: [PATCH 4/6] add comments --- contracts/token/Credit.sol | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/contracts/token/Credit.sol b/contracts/token/Credit.sol index fe28e7d..38480f1 100644 --- a/contracts/token/Credit.sol +++ b/contracts/token/Credit.sol @@ -18,6 +18,12 @@ import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +/** + * @title Credit + * @notice To transfer Credit tokens, either the sender or the recipient must have `TRANSFER_ALLOWED_ROLE`. + * @dev Admin must track the balance of USDC in the contract compared to the total supply of Credit. + */ + contract Credit is ContextUpgradeable, // _msgSender, _msgData AccessControlEnumerableUpgradeable, // RBAC enumeration @@ -100,10 +106,22 @@ contract Credit is //-------------------------------- Token Mint/Burn start --------------------------------/ + /** + * @notice Mint Credit tokens. + * @dev Caller must have `MINTER_ROLE`. + * @param _to Address to mint tokens to. Must have `TRANSFER_ALLOWED_ROLE`. + * @param _amount Amount of tokens to mint. + */ function mint(address _to, uint256 _amount) external whenNotPaused onlyRole(MINTER_ROLE) { _mint(_to, _amount); } + /** + * @notice Burn Credit tokens. + * @dev Caller must have `BURNER_ROLE`. + * @param _from Address to burn tokens from. Must have `TRANSFER_ALLOWED_ROLE` + * @param _amount Amount of tokens to burn. + */ function burn(address _from, uint256 _amount) external whenNotPaused onlyRole(BURNER_ROLE) { _burn(_from, _amount); } @@ -112,9 +130,17 @@ contract Credit is //-------------------------------- Oyster Market start --------------------------------// + /** + * @notice Burn Credit tokens and receive USDC. + * `_amount` of Credit tokens will be burned and `_amount` of USDC will be sent to `_to`. + * @dev Caller must have `REDEEMER_ROLE`. + * @dev Can revert if `Credit` contract does not have enough balance of USDC. + * @param _to Address to receive USDC. + * @param _amount Amount of tokens to redeem. + */ function redeemAndBurn(address _to, uint256 _amount) external whenNotPaused onlyRole(REDEEMER_ROLE) { + _burn(_msgSender(), _amount); IERC20(USDC).safeTransfer(_to, _amount); - _burn(_msgSender(), _amount); } //-------------------------------- Oyster Market end --------------------------------// @@ -133,6 +159,14 @@ contract Credit is //-------------------------------- Emergency Withdraw start --------------------------------// + /** + * @notice Emergency withdraw tokens from the contract. + * @dev Caller must have `DEFAULT_ADMIN_ROLE` + * and `_to` address must have `EMERGENCY_WITHDRAW_ROLE`. + * @param _token Address of the token to withdraw. + * @param _to Address to receive the tokens. Must have `EMERGENCY_WITHDRAW_ROLE`. + * @param _amount Amount of tokens to withdraw. + */ function emergencyWithdraw(address _token, address _to, uint256 _amount) external onlyAdmin { require(hasRole(EMERGENCY_WITHDRAW_ROLE, _to), OnlyToEmergencyWithdrawRole()); IERC20(_token).safeTransfer(_to, _amount); From b8e73b0b6321735141cb6cebbd88a5296659c760 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 26 Mar 2025 14:22:26 +0900 Subject: [PATCH 5/6] test: fix failing test --- test/token/Credit.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/token/Credit.ts b/test/token/Credit.ts index 5e70e22..221dcdc 100644 --- a/test/token/Credit.ts +++ b/test/token/Credit.ts @@ -277,8 +277,8 @@ describe("Credit", function () { //! Transfer 500 USDC to Credit contract await usdc.connect(admin).transfer(credit.address, ethers.utils.parseUnits("500", "6")); - // Transfer 100 Credit to user - await credit.connect(admin).transfer(await user.getAddress(), creditAmount(100)); + // Transfer 1000 Credit to user + await credit.connect(admin).transfer(await user.getAddress(), creditAmount(1000)); // Grant `TRANSFER_ALLOWED_ROLE` to user await credit.connect(admin).grantRole(await credit.TRANSFER_ALLOWED_ROLE(), await user.getAddress()); From c38ff8160e86ff45275cbb2527a529c29ede2019 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 26 Mar 2025 20:37:03 +0900 Subject: [PATCH 6/6] Upgrade: Upgrade MarketV1 in Arb Sepolia --- addresses/421614.json | 2 +- contracts/enclaves/MarketV1.sol | 8 ++++---- scripts/deploy/enclaves/UpgradeMarketV1.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/addresses/421614.json b/addresses/421614.json index b110ac2..804e7a4 100644 --- a/addresses/421614.json +++ b/addresses/421614.json @@ -5,7 +5,7 @@ "credit": "0x1343d88885eE888CEe79FEb3DfD0C5fC8fd65Af1" }, "implementation": { - "marketV1": "0x573B6a07d1Eb414B9CED0707DEca19b07798D105", + "marketV1": "0xC1162a62E48Dc8bb215Bd445D724c8f4EeF59586", "credit": "0x587A50Fd13161503384A2a431b838868Db0b3b39" } } \ No newline at end of file diff --git a/contracts/enclaves/MarketV1.sol b/contracts/enclaves/MarketV1.sol index 99cd501..18bf796 100644 --- a/contracts/enclaves/MarketV1.sol +++ b/contracts/enclaves/MarketV1.sol @@ -197,9 +197,9 @@ contract MarketV1 is event CreditTokenUpdated(address indexed oldCreditToken, address indexed newCreditToken); event NoticePeriodUpdated(uint256 noticePeriod); - event JobOpened(bytes32 indexed jobId, string metadata, address indexed owner, address indexed provider); + event JobOpened(bytes32 indexed jobId, string metadata, address indexed owner, address indexed provider, uint256 timestamp); event JobSettled(bytes32 indexed jobId, uint256 lastSettled); - event JobClosed(bytes32 indexed jobId); + event JobClosed(bytes32 indexed jobId, uint256 timestamp); event JobDeposited(bytes32 indexed jobId, address indexed token, address indexed from, uint256 amount); event JobWithdrawn(bytes32 indexed jobId, address indexed token, address indexed to, uint256 amount); event JobSettlementWithdrawn( @@ -275,7 +275,7 @@ contract MarketV1 is // create job with initial balance 0 jobs[jobId] = Job(_metadata, _owner, _provider, 0, 0, block.timestamp); - emit JobOpened(jobId, _metadata, _owner, _provider); + emit JobOpened(jobId, _metadata, _owner, _provider, block.timestamp); // deposit initial balance _deposit(jobId, _msgSender(), _balance); @@ -311,7 +311,7 @@ contract MarketV1 is } delete jobs[_jobId]; - emit JobClosed(_jobId); + emit JobClosed(_jobId, block.timestamp); } function _jobDeposit(bytes32 _jobId, uint256 _amount) internal { diff --git a/scripts/deploy/enclaves/UpgradeMarketV1.ts b/scripts/deploy/enclaves/UpgradeMarketV1.ts index 0a1361e..187e1e8 100644 --- a/scripts/deploy/enclaves/UpgradeMarketV1.ts +++ b/scripts/deploy/enclaves/UpgradeMarketV1.ts @@ -36,10 +36,10 @@ async function deployAndUpgradeMarketV1() { const marketV1UpgradeTx = await marketV1Proxy.connect(admin).upgradeTo(newMarketV1Impl.address); await marketV1UpgradeTx.wait(); - // Reinitialize MarketV1 (noticePeriod, creditToken) - const marketV1 = MarketV1__factory.connect(marketV1Proxy.address, admin); - const reinitializeTx = await marketV1.connect(admin).reinitialize(FIVE_MINUTES, addresses.proxy.credit); - await reinitializeTx.wait(); + // // Reinitialize MarketV1 (noticePeriod, creditToken) + // const marketV1 = MarketV1__factory.connect(marketV1Proxy.address, admin); + // const reinitializeTx = await marketV1.connect(admin).reinitialize(FIVE_MINUTES, addresses.proxy.credit); + // await reinitializeTx.wait(); /*////////////////////////////////////////////////////////////// VERIFY CONTRACTS