From d2eafc6dbcfed4bc584348e962250c663ae5cf7f Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 25 Mar 2025 22:30:02 +0900 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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/9] 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/9] 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 From c72a171ddb8b8ab74187be64ef1ef3a16b34cc80 Mon Sep 17 00:00:00 2001 From: Prateek Date: Mon, 12 May 2025 14:46:15 +0400 Subject: [PATCH 7/9] feat: max rate is used to pay for notice period --- contracts/enclaves/MarketV1.sol | 46 +++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/contracts/enclaves/MarketV1.sol b/contracts/enclaves/MarketV1.sol index 18bf796..ad32ea6 100644 --- a/contracts/enclaves/MarketV1.sol +++ b/contracts/enclaves/MarketV1.sol @@ -181,6 +181,7 @@ contract MarketV1 is uint256 rate; uint256 balance; uint256 lastSettled; // payment has been settled up to this timestamp + uint256 maxRate; // max rate for the job } mapping(bytes32 => Job) public jobs; @@ -250,14 +251,16 @@ contract MarketV1 is function _emergencyWithdrawCredit(address _to, bytes32[] calldata _jobIds) internal { require(hasRole(EMERGENCY_WITHDRAW_ROLE, _to), "only to emergency withdraw role"); - uint256 settleTill = block.timestamp + noticePeriod; - for (uint256 i = 0; i < _jobIds.length; i++) { bytes32 jobId = _jobIds[i]; - _jobSettle(jobId, jobs[jobId].rate, settleTill); + _jobSettle(jobId, jobs[jobId].rate); uint256 creditBalance = jobCreditBalance[jobId]; if (creditBalance > 0) { - _withdraw(jobId, _to, creditBalance); + jobs[jobId].balance -= creditBalance; + // set job credit balance to 0 + jobCreditBalance[jobId] = 0; + creditToken.safeTransfer(_to, creditBalance); + emit JobWithdrawn(jobId, address(creditToken), _to, creditBalance); } } } @@ -274,7 +277,7 @@ contract MarketV1 is bytes32 jobId = bytes32(_jobIndex); // create job with initial balance 0 - jobs[jobId] = Job(_metadata, _owner, _provider, 0, 0, block.timestamp); + jobs[jobId] = Job(_metadata, _owner, _provider, 0, 0, block.timestamp, 0); emit JobOpened(jobId, _metadata, _owner, _provider, block.timestamp); // deposit initial balance @@ -284,25 +287,28 @@ contract MarketV1 is _jobReviseRate(jobId, _rate); } - function _jobSettle(bytes32 _jobId, uint256 _rate, uint256 _settleTill) internal returns (bool isBalanceEnough) { + function _jobSettle(bytes32 _jobId, uint256 _rate) internal returns (bool isBalanceEnough) { uint256 lastSettled = jobs[_jobId].lastSettled; - if (_settleTill == lastSettled) return true; - require(_settleTill > lastSettled, "cannot settle before lastSettled"); + if (block.timestamp <= lastSettled) { + return true; + } + require(jobs[_jobId].balance > 0, "insufficient funds to settle"); + require(jobs[_jobId].rate > 0, "invalid rate"); - uint256 usageDuration = _settleTill - lastSettled; + uint256 usageDuration = block.timestamp - lastSettled; uint256 amountUsed = _calcAmountUsed(_rate, usageDuration); uint256 settleAmount = _min(amountUsed, jobs[_jobId].balance); _settle(_jobId, settleAmount); - jobs[_jobId].lastSettled = _settleTill; - emit JobSettled(_jobId, _settleTill); + jobs[_jobId].lastSettled = block.timestamp; + emit JobSettled(_jobId, block.timestamp); isBalanceEnough = amountUsed <= settleAmount; } function _jobClose(bytes32 _jobId) internal { // deduct shutdown delay cost - _jobSettle(_jobId, jobs[_jobId].rate, block.timestamp + noticePeriod); + _jobSettle(_jobId, jobs[_jobId].rate); // refund leftover balance uint256 _balance = jobs[_jobId].balance; @@ -316,14 +322,14 @@ contract MarketV1 is function _jobDeposit(bytes32 _jobId, uint256 _amount) internal { require(_amount > 0, "invalid amount"); - require(_jobSettle(_jobId, jobs[_jobId].rate, block.timestamp + noticePeriod), "insufficient funds to deposit"); + require(_jobSettle(_jobId, jobs[_jobId].rate), "insufficient funds to deposit"); _deposit(_jobId, _msgSender(), _amount); } function _jobWithdraw(bytes32 _jobId, uint256 _amount) internal { require(_amount > 0, "invalid amount"); - require(_jobSettle(_jobId, jobs[_jobId].rate, block.timestamp + noticePeriod), "insufficient funds to withdraw"); + require(_jobSettle(_jobId, jobs[_jobId].rate), "insufficient funds to withdraw"); // withdraw _withdraw(_jobId, _msgSender(), _amount); @@ -336,7 +342,7 @@ contract MarketV1 is uint256 lastSettled = jobs[_jobId].lastSettled; if (block.timestamp > lastSettled) { require( - _jobSettle(_jobId, jobs[_jobId].rate, block.timestamp), + _jobSettle(_jobId, jobs[_jobId].rate), "insufficient funds to settle before revising rate" ); } @@ -349,7 +355,13 @@ contract MarketV1 is // deduct shutdown delay cost // higher rate is used to calculate shutdown delay cost uint256 higherRate = _max(oldRate, _newRate); - require(_jobSettle(_jobId, higherRate, block.timestamp + noticePeriod), "insufficient funds to revise rate"); + uint256 prevHighestRate = jobs[_jobId].maxRate; + if (higherRate > prevHighestRate) { + jobs[_jobId].maxRate = higherRate; + uint256 noticePeriodExtraCost = _calcAmountUsed((higherRate - prevHighestRate), noticePeriod); + require(jobs[_jobId].balance > noticePeriodExtraCost, "insufficient funds to revise rate"); + _settle(_jobId, noticePeriodExtraCost); + } } function _jobMetadataUpdate(bytes32 _jobId, string calldata _metadata) internal { @@ -388,7 +400,7 @@ contract MarketV1 is * @param _jobId The job to settle. */ function jobSettle(bytes32 _jobId) external onlyExistingJob(_jobId) { - _jobSettle(_jobId, jobs[_jobId].rate, block.timestamp); + _jobSettle(_jobId, jobs[_jobId].rate); } /** From 776f2bfc1917298026faf429d92d0335554bbbe0 Mon Sep 17 00:00:00 2001 From: Prateek Date: Mon, 12 May 2025 15:37:12 +0400 Subject: [PATCH 8/9] fix: tests for Market V1 --- test/enclaves/MarketV1.ts | 1585 +++++-------------------------------- 1 file changed, 197 insertions(+), 1388 deletions(-) diff --git a/test/enclaves/MarketV1.ts b/test/enclaves/MarketV1.ts index d42380d..50dfd51 100644 --- a/test/enclaves/MarketV1.ts +++ b/test/enclaves/MarketV1.ts @@ -312,8 +312,6 @@ describe("MarketV1", function () { let admin2: Signer; let INITIAL_JOB_INDEX: string; - let JOB_OPENED_TIMESTAMP: number; - before(async function () { signers = await ethers.getSigners(); @@ -475,7 +473,8 @@ describe("MarketV1", function () { expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); expect(jobInfo.balance).to.equal(initialBalance.sub(noticePeriodCost)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + FIVE_MINUTES, INITIAL_TIMESTAMP + FIVE_MINUTES + 1); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP, INITIAL_TIMESTAMP + 1); + expect(jobInfo.maxRate).to.equal(JOB_RATE_1); expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(initialBalance)); expect(await token.balanceOf(marketv1.address)).to.equal(initialBalance.sub(noticePeriodCost)); @@ -528,7 +527,7 @@ describe("MarketV1", function () { expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); expect(jobInfo.balance).to.equal(initialBalance.sub(noticePeriodCost)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + FIVE_MINUTES, INITIAL_TIMESTAMP + FIVE_MINUTES + 1); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP, INITIAL_TIMESTAMP + 1); expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal(initialBalance.sub(noticePeriodCost)); }); @@ -569,10 +568,7 @@ describe("MarketV1", function () { describe("Job Settle", function () { const initialDeposit = usdc(50); const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - - beforeEach(async () => { - JOB_OPENED_TIMESTAMP = (await ethers.provider.getBlock('latest')).timestamp; - }); + let jobOpenActualTimestamp: number; takeSnapshotBeforeAndAfterEveryTest(async () => { }); @@ -583,40 +579,55 @@ describe("MarketV1", function () { await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + jobOpenActualTimestamp = (await ethers.provider.getBlock('latest')).timestamp; }); describe("CASE1: Settle Job immediately after Job Open", function () { - it("should revert before lastSettled", async () => { - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + it("should have balance and lastSettled reflecting initial state after open", async () => { + const jobBeforeSettle = await marketv1.jobs(INITIAL_JOB_INDEX); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + + expect(jobInfo.balance).to.equal(initialBalance); + expect(jobInfo.lastSettled).to.be.closeTo(jobOpenActualTimestamp, 2); }); }); describe("CASE2: Settle Job 2 minutes after Job Open", function () { - it("should revert before lastSettled", async () => { - const TWO_MINUTES = 60 * 2; - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + it("should settle for 2 minutes", async () => { + const DURATION_TO_SETTLE = TWO_MINUTES; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE))); }); }); describe("CASE3: Settle Job 1 second before notice period", function () { - it("should revert before lastSettled", async () => { - const jobOpenedTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - await time.increaseTo(jobOpenedTimestamp + NOTICE_PERIOD - 1); - - const lastSettled = (await marketv1.jobs(INITIAL_JOB_INDEX)).lastSettled; - expect(lastSettled).to.equal(jobOpenedTimestamp + NOTICE_PERIOD); - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + it("should settle for (notice period - 1 second)", async () => { + const DURATION_TO_SETTLE = NOTICE_PERIOD - 1; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); + + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE))); }); }); describe("CASE4: Settle Job 2 minutes after notice period", function () { - it("should spend notice period cost and 2 minutes worth tokens", async () => { - const TWO_MINUTES = 60 * 2; - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); + it("should spend for (notice period + 2 minutes)", async () => { + const DURATION_TO_SETTLE = NOTICE_PERIOD + TWO_MINUTES; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); - // Job Settle + const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); @@ -625,95 +636,112 @@ describe("MarketV1", function () { expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); - const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); + expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); - const lastSettledTimestampExpected = INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES; - expect(jobInfo.lastSettled).to.equal(lastSettledTimestampExpected); + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - // User balance const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); expect(await token.balanceOf(await user.getAddress())).to.equal(userBalanceExpected); - // Provider balance - const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + const providerBalanceExpected = noticeCostPaidAtOpen.add(amountPaidThisSettle); + expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); - // MarketV1 balance - const marketv1BalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + expect(await token.balanceOf(marketv1.address)).to.be.closeTo(jobBalanceExpected, 2); }); }); }); describe("Credit Only", function () { beforeEach(async () => { - // await token.connect(user).approve(marketv1.address, initialDeposit); await creditToken.connect(user).approve(marketv1.address, initialDeposit); await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + jobOpenActualTimestamp = (await ethers.provider.getBlock('latest')).timestamp; }); describe("CASE1: Settle Job immediately after Job Open", function () { - it("should revert before lastSettled", async () => { - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + it("should have balance and lastSettled reflecting initial state after open", async () => { + const jobBeforeSettle = await marketv1.jobs(INITIAL_JOB_INDEX); + const creditBalanceBeforeSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const creditBalanceAfterSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + expect(jobInfo.balance).to.equal(initialBalance); + expect(jobInfo.lastSettled).to.be.closeTo(jobOpenActualTimestamp, 2); + expect(creditBalanceAfterSettle).to.equal(creditBalanceBeforeSettle); + expect(creditBalanceAfterSettle).to.equal(initialBalance); }); }); describe("CASE2: Settle Job 2 minutes after Job Open", function () { - it("should revert before lastSettled", async () => { - const TWO_MINUTES = 60 * 2; - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + it("should settle for 2 minutes using credit", async () => { + const DURATION_TO_SETTLE = TWO_MINUTES; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); + + const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); + expect(jobCreditBal).to.equal(initialBalance.sub(amountToSettle)); }); }); describe("CASE3: Settle Job exactly after notice period", function () { - it("should revert before lastSettled", async () => { - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); + it("should settle for notice period duration using credit", async () => { + const DURATION_TO_SETTLE = NOTICE_PERIOD; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); + + const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); + expect(jobCreditBal).to.equal(initialBalance.sub(amountToSettle)); }); }); describe("CASE4: Settle Job 2 minutes after notice period", function () { - it("should spend notice period cost and 2 minutes worth tokens", async () => { - const TWO_MINUTES = 60 * 2; - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); + it("should spend notice period cost and 2 minutes worth tokens from credit", async () => { + const DURATION_TO_SETTLE = NOTICE_PERIOD + TWO_MINUTES; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); - // Job Settle + const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); - // Job Info After Settle const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); expect(jobInfo.metadata).to.equal("some metadata"); expect(jobInfo.owner).to.equal(await user.getAddress()); expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); - const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); + expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); + expect(jobCreditBal).to.be.closeTo(jobBalanceExpected, 2); - const lastSettledTimestampExpected = INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 3); - // User balance - const userTokenBalanceExpected = SIGNER1_INITIAL_FUND; - expect(await token.balanceOf(await user.getAddress())).to.be.within(userTokenBalanceExpected.sub(JOB_RATE_1), userTokenBalanceExpected.add(JOB_RATE_1)); - const userCreditBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); + const providerBalanceExpected = noticeCostPaidAtOpen.add(amountPaidThisSettle); + expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); - // Provider balance - const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - // MarketV1 balance - const marketv1TokenBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1TokenBalanceExpected.sub(JOB_RATE_1), marketv1TokenBalanceExpected.add(JOB_RATE_1)); - const marketv1CreditBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); - expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); + expect(await token.balanceOf(marketv1.address)).to.equal(0); + expect(await creditToken.balanceOf(marketv1.address)).to.be.closeTo(jobCreditBal, 2); }); }); }); @@ -725,1367 +753,148 @@ describe("MarketV1", function () { beforeEach(async () => { await token.connect(user).approve(marketv1.address, usdcDeposit); await creditToken.connect(user).approve(marketv1.address, creditDeposit); - // deposit 10 credit and 40 usdc await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, usdcDeposit.add(creditDeposit)); + jobOpenActualTimestamp = (await ethers.provider.getBlock('latest')).timestamp; }); describe("CASE1: Settle Job immediately after Job Open", function () { - it("should revert before lastSettled", async () => { - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + it("should have balance and lastSettled reflecting initial state after open", async () => { + const jobBeforeSettle = await marketv1.jobs(INITIAL_JOB_INDEX); + const creditBalanceBeforeSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const creditBalanceAfterSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + expect(jobInfo.balance).to.equal(initialBalance); + expect(jobInfo.lastSettled).to.be.closeTo(jobOpenActualTimestamp, 2); + expect(creditBalanceAfterSettle).to.equal(creditBalanceBeforeSettle); + expect(creditBalanceAfterSettle).to.equal(creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1))); }); }); describe("CASE2: Settle Job 2 minutes after Job Open", function () { - it("should revert before lastSettled", async () => { - const TWO_MINUTES = 60 * 2; - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + it("should settle for 2 minutes, using credit then USDC", async () => { + const DURATION_TO_SETTLE = TWO_MINUTES; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); + + const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + const initialJobCreditBalance = creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); + + let expectedCreditBalanceAfterSettle = initialJobCreditBalance.sub(amountToSettle); + if (expectedCreditBalanceAfterSettle.lt(0)) { + expectedCreditBalanceAfterSettle = BN.from(0); + } + expect(jobCreditBal).to.equal(expectedCreditBalanceAfterSettle); }); }); describe("CASE3: Settle Job exactly after notice period", function () { - it("should revert before lastSettled", async () => { - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); - - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + it("should settle for notice period, using credit then USDC", async () => { + const DURATION_TO_SETTLE = NOTICE_PERIOD; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); + + const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + const initialJobCreditBalance = creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); + + let expectedCreditBalanceAfterSettle = initialJobCreditBalance.sub(amountToSettle); + if (expectedCreditBalanceAfterSettle.lt(0)) { + expectedCreditBalanceAfterSettle = BN.from(0); + } + expect(jobCreditBal).to.equal(expectedCreditBalanceAfterSettle); }); }); - describe("CASE4: Settle Job 2 minutes after notice period - only Credit is settled", function () { - it("should settle notice period cost and 2 minutes worth tokens only from Credit", async () => { - const TWO_MINUTES = 60 * 2; - const TIME_JOB_OPEN = (await ethers.provider.getBlock('latest')).timestamp; - await time.increaseTo(TIME_JOB_OPEN + NOTICE_PERIOD + TWO_MINUTES); - const TIME_JOB_SETTLE = (await ethers.provider.getBlock('latest')).timestamp; - const TIME_DIFF = TIME_JOB_SETTLE - TIME_JOB_OPEN; - - const noticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + describe("CASE4: Settle Job 2 minutes after notice period - credit might be used up", function () { + it("should settle, exhausting credit first, then USDC", async () => { + const DURATION_TO_SETTLE = NOTICE_PERIOD + TWO_MINUTES; + const TIME_JOB_SETTLE_TARGET = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(TIME_JOB_SETTLE_TARGET); + + const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); + const initialJobCreditBalance = creditDeposit.sub(noticeCostPaidAtOpen); - // Job Settle await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); - /* Job Info After Settle */ const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const finalJobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); expect(jobInfo.owner).to.equal(await user.getAddress()); expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); - // Job Balance - const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - // Job Last Settled - const lastSettledTimestampExpected = TIME_JOB_OPEN + NOTICE_PERIOD + TWO_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - // Job Credit Balance - const amountSettledExpected = calcAmountToPay(JOB_RATE_1, TIME_DIFF); - const jobCreditBalanceExpected = creditDeposit.sub(amountSettledExpected); - expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.be.within(jobCreditBalanceExpected.sub(JOB_RATE_1), jobCreditBalanceExpected.add(JOB_RATE_1)); - - /* User balance */ - // User Token balance - const userTokenBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcDeposit); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userTokenBalanceExpected.sub(JOB_RATE_1), userTokenBalanceExpected.add(JOB_RATE_1)); - // User Credit balance - const userCreditBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); - - /* Provider balance */ - // Provider Token balance - const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - /* MarketV1 balance */ - // MarketV1 Token balance - const marketv1TokenBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1TokenBalanceExpected.sub(JOB_RATE_1), marketv1TokenBalanceExpected.add(JOB_RATE_1)); - // MarketV1 Credit balance - const marketv1CreditBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); - expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); + + const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); + expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); + + expect(jobInfo.lastSettled).to.be.closeTo(TIME_JOB_SETTLE_TARGET, 3); + + let expectedFinalCreditBalance = initialJobCreditBalance.sub(amountPaidThisSettle); + if (expectedFinalCreditBalance.lt(0)) { + expectedFinalCreditBalance = BN.from(0); + } + expect(finalJobCreditBalance).to.equal(expectedFinalCreditBalance); + + const providerPaymentThisSettle = amountPaidThisSettle; + const providerBalanceExpected = noticeCostPaidAtOpen.add(providerPaymentThisSettle); + expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); + + const expectedMarketV1UsdcBalance = jobBalanceExpected.sub(expectedFinalCreditBalance); + expect(await token.balanceOf(marketv1.address)).to.be.closeTo(expectedMarketV1UsdcBalance, 2); + expect(await creditToken.balanceOf(marketv1.address)).to.equal(expectedFinalCreditBalance); }); }); describe("CASE5: Settle Job 20 minutes after notice period - both Credit and USDC are settled", function () { it("should settle all Credits and some USDC", async () => { - const TWENTY_MINUTES = 60 * 20; - await time.increaseTo(JOB_OPENED_TIMESTAMP + NOTICE_PERIOD + TWENTY_MINUTES); - const TIME_JOB_SETTLE = (await ethers.provider.getBlock('latest')).timestamp; - const TIME_DIFF = TIME_JOB_SETTLE - JOB_OPENED_TIMESTAMP; + const DURATION_TO_SETTLE = NOTICE_PERIOD + 60 * 20; + const TIME_JOB_SETTLE_TARGET = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(TIME_JOB_SETTLE_TARGET); - const noticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); + const initialJobCreditBalance = creditDeposit.sub(noticeCostPaidAtOpen); - // Job Settle await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); - /* Job Info After Settle */ const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const finalJobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - // Job Balance - const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWENTY_MINUTES)); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - // Job Last Settled - const lastSettledTimestampExpected = JOB_OPENED_TIMESTAMP + NOTICE_PERIOD + TWENTY_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - // Job Credit Balance - const amountSettledExpected = calcAmountToPay(JOB_RATE_1, TIME_DIFF); - const jobCreditBalanceExpected = creditDeposit.sub(amountSettledExpected); - expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.be.within(jobCreditBalanceExpected.sub(JOB_RATE_1), jobCreditBalanceExpected.add(JOB_RATE_1)); - expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal(0); - - /* User balance */ - // User Token balance - const userTokenBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcDeposit); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userTokenBalanceExpected.sub(JOB_RATE_1), userTokenBalanceExpected.add(JOB_RATE_1)); - // User Credit balance - const userCreditBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); - - /* Provider balance */ - // Provider Token balance - const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - /* MarketV1 balance */ - // MarketV1 Token balance - const marketv1TokenBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1TokenBalanceExpected.sub(JOB_RATE_1), marketv1TokenBalanceExpected.add(JOB_RATE_1)); - // MarketV1 Credit balance - const marketv1CreditBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); - expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); - }); - }); - }); - }); - - describe("Job Deposit", function () { - takeSnapshotBeforeAndAfterEveryTest(async () => { }); - - describe("USDC Only", function () { - it("should deposit to job with USDC", async () => { - const initialDeposit = usdc(50); - const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - const additionalDepositAmount = usdc(25); - - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - - // Deposit 25 USDC - await marketv1 - .connect(signers[1]) - .jobDeposit(INITIAL_JOB_INDEX, additionalDepositAmount); - - // Job after deposit - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(addrs[1]); - expect(jobInfo.provider).to.equal(addrs[2]); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - expect(jobInfo.balance).to.equal(initialBalance.add(additionalDepositAmount)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + FIVE_MINUTES - 3, INITIAL_TIMESTAMP + FIVE_MINUTES + 3); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(additionalDepositAmount); - expect(await token.balanceOf(addrs[1])).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = initialBalance.add(additionalDepositAmount); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - - it("should revert when depositing to job without enough approved", async () => { - const initialDeposit = usdc(50); - const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - const additionalDepositAmount = usdc(25); - - await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - - // Deposit 25 USDC - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, additionalDepositAmount)).to.be.revertedWith("ERC20: insufficient allowance"); - }); - - it("should revert when depositing to job without enough balance", async () => { - const initialDeposit = SIGNER1_INITIAL_FUND; - const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - const additionalDepositAmount = usdc(25); - - // Open Job - await token.connect(user).approve(marketv1.address, SIGNER1_INITIAL_FUND.add(usdc(1000))); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - - // Deposit 25 USDC - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, additionalDepositAmount)).to.be.revertedWith("ERC20: transfer amount exceeds balance"); - }); - - it("should revert when depositing to never registered job", async () => { - await expect(marketv1 - .connect(user) - .jobDeposit(ethers.utils.hexZeroPad("0x01", 32), 25)).to.be.revertedWith("job not found"); - }); - - it("should revert when depositing to closed job", async () => { - const initialDeposit = usdc(50); - - // Job Open - await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - - // Job Close - await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); - - // Job Deposit - await expect(marketv1 - .connect(signers[1]) - .jobDeposit(INITIAL_JOB_INDEX, 25)).to.be.revertedWith("job not found"); - }); - }); - - describe("Credit Only", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const ADDITIONAL_DEPOSIT_AMOUNT = usdc(25); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - - it("should deposit to job with Credit", async () => { - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - - // Deposit 25 Credit - await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_DEPOSIT_AMOUNT); - await marketv1 - .connect(signers[1]) - .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT); - - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - // Job after deposit - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - expect(jobInfo.balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).add(ADDITIONAL_DEPOSIT_AMOUNT)); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + NOTICE_PERIOD); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).sub(ADDITIONAL_DEPOSIT_AMOUNT); - expect(await token.balanceOf(addrs[1])).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = INITIAL_DEPOSIT_AMOUNT.add(ADDITIONAL_DEPOSIT_AMOUNT); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - - it("should revert when depositing to job without approving both Credit and USDC", async () => { - const additionalDepositAmount = usdc(25); - - await token.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - - // Deposit without approving Credit - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT)).to.be.revertedWith("ERC20: insufficient allowance"); - }); - - it("should deposit USDC when Credit credit balance is not enough", async () => { - const initialDeposit = SIGNER1_INITIAL_FUND; - - // Open Job - await token.connect(user2).approve(marketv1.address, SIGNER1_INITIAL_FUND.add(usdc(1000))); - await marketv1 - .connect(user2) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - - // Approve 25 Credit without having enough Credit balance - await creditToken.connect(user2).approve(marketv1.address, ADDITIONAL_DEPOSIT_AMOUNT); - await expect(marketv1 - .connect(user2) - .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT)).to.be.revertedWith("ERC20: transfer amount exceeds balance"); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const creditBalance = await creditToken.balanceOf(await user2.getAddress()); - expect(jobInfo.balance).to.equal(initialDeposit.sub(NOTICE_PERIOD_COST)); - expect(creditBalance).to.equal(0); - }); - - it("should revert when depositing to never registered job", async () => { - await creditToken.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT)).to.be.revertedWith("job not found"); - }); - - it("should revert when depositing to closed job", async () => { - const initialDeposit = usdc(50); - - // Job Open - await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - - // Job Close - await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); - - // Job Deposit - await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_DEPOSIT_AMOUNT); - await expect(marketv1 - .connect(signers[1]) - .jobDeposit(INITIAL_JOB_INDEX, 25)).to.be.revertedWith("job not found"); - }); - }); - - describe("Both Credit and USDC", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - const TOTAL_ADITIONAL_DEPOSIT_AMOUNT = usdc(40); - const ADDITIONAL_CREDIT_DEPOSIT_AMOUNT = usdc(30); - const ADDITIONAL_USDC_DEPOSIT_AMOUNT = usdc(10); - const TOTAL_DEPOSIT_AMOUNT = INITIAL_DEPOSIT_AMOUNT.add(ADDITIONAL_USDC_DEPOSIT_AMOUNT).add(ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); - - it("should deposit 10 USDC and 30 Credit", async () => { - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - expect((await marketv1.jobs(INITIAL_JOB_INDEX)).balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); - - // Deposit 30 Credit and 10 USDC - await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); - await marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, TOTAL_ADITIONAL_DEPOSIT_AMOUNT); - - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - // Job after deposit - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const creditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - expect(jobInfo.balance).to.equal(TOTAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + NOTICE_PERIOD); - expect(creditBalance).to.equal(ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); - - // User Balance - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).sub(ADDITIONAL_USDC_DEPOSIT_AMOUNT).sub(ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); - expect(await token.balanceOf(addrs[1])).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - // MarketV1 Balance - const marketv1BalanceExpected = TOTAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - - it("should revert when depositing to job without enough USDC approved", async () => { - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - - // Deposit 30 Credit and 1000 USDC - await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT.add(SIGNER1_INITIAL_FUND))).to.be.revertedWith("ERC20: insufficient allowance"); - }); - - it("should revert when user does not have enough USDC balance", async () => { - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - - // Deposit 30 Credit and 1000 USDC - await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); - await token.connect(user).approve(marketv1.address, SIGNER1_INITIAL_FUND); - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT.add(SIGNER1_INITIAL_FUND))).to.be.revertedWith("ERC20: transfer amount exceeds balance"); - }); - - it("should revert when balance is below notice period cost", async () => { - // Open Job - await token.connect(user).approve(marketv1.address, SIGNER1_INITIAL_FUND.add(usdc(1000))); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, SIGNER1_INITIAL_FUND); - - // 100_000 seconds passed (spend 1000 usdc) - await time.increaseTo(INITIAL_TIMESTAMP + 100_000); - - // Deposit 25 USDC - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, usdc(25))).to.be.revertedWith("insufficient funds to deposit"); - }); - }); - }); - - describe("Job Withdraw", function () { - const TWO_MINUTES = 60 * 2; - const SEVEN_MINUTES = 60 * 7; - - takeSnapshotBeforeAndAfterEveryTest(async () => { }); - - describe("USDC Only", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - const TOTAL_WITHDRAW_AMOUNT = usdc(10); - - beforeEach(async () => { - // Deposit 50 USDC - await token.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - }); - - it("should withdraw from job immediately", async () => { - const withdrawAmount = usdc(10); - - // Job Withdraw - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, withdrawAmount); - - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - let jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - expect(jobInfo.balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawAmount)); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + NOTICE_PERIOD); - - expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(withdrawAmount)); - expect(await token.balanceOf(await provider.getAddress())).to.equal(NOTICE_PERIOD_COST); - expect(await token.balanceOf(marketv1.address)).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawAmount)); - }); - - it("should withdraw from job before lastSettled", async () => { - - // 2 minutes passed - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - - const providerBalanceBefore = await token.balanceOf(await provider.getAddress()); - - // Job Withdraw - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); // withdraw 10 USDC - - const SETTLED_AMOUNT = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); - - // Job info after Withdrawal - let jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + FIVE_MINUTES + TWO_MINUTES - 3, INITIAL_TIMESTAMP + FIVE_MINUTES + TWO_MINUTES + 3); - - // Check User USDC balance - const userUSDCBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userUSDCBalanceExpected.sub(JOB_RATE_1), userUSDCBalanceExpected.add(JOB_RATE_1)); - - // Check Provider USDC balance - const providerUSDCBalanceExpected = NOTICE_PERIOD_COST.add(SETTLED_AMOUNT); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerUSDCBalanceExpected.sub(JOB_RATE_1), providerUSDCBalanceExpected.add(JOB_RATE_1)); - - // Check MarketV1 USDC balance - const marketv1BalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - - it("should withdraw from job after lastSettled with settlement", async () => { - const settledAmountExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); - - // 7 minutes passed after Job Open - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); - - expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT)); - - const providerBalanceExpected = calcNoticePeriodCost(JOB_RATE_1).add(settledAmountExpected); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(settledAmountExpected); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - - it("should revert when withdrawing from non existent job", async () => { - const max_uint256_bytes32 = ethers.utils.hexZeroPad(ethers.constants.MaxUint256.toHexString(), 32); - - await expect(marketv1 - .connect(user) - .jobWithdraw(max_uint256_bytes32, usdc(100))).to.be.revertedWith("only job owner"); - }); - - it("should revert when withdrawing from third party job", async () => { - await expect(marketv1 - .connect(signers[3]) // neither owner nor provider - .jobWithdraw(INITIAL_JOB_INDEX, usdc(100))).to.be.revertedWith("only job owner"); - }); - - it("should revert when balance is below notice period cost", async () => { - // deposited 50 USDC - // 0.01 USDC/s - // notice period cost: 0.01 * 300 = 3 USDC - // 47 USDC left - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + 4500); // spend 45 USDC (300 + 4500 seconds passed) - - await expect(marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, usdc(1))).to.be.revertedWith("insufficient funds to withdraw"); - }); - - it("should revert when withdrawal request amount exceeds max withdrawable amount", async () => { - // Current balance: 47 USDC - - await expect(marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, usdc(48))).to.be.revertedWith("withdrawal amount exceeds job balance"); - }); - }); - - describe("Credit Only", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - const CREDIT_WITHDRAW_AMOUNT = usdc(10); - const TOTAL_WITHDRAW_AMOUNT = usdc(10); - - beforeEach(async () => { - // Deposit 50 Credit - await creditToken.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); - await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - }); - - it("should withdraw from job immediately", async () => { - // Job Withdraw - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, CREDIT_WITHDRAW_AMOUNT); // withdraw 10 Credit - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - expect(jobInfo.balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(CREDIT_WITHDRAW_AMOUNT)); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + NOTICE_PERIOD); - - expect(jobCreditBalance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(CREDIT_WITHDRAW_AMOUNT)); - - // User Balance - const userCreditBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(CREDIT_WITHDRAW_AMOUNT); - expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); - - // Provider Balance - const providerCreditBalanceExpected = 0; - const providerUSDCBalanceExpected = NOTICE_PERIOD_COST; - expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); - expect(await token.balanceOf(await provider.getAddress())).to.equal(providerUSDCBalanceExpected); - - const marketv1CreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(CREDIT_WITHDRAW_AMOUNT); - expect(await creditToken.balanceOf(marketv1.address)).to.equal(marketv1CreditBalanceExpected); - }); - - it("should withdraw from job before lastSettled", async () => { - // 2 minutes passed - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - const SETTLED_AMOUNT = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); - - // Job Withdraw - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, CREDIT_WITHDRAW_AMOUNT); // withdraw 10 Credit - - // Job info after Withdrawal - let jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + FIVE_MINUTES + TWO_MINUTES - 3, INITIAL_TIMESTAMP + FIVE_MINUTES + TWO_MINUTES + 3); - - const jobCreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); - expect(jobCreditBalance).to.be.within(jobCreditBalanceExpected.sub(JOB_RATE_1), jobCreditBalanceExpected.add(JOB_RATE_1)); - - // User Balance - const userCreditBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT); - const userUSDCBalanceExpected = 0; - expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); - expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND); - - // Provider Balance - const providerCreditBalanceExpected = 0; - const providerUSDCBalanceExpected = NOTICE_PERIOD_COST.add(SETTLED_AMOUNT); - expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerUSDCBalanceExpected.sub(JOB_RATE_1), providerUSDCBalanceExpected.add(JOB_RATE_1)); - - // Check MarketV1 USDC balance - const marketv1CreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); - const marketv1USDCBalanceExpected = 0; - expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.equal(marketv1USDCBalanceExpected); - }); - - it("should withdraw from job after lastSettled with settlement", async () => { - const settledAmountExpected = calcAmountToPay(JOB_RATE_1, SEVEN_MINUTES); - - // 7 minutes passed after Job Open - await time.increaseTo(INITIAL_TIMESTAMP + SEVEN_MINUTES); - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); - - - // User Balance - const userCreditBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT); - const userUSDCBalanceExpected = SIGNER1_INITIAL_FUND; - expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); - expect(await token.balanceOf(await user.getAddress())).to.equal(userUSDCBalanceExpected); - - // Provider Balance - const providerCreditBalanceExpected = 0; - const providerUSDCBalanceExpected = NOTICE_PERIOD_COST.add(settledAmountExpected); - expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerUSDCBalanceExpected.sub(JOB_RATE_1), providerUSDCBalanceExpected.add(JOB_RATE_1)); - - // Check MarketV1 Balance - const marketv1CreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(settledAmountExpected).sub(TOTAL_WITHDRAW_AMOUNT); - const marketv1USDCBalanceExpected = 0; - expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.equal(marketv1USDCBalanceExpected); - }); - }); - - describe("Both Credit and USDC", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const INITIAL_CREDIT_DEPOSIT_AMOUNT = usdc(40); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - const TOTAL_WITHDRAW_AMOUNT = usdc(20); // 13 USDC + 7 Credit - - beforeEach(async () => { - // Deposit 10 Credit, 40 USDC - await creditToken.connect(user).approve(marketv1.address, INITIAL_CREDIT_DEPOSIT_AMOUNT); - await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - }); - - it("should withdraw only USDC", async () => { - const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); - const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); - - const USDC_WITHDRAWAL_AMOUNT = usdc(5); - - // Job Withdraw - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, USDC_WITHDRAWAL_AMOUNT); // withdraw 5 usdc - - // Job Info - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(TOTAL_WITHDRAW_AMOUNT); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + NOTICE_PERIOD); - const jobCreditBalanceExpected = INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST); - expect(jobCreditBalance).to.equal(jobCreditBalanceExpected); - - // User Balance - const userCreditBalanceExpected = userCreditBalanceBefore; - const userUSDCBalanceExpected = userUSDCBalanceBefore.sub(USDC_WITHDRAWAL_AMOUNT); - expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userUSDCBalanceExpected.sub(JOB_RATE_1), userUSDCBalanceExpected.add(JOB_RATE_1)); - - // Provider Balance - const providerCreditBalanceExpected = 0; - const providerUSDCBalanceExpected = NOTICE_PERIOD_COST; - expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); - expect(await token.balanceOf(await provider.getAddress())).to.equal(providerUSDCBalanceExpected); - - const marketv1CreditBalanceExpected = INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST); - const marketv1USDCBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(INITIAL_CREDIT_DEPOSIT_AMOUNT).sub(USDC_WITHDRAWAL_AMOUNT); - expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1USDCBalanceExpected.sub(JOB_RATE_1), marketv1USDCBalanceExpected.add(JOB_RATE_1)); - }); - - it("should withdraw both USDC and Credit", async () => { - const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); - const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); - - const withdrawnUSDCAmountExpected = ((await marketv1.jobs(INITIAL_JOB_INDEX)).balance).sub(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)); - const withdrawnCreditAmountExpected = TOTAL_WITHDRAW_AMOUNT.sub(withdrawnUSDCAmountExpected); - // Job Withdraw - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); // withdraw 20 (13 USDC + 7 Credit) - - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - // Job Info - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(TOTAL_WITHDRAW_AMOUNT); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + NOTICE_PERIOD); - expect(jobCreditBalance).to.equal(INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawnCreditAmountExpected)); - - // User Balance - const userCreditBalanceExpected = userCreditBalanceBefore.add(withdrawnCreditAmountExpected); - const userUSDCBalanceExpected = userUSDCBalanceBefore.add(withdrawnUSDCAmountExpected); - expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); - expect(await token.balanceOf(await user.getAddress())).to.equal(userUSDCBalanceExpected); - - // Provider Balance - const providerCreditBalanceExpected = 0; - const providerUSDCBalanceExpected = NOTICE_PERIOD_COST; - expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); - expect(await token.balanceOf(await provider.getAddress())).to.equal(providerUSDCBalanceExpected); - - // MarketV1 Balance - const marketv1CreditBalanceExpected = INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawnCreditAmountExpected); - expect(await creditToken.balanceOf(marketv1.address)).to.equal(marketv1CreditBalanceExpected); - expect(await token.balanceOf(marketv1.address)).to.equal(0); - }); - }); - }); - - describe("Job Revise Rate", function () { - const JOB_LOWER_RATE = BN.from(5).e16().div(10); - const JOB_HIGHER_RATE = BN.from(2).e16(); - - const initialDeposit = usdc(50); - const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - - takeSnapshotBeforeAndAfterEveryTest(async () => { }); - - beforeEach(async () => { - await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - }); - - it("should revise rate higher", async () => { - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, JOB_HIGHER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(JOB_HIGHER_RATE); - expect(jobInfo.balance).to.equal(initialBalance); - - expect(jobInfo.lastSettled).to.equal(currentTimestamp + FIVE_MINUTES); - expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(initialDeposit)); - expect(await token.balanceOf(await provider.getAddress())).to.equal(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.equal(initialBalance); - }); - - it("should revise rate lower", async () => { - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, JOB_LOWER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(JOB_LOWER_RATE); - expect(jobInfo.balance).to.equal(initialBalance); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + FIVE_MINUTES); - expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(initialDeposit)); - expect(await token.balanceOf(await provider.getAddress())).to.equal(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.equal(initialBalance); - }); - - it("should revert when initiating rate revision for non existent job", async () => { - await expect(marketv1 - .connect(user) - .jobReviseRate(ethers.utils.hexZeroPad("0x01", 32), JOB_HIGHER_RATE)).to.be.revertedWith("only job owner"); - }); - - it("should revert when initiating rate revision for third party job", async () => { - await expect(marketv1 - .connect(signers[3]) // neither owner nor provider - .jobReviseRate(INITIAL_JOB_INDEX, JOB_HIGHER_RATE)).to.be.revertedWith("only job owner"); - }); - - const HIGHER_RATE = BN.from(2).e16(); // 0.02 USDC/s - const LOWER_RATE = BN.from(5).e15(); // 0.005 USDC/s - - describe("CASE 1: Revising Rate immediately after job open", function () { - - describe("when rate is higher", function () { - - it("should spend notice period cost only", async () => { - const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); - - // Job info after Rate Revision - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(HIGHER_RATE); - const jobBalanceExpected = initialBalance.sub(noticePeriodCostExpected); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(noticePeriodCostExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = noticePeriodCostExpected; - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - - describe("when rate is lower", function () { - it("should spend notice period cost only", async () => { - const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); - // Job info after Rate Revision - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(LOWER_RATE); - const jobBalanceExpected = initialDeposit.sub(noticePeriodCostExpected); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(noticePeriodCostExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = noticePeriodCostExpected; - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - }); - - describe("CASE 2: Revising Rate 2 minutes after job open", function () { - const TWO_MINUTES = 60 * 2; - const SEVEN_MINUTES = 60 * 7; - - describe("when rate is higher", function () { - it("should spend notice period cost + 3 minutes worth tokens with higher rate", async () => { - // 5 min * initial rate + 3 min * higher rate - const usdcSpentExpected = calcNoticePeriodCost(JOB_RATE_1).add(calcAmountToPay(HIGHER_RATE, TWO_MINUTES)); - const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(HIGHER_RATE); - - const jobBalanceExpected = initialDeposit.sub(usdcSpentExpected); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp + FIVE_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(usdcSpentExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = usdcSpentExpected; - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - - describe("when rate is lower", function () { - it("should spend notice period cost + 3 minutes worth tokens with initial rate", async () => { - // 5 min * initial rate + 3 min * initial rate - const usdcSpentExpected = calcNoticePeriodCost(JOB_RATE_1).add(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); + const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); + expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(LOWER_RATE); - - const jobBalanceExpected = initialDeposit.sub(usdcSpentExpected); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp + FIVE_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(usdcSpentExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = usdcSpentExpected; - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - }); - - describe("CASE 3: Revising Rate exactly after notice period", function () { - const TEN_MINUTES = 60 * 10; - - describe("when rate is higher", function () { - it("should spend 5 minutes worth tokens with initial rate and 5 minutes worth tokens with higher rate", async () => { - const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); - const secondNoticePeriodCost = calcNoticePeriodCost(HIGHER_RATE); - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(HIGHER_RATE); - - const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp + FIVE_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - - describe("when rate is lower", function () { - it("should spend 5 minutes worth tokens with initial rate and 5 minutes worth tokens with initial rate", async () => { - const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); - const secondNoticePeriodCost = calcNoticePeriodCost(LOWER_RATE); - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(LOWER_RATE); - - const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp + FIVE_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - }); - - describe("CASE 4: Revising Rate 2 minutes after notice period", function () { - const TWO_MINUTES = 60 * 2; - const TWELVE_MINUTES = 60 * 12; - - describe("when rate is higher", function () { - it("should spend 7 minutes worth tokens with initial rate and 5 minutes worth tokens with higher rate", async () => { - const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); - const secondNoticePeriodCost = calcNoticePeriodCost(HIGHER_RATE); - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(HIGHER_RATE); - - const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp + FIVE_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + expect(jobInfo.lastSettled).to.be.closeTo(TIME_JOB_SETTLE_TARGET, 3); - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - - describe("when rate is lower", function () { - it("should spend 7 minutes worth tokens with initial rate and 5 minutes worth tokens with initial rate", async () => { - const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); - const secondNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(LOWER_RATE); - - const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp + FIVE_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - }); - }); - - describe("Job Close", function () { - const initialDeposit = usdc(50); - const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - - takeSnapshotBeforeAndAfterEveryTest(async () => { }); - - describe("USDC Only", function () { - beforeEach(async () => { - await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - }); - - it("should close job", async () => { - // Job Close - await marketv1 - .connect(user) - .jobClose(INITIAL_JOB_INDEX); // here, user should get back (initial deposit - notice period cost) - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal(""); - expect(jobInfo.owner).to.equal(ethers.constants.AddressZero); - expect(jobInfo.provider).to.equal(ethers.constants.AddressZero); - expect(jobInfo.rate).to.equal(0); - expect(jobInfo.balance).to.equal(0); - expect(jobInfo.lastSettled).to.equal(0); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - }); - - it("should revert when closing non existent job", async () => { - await expect(marketv1 - .connect(user) - .jobClose(ethers.utils.hexZeroPad("0x01", 32))).to.be.revertedWith("only job owner"); - }); - - it("should revert when closing third party job", async () => { - await expect(marketv1 - .connect(signers[3]) // neither owner nor provider - .jobClose(INITIAL_JOB_INDEX)).to.be.revertedWith("only job owner"); - }); - - describe("Scenario 1: Closing Job immediately after opening", function () { - it("should spend notice period cost only", async () => { - await marketv1 - .connect(user) - .jobClose(INITIAL_JOB_INDEX); - - const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); - - // user balance after = initial fund - notice period cost - expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(noticePeriodCostExpected)); - // provider balance after = notice period cost - expect(await token.balanceOf(await provider.getAddress())).to.equal(noticePeriodCostExpected); - // marketv1 balance after = 0 - expect(await token.balanceOf(marketv1.address)).to.equal(0); - }); - }); - - describe("Scenario 2: Closing Job 2 minutes after opening (before notice period)", function () { - it("should spend notice period cost only", async () => { - const TWO_MINUTES = 60 * 2; - - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - - await marketv1 - .connect(user) - .jobClose(INITIAL_JOB_INDEX); - - const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); - - // user balance after = initial fund - 3 minutes worth tokens - notice period cost - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdc(TWO_MINUTES)).sub(noticePeriodCostExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - // provider balance after = 2 minutes worth tokens + notice period cost - const providerBalanceExpected = usdc(TWO_MINUTES).add(noticePeriodCostExpected); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - // marketv1 balance after = 0 - expect(await token.balanceOf(marketv1.address)).to.equal(0); - }); - }); - - describe("Scenario 3: Closing Job exactly after notice period", function () { - it("should spend 10 minutes worth tokens", async () => { - const usdcSpentExpected = calcAmountToPay(JOB_RATE_1, FIVE_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); - - await marketv1 - .connect(user) - .jobClose(INITIAL_JOB_INDEX); - - // user balance after = initial fund - 5 minutes worth tokens - notice period cost - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcSpentExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + expect(finalJobCreditBalance).to.equal(0); - // provider balance after = 5 minutes worth tokens + notice period cost - const providerBalanceExpected = usdcSpentExpected; - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + const providerPaymentThisSettle = amountPaidThisSettle; + const providerBalanceExpected = noticeCostPaidAtOpen.add(providerPaymentThisSettle); + expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); - expect(await token.balanceOf(marketv1.address)).to.equal(0); + expect(await token.balanceOf(marketv1.address)).to.be.closeTo(jobBalanceExpected, 2); + expect(await creditToken.balanceOf(marketv1.address)).to.equal(0); }); }); - - describe("Scenario 4: Closing Job 2 minutes after notice period", function () { - it("should spend 12 minutes worth tokens", async () => { - const SEVEN_MINUTES = 60 * 7; - const usdcSpentExpected = calcAmountToPay(JOB_RATE_1, SEVEN_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + SEVEN_MINUTES); - - await marketv1 - .connect(user) - .jobClose(INITIAL_JOB_INDEX); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcSpentExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = usdcSpentExpected; - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - expect(await token.balanceOf(marketv1.address)).to.equal(0); - }); - }); - }); - - describe("Credit Only", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - - beforeEach(async () => { - // Deposit 50 Credit - await creditToken.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); - await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - }); - - it("should close job and withdraw all credit", async () => { - expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); - const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); - const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); - - // Close job - await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); - - const userCreditBalanceExpected = userCreditBalanceBefore.add(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); - const userUSDCBalanceExpected = userUSDCBalanceBefore; - expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); - expect(await token.balanceOf(await user.getAddress())).to.equal(userUSDCBalanceExpected); - }); - }); - - describe("Both Credit and USDC", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const INITIAL_CREDIT_DEPOSIT_AMOUNT = usdc(40); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - - beforeEach(async () => { - // Deposit 40 Credit, 10 USDC - await creditToken.connect(user).approve(marketv1.address, INITIAL_CREDIT_DEPOSIT_AMOUNT); - await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - }); - - it("should close job and withdraw all Credit and USDC", async () => { - const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); - const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); - - // Close job - await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); - - const userCreditBalanceExpected = userCreditBalanceBefore.add(INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); - const userUSDCBalanceExpected = userUSDCBalanceBefore.add(INITIAL_DEPOSIT_AMOUNT.sub(INITIAL_CREDIT_DEPOSIT_AMOUNT)); - expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userUSDCBalanceExpected.sub(JOB_RATE_1), userUSDCBalanceExpected.add(JOB_RATE_1)); - }); - }); - }); - - describe("Metdata Update", function () { - const initialDeposit = usdc(50); - - takeSnapshotBeforeAndAfterEveryTest(async () => { }); - - beforeEach(async () => { - await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - }); - - it("should update metadata", async () => { - await marketv1 - .connect(user) - .jobMetadataUpdate(INITIAL_JOB_INDEX, "some updated metadata"); - - const jobInfo2 = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo2.metadata).to.equal("some updated metadata"); - }); - - it("should revert when updating metadata of other jobs", async () => { - await expect(marketv1 - .connect(signers[3]) // neither owner nor provider - .jobMetadataUpdate(INITIAL_JOB_INDEX, "some updated metadata")).to.be.revertedWith("only job owner"); - }); - }); - - describe("Emergency Withdraw", function () { - const NUM_TOTAL_JOB = 5; - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - let TOTAL_DEPOSIT_AMOUNT = BN.from(0); - let jobs: string[] = []; - let deposits: BN[] = []; - - beforeEach(async () => { - await marketv1.connect(admin).grantRole(await marketv1.EMERGENCY_WITHDRAW_ROLE(), await admin2.getAddress()); - - // open 5 jobs - for (let i = 0; i < NUM_TOTAL_JOB; i++) { - const EXTRA_DEPOSIT_AMOUNT = usdc(i * 10); - const DEPOSIT_AMOUNT = INITIAL_DEPOSIT_AMOUNT.add(EXTRA_DEPOSIT_AMOUNT); - - // list of jobs and deposits - jobs.push(await marketv1.jobIndex()); - deposits.push(DEPOSIT_AMOUNT); - // total credit deposit amount - TOTAL_DEPOSIT_AMOUNT = TOTAL_DEPOSIT_AMOUNT.add(DEPOSIT_AMOUNT); - - // open job only with credit - await creditToken.connect(user).approve(marketv1.address, DEPOSIT_AMOUNT); - await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, DEPOSIT_AMOUNT); - } }); - - it("should revert when withdrawing to address without EMERGENCY_WITHDRAW_ROLE", async () => { - await expect(marketv1 - .connect(admin) - .emergencyWithdrawCredit(await user.getAddress(), [INITIAL_JOB_INDEX])).to.be.revertedWith("only to emergency withdraw role"); - }); - - it("should revert when non-admin calls emergencyWithdrawCredit", async () => { - await expect(marketv1 - .connect(user) - .emergencyWithdrawCredit(await user.getAddress(), jobs)).to.be.revertedWith("only admin"); - }); - - it("should settle all jobs and withdraw all credit", async () => { - const totalSettledAmountExpected = calcNoticePeriodCost(JOB_RATE_1).mul(NUM_TOTAL_JOB); - - await marketv1.connect(admin).emergencyWithdrawCredit(await admin2.getAddress(), jobs); - - const CURRENT_TIMESTAMP = (await ethers.provider.getBlock('latest')).timestamp; - for (let i = 0; i < NUM_TOTAL_JOB; i++) { - // fetch job info - const jobInfo = await marketv1.jobs(jobs[i]); - - // should settle all jobs - expect(jobInfo.lastSettled).to.equal((CURRENT_TIMESTAMP + NOTICE_PERIOD).toString()); - - // job credit balance should be 0 - expect(await marketv1.jobCreditBalance(jobs[i])).to.equal(0); - } - - // withdrawal recipient - const withdrawalAmountExpected = TOTAL_DEPOSIT_AMOUNT.sub(totalSettledAmountExpected); - expect(await creditToken.balanceOf(await admin2.getAddress())).to.be.within(withdrawalAmountExpected.sub(JOB_RATE_1), withdrawalAmountExpected.add(JOB_RATE_1)); - - // Provider - expect(await token.balanceOf(await provider.getAddress())).to.be.within(totalSettledAmountExpected.sub(JOB_RATE_1), totalSettledAmountExpected.add(JOB_RATE_1)); - - // MarketV1 - expect(await creditToken.balanceOf(marketv1.address)).to.equal(0); - }); - }) + }); }); \ No newline at end of file From 5be7248af36784f52a52d237a87ebf1f68f25291 Mon Sep 17 00:00:00 2001 From: Prateek Date: Thu, 22 May 2025 12:38:40 +0400 Subject: [PATCH 9/9] fix: tests --- test/enclaves/MarketV1.ts | 1647 ++++++++++++++++++++++++++++++++----- 1 file changed, 1454 insertions(+), 193 deletions(-) diff --git a/test/enclaves/MarketV1.ts b/test/enclaves/MarketV1.ts index 50dfd51..951e410 100644 --- a/test/enclaves/MarketV1.ts +++ b/test/enclaves/MarketV1.ts @@ -66,7 +66,8 @@ const calcNoticePeriodCost = (rate: BN) => { }; const calcAmountToPay = (rate: BN, duration: number) => { - return rate.mul(BN.from(duration)).add(10 ** 12 - 1).div(10 ** 12); + const DECIMALS = BN.from("1000000000000"); + return rate.mul(BN.from(duration)).add(DECIMALS.sub(1)).div(DECIMALS); } const incrementJobId = (jobId: string, increment: number) => { @@ -312,6 +313,8 @@ describe("MarketV1", function () { let admin2: Signer; let INITIAL_JOB_INDEX: string; + let JOB_OPENED_TIMESTAMP: number; + before(async function () { signers = await ethers.getSigners(); @@ -463,17 +466,21 @@ describe("MarketV1", function () { const initialBalance = usdc(50); const noticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); - await marketv1 + const tx = await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialBalance); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const jobOpenTs = block.timestamp; + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); expect(jobInfo.metadata).to.equal("some metadata"); expect(jobInfo.owner).to.equal(await user.getAddress()); expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); expect(jobInfo.balance).to.equal(initialBalance.sub(noticePeriodCost)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP, INITIAL_TIMESTAMP + 1); + expect(jobInfo.lastSettled).to.equal(jobOpenTs); expect(jobInfo.maxRate).to.equal(JOB_RATE_1); expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(initialBalance)); @@ -517,17 +524,20 @@ describe("MarketV1", function () { const noticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); await creditToken.connect(user).approve(marketv1.address, initialBalance); - await marketv1 + const tx = await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialBalance); - + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const jobOpenTs = block.timestamp; + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); expect(jobInfo.metadata).to.equal("some metadata"); expect(jobInfo.owner).to.equal(await user.getAddress()); expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); expect(jobInfo.balance).to.equal(initialBalance.sub(noticePeriodCost)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP, INITIAL_TIMESTAMP + 1); + expect(jobInfo.lastSettled).to.equal(jobOpenTs); expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal(initialBalance.sub(noticePeriodCost)); }); @@ -568,7 +578,6 @@ describe("MarketV1", function () { describe("Job Settle", function () { const initialDeposit = usdc(50); const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - let jobOpenActualTimestamp: number; takeSnapshotBeforeAndAfterEveryTest(async () => { }); @@ -576,58 +585,47 @@ describe("MarketV1", function () { beforeEach(async () => { await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 + const tx = await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - jobOpenActualTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + JOB_OPENED_TIMESTAMP = block.timestamp; }); describe("CASE1: Settle Job immediately after Job Open", function () { - it("should have balance and lastSettled reflecting initial state after open", async () => { - const jobBeforeSettle = await marketv1.jobs(INITIAL_JOB_INDEX); - await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + it("should lose notice period cost", async () => { + const tx = await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const jobSettleTs = block.timestamp; - expect(jobInfo.balance).to.equal(initialBalance); - expect(jobInfo.lastSettled).to.be.closeTo(jobOpenActualTimestamp, 2); - }); - }); - - describe("CASE2: Settle Job 2 minutes after Job Open", function () { - it("should settle for 2 minutes", async () => { - const DURATION_TO_SETTLE = TWO_MINUTES; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); - - await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE))); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))); + expect(jobInfo.lastSettled).to.equal(jobSettleTs); }); }); - - describe("CASE3: Settle Job 1 second before notice period", function () { - it("should settle for (notice period - 1 second)", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD - 1; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); + describe("CASE2: Settle Job 2 minutes after Job Open", function () { + it("Balance should decrease by 2 minutes + Notice period worth", async () => { + const TWO_MINUTES = 60 * 2; + await time.increaseTo(JOB_OPENED_TIMESTAMP + TWO_MINUTES); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE))); + expect(jobInfo.lastSettled).to.be.equal(JOB_OPENED_TIMESTAMP + TWO_MINUTES); + expect(jobInfo.balance).to.equal( + initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber())), + ); }); }); describe("CASE4: Settle Job 2 minutes after notice period", function () { - it("should spend for (notice period + 2 minutes)", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD + TWO_MINUTES; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); - - const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); + it("should spend notice period cost and 2 minutes + notice period worth tokens", async () => { + const TWO_MINUTES = 60 * 2; + await time.increaseTo(JOB_OPENED_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); + + // Job Settle await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); @@ -636,112 +634,128 @@ describe("MarketV1", function () { expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); - const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); - expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); + const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + const lastSettledTimestampExpected = JOB_OPENED_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES; + expect(jobInfo.lastSettled).to.equal(lastSettledTimestampExpected); + // User balance const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); expect(await token.balanceOf(await user.getAddress())).to.equal(userBalanceExpected); - const providerBalanceExpected = noticeCostPaidAtOpen.add(amountPaidThisSettle); - expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); + // Provider balance + const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.be.closeTo(jobBalanceExpected, 2); + // MarketV1 balance + const marketv1BalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); }); }); }); describe("Credit Only", function () { beforeEach(async () => { + // await token.connect(user).approve(marketv1.address, initialDeposit); await creditToken.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 + const tx = await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - jobOpenActualTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + JOB_OPENED_TIMESTAMP = block.timestamp; }); describe("CASE1: Settle Job immediately after Job Open", function () { - it("should have balance and lastSettled reflecting initial state after open", async () => { - const jobBeforeSettle = await marketv1.jobs(INITIAL_JOB_INDEX); - const creditBalanceBeforeSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + it("should deduct notice period worth from credit balance", async () => { + const tx = await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const jobSettleTs = block.timestamp; - await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const creditBalanceAfterSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - + expect(jobInfo.lastSettled).to.equal(jobSettleTs); expect(jobInfo.balance).to.equal(initialBalance); - expect(jobInfo.lastSettled).to.be.closeTo(jobOpenActualTimestamp, 2); - expect(creditBalanceAfterSettle).to.equal(creditBalanceBeforeSettle); - expect(creditBalanceAfterSettle).to.equal(initialBalance); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal( + initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)), + ); }); }); describe("CASE2: Settle Job 2 minutes after Job Open", function () { - it("should settle for 2 minutes using credit", async () => { - const DURATION_TO_SETTLE = TWO_MINUTES; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); - - const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + it("should revert before lastSettled", async () => { + const TWO_MINUTES = 60 * 2; + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const tx = await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const jobSettleTs = block.timestamp; const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); - expect(jobCreditBal).to.equal(initialBalance.sub(amountToSettle)); + expect(jobInfo.lastSettled).to.be.equal(jobSettleTs); + expect(jobInfo.lastSettled).to.be.closeTo(INITIAL_TIMESTAMP + TWO_MINUTES, 1); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal( + initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)).sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()) + )); }); }); describe("CASE3: Settle Job exactly after notice period", function () { - it("should settle for notice period duration using credit", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); - - const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + it("should revert before lastSettled", async () => { + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); - await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const tx = await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const jobSettleTs = block.timestamp; const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); - expect(jobCreditBal).to.equal(initialBalance.sub(amountToSettle)); + expect(jobInfo.lastSettled).to.be.equal(jobSettleTs); + expect(jobInfo.lastSettled).to.be.closeTo(INITIAL_TIMESTAMP + NOTICE_PERIOD, 1); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal( + initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)).sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()) + )); }); }); describe("CASE4: Settle Job 2 minutes after notice period", function () { - it("should spend notice period cost and 2 minutes worth tokens from credit", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD + TWO_MINUTES; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); + it("should spend notice period cost and 2 minutes worth tokens", async () => { + const TWO_MINUTES = 60 * 2; + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); - const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); + // Job Settle await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + // Job Info After Settle const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); expect(jobInfo.metadata).to.equal("some metadata"); expect(jobInfo.owner).to.equal(await user.getAddress()); expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); - const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); - expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); - expect(jobCreditBal).to.be.closeTo(jobBalanceExpected, 2); + const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 3); + const lastSettledTimestampExpected = INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - const providerBalanceExpected = noticeCostPaidAtOpen.add(amountPaidThisSettle); - expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); + // User balance + const userTokenBalanceExpected = SIGNER1_INITIAL_FUND; + expect(await token.balanceOf(await user.getAddress())).to.be.within(userTokenBalanceExpected.sub(JOB_RATE_1), userTokenBalanceExpected.add(JOB_RATE_1)); + const userCreditBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.equal(0); - expect(await creditToken.balanceOf(marketv1.address)).to.be.closeTo(jobCreditBal, 2); + // Provider balance + const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + // MarketV1 balance + const marketv1TokenBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1TokenBalanceExpected.sub(JOB_RATE_1), marketv1TokenBalanceExpected.add(JOB_RATE_1)); + const marketv1CreditBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); + expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); }); }); }); @@ -753,148 +767,1395 @@ describe("MarketV1", function () { beforeEach(async () => { await token.connect(user).approve(marketv1.address, usdcDeposit); await creditToken.connect(user).approve(marketv1.address, creditDeposit); - await marketv1 + // deposit 10 credit and 40 usdc + const tx = await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, usdcDeposit.add(creditDeposit)); - jobOpenActualTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + JOB_OPENED_TIMESTAMP = block.timestamp; }); describe("CASE1: Settle Job immediately after Job Open", function () { - it("should have balance and lastSettled reflecting initial state after open", async () => { - const jobBeforeSettle = await marketv1.jobs(INITIAL_JOB_INDEX); - const creditBalanceBeforeSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - + it("should revert before lastSettled", async () => { await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const creditBalanceAfterSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - - expect(jobInfo.balance).to.equal(initialBalance); - expect(jobInfo.lastSettled).to.be.closeTo(jobOpenActualTimestamp, 2); - expect(creditBalanceAfterSettle).to.equal(creditBalanceBeforeSettle); - expect(creditBalanceAfterSettle).to.equal(creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1))); + expect(jobInfo.lastSettled).to.be.closeTo(INITIAL_TIMESTAMP, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal( + creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)).sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber())) + ); }); }); describe("CASE2: Settle Job 2 minutes after Job Open", function () { - it("should settle for 2 minutes, using credit then USDC", async () => { - const DURATION_TO_SETTLE = TWO_MINUTES; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); - - const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - const initialJobCreditBalance = creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - + it("should revert before lastSettled", async () => { + const TWO_MINUTES = 60 * 2; + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); - let expectedCreditBalanceAfterSettle = initialJobCreditBalance.sub(amountToSettle); - if (expectedCreditBalanceAfterSettle.lt(0)) { - expectedCreditBalanceAfterSettle = BN.from(0); - } - expect(jobCreditBal).to.equal(expectedCreditBalanceAfterSettle); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + TWO_MINUTES, INITIAL_TIMESTAMP + TWO_MINUTES + 1); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))); + + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal( + creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1).add(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))) + ); }); }); describe("CASE3: Settle Job exactly after notice period", function () { - it("should settle for notice period, using credit then USDC", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); - - const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - const initialJobCreditBalance = creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - + it("Balance should decrease by NOTICE PERIOD * 2 duration", async () => { + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + NOTICE_PERIOD, INITIAL_TIMESTAMP + NOTICE_PERIOD + 1); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))); - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); - - let expectedCreditBalanceAfterSettle = initialJobCreditBalance.sub(amountToSettle); - if (expectedCreditBalanceAfterSettle.lt(0)) { - expectedCreditBalanceAfterSettle = BN.from(0); - } - expect(jobCreditBal).to.equal(expectedCreditBalanceAfterSettle); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal( + creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1).add(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))) + ); }); }); - describe("CASE4: Settle Job 2 minutes after notice period - credit might be used up", function () { - it("should settle, exhausting credit first, then USDC", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD + TWO_MINUTES; - const TIME_JOB_SETTLE_TARGET = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(TIME_JOB_SETTLE_TARGET); - - const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); - const initialJobCreditBalance = creditDeposit.sub(noticeCostPaidAtOpen); + describe("CASE4: Settle Job 2 minutes after notice period - only Credit is settled", function () { + it("should settle notice period cost and 2 minutes worth tokens only from Credit", async () => { + const TWO_MINUTES = 60 * 2; + const TIME_JOB_OPEN = (await ethers.provider.getBlock('latest')).timestamp; + await time.increaseTo(TIME_JOB_OPEN + NOTICE_PERIOD + TWO_MINUTES); + const TIME_JOB_SETTLE = (await ethers.provider.getBlock('latest')).timestamp; + const TIME_DIFF = TIME_JOB_SETTLE - TIME_JOB_OPEN; + + const noticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + // Job Settle await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + /* Job Info After Settle */ const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const finalJobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); expect(jobInfo.owner).to.equal(await user.getAddress()); expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); - - const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); - expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); - - expect(jobInfo.lastSettled).to.be.closeTo(TIME_JOB_SETTLE_TARGET, 3); - - let expectedFinalCreditBalance = initialJobCreditBalance.sub(amountPaidThisSettle); - if (expectedFinalCreditBalance.lt(0)) { - expectedFinalCreditBalance = BN.from(0); - } - expect(finalJobCreditBalance).to.equal(expectedFinalCreditBalance); - - const providerPaymentThisSettle = amountPaidThisSettle; - const providerBalanceExpected = noticeCostPaidAtOpen.add(providerPaymentThisSettle); - expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); - - const expectedMarketV1UsdcBalance = jobBalanceExpected.sub(expectedFinalCreditBalance); - expect(await token.balanceOf(marketv1.address)).to.be.closeTo(expectedMarketV1UsdcBalance, 2); - expect(await creditToken.balanceOf(marketv1.address)).to.equal(expectedFinalCreditBalance); + // Job Balance + const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + // Job Last Settled + const lastSettledTimestampExpected = TIME_JOB_OPEN + NOTICE_PERIOD + TWO_MINUTES; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + // Job Credit Balance + const amountSettledExpected = calcAmountToPay(JOB_RATE_1, TIME_DIFF); + const jobCreditBalanceExpected = creditDeposit.sub(amountSettledExpected); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.be.within(jobCreditBalanceExpected.sub(JOB_RATE_1), jobCreditBalanceExpected.add(JOB_RATE_1)); + + /* User balance */ + // User Token balance + const userTokenBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcDeposit); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userTokenBalanceExpected.sub(JOB_RATE_1), userTokenBalanceExpected.add(JOB_RATE_1)); + // User Credit balance + const userCreditBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); + + /* Provider balance */ + // Provider Token balance + const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + /* MarketV1 balance */ + // MarketV1 Token balance + const marketv1TokenBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1TokenBalanceExpected.sub(JOB_RATE_1), marketv1TokenBalanceExpected.add(JOB_RATE_1)); + // MarketV1 Credit balance + const marketv1CreditBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); + expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); }); }); describe("CASE5: Settle Job 20 minutes after notice period - both Credit and USDC are settled", function () { it("should settle all Credits and some USDC", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD + 60 * 20; - const TIME_JOB_SETTLE_TARGET = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(TIME_JOB_SETTLE_TARGET); + const TWENTY_MINUTES = 60 * 20; + await time.increaseTo(JOB_OPENED_TIMESTAMP + NOTICE_PERIOD + TWENTY_MINUTES); + const TIME_JOB_SETTLE = (await ethers.provider.getBlock('latest')).timestamp; + const TIME_DIFF = TIME_JOB_SETTLE - JOB_OPENED_TIMESTAMP; - const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); - const initialJobCreditBalance = creditDeposit.sub(noticeCostPaidAtOpen); + const noticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + // Job Settle await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + /* Job Info After Settle */ const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const finalJobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + // Job Balance + const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWENTY_MINUTES)); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + // Job Last Settled + const lastSettledTimestampExpected = JOB_OPENED_TIMESTAMP + NOTICE_PERIOD + TWENTY_MINUTES; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + // Job Credit Balance + const amountSettledExpected = calcAmountToPay(JOB_RATE_1, TIME_DIFF); + const jobCreditBalanceExpected = creditDeposit.sub(amountSettledExpected); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.be.within(jobCreditBalanceExpected.sub(JOB_RATE_1), jobCreditBalanceExpected.add(JOB_RATE_1)); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal(0); + + /* User balance */ + // User Token balance + const userTokenBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcDeposit); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userTokenBalanceExpected.sub(JOB_RATE_1), userTokenBalanceExpected.add(JOB_RATE_1)); + // User Credit balance + const userCreditBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); + + /* Provider balance */ + // Provider Token balance + const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + /* MarketV1 balance */ + // MarketV1 Token balance + const marketv1TokenBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1TokenBalanceExpected.sub(JOB_RATE_1), marketv1TokenBalanceExpected.add(JOB_RATE_1)); + // MarketV1 Credit balance + const marketv1CreditBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); + expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); + }); + }); + }); + }); + + describe("Job Deposit", function () { + takeSnapshotBeforeAndAfterEveryTest(async () => { }); + + describe("USDC Only", function () { + it("should deposit to job with USDC", async () => { + const initialDeposit = usdc(50); + const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + const additionalDepositAmount = usdc(25); + + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + + // Deposit 25 USDC + await marketv1 + .connect(signers[1]) + .jobDeposit(INITIAL_JOB_INDEX, additionalDepositAmount); + + // Job after deposit + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(addrs[1]); + expect(jobInfo.provider).to.equal(addrs[2]); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + expect(jobInfo.balance).to.equal(initialBalance.add(additionalDepositAmount)); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP - 3, INITIAL_TIMESTAMP + 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(additionalDepositAmount); + expect(await token.balanceOf(addrs[1])).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = initialBalance.add(additionalDepositAmount); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + + it("should revert when depositing to job without enough approved", async () => { + const initialDeposit = usdc(50); + const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + const additionalDepositAmount = usdc(25); + + await token.connect(user).approve(marketv1.address, initialDeposit); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + + // Deposit 25 USDC + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, additionalDepositAmount)).to.be.revertedWith("ERC20: insufficient allowance"); + }); + + it("should revert when depositing to job without enough balance", async () => { + const initialDeposit = SIGNER1_INITIAL_FUND; + const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + const additionalDepositAmount = usdc(25); + + // Open Job + await token.connect(user).approve(marketv1.address, SIGNER1_INITIAL_FUND.add(usdc(1000))); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + + // Deposit 25 USDC + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, additionalDepositAmount)).to.be.revertedWith("ERC20: transfer amount exceeds balance"); + }); + + it("should revert when depositing to never registered job", async () => { + await expect(marketv1 + .connect(user) + .jobDeposit(ethers.utils.hexZeroPad("0x01", 32), 25)).to.be.revertedWith("job not found"); + }); + + it("should revert when depositing to closed job", async () => { + const initialDeposit = usdc(50); + + // Job Open + await token.connect(user).approve(marketv1.address, initialDeposit); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + + // Job Close + await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); + + // Job Deposit + await expect(marketv1 + .connect(signers[1]) + .jobDeposit(INITIAL_JOB_INDEX, 25)).to.be.revertedWith("job not found"); + }); + }); + + describe("Credit Only", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const ADDITIONAL_DEPOSIT_AMOUNT = usdc(25); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + + it("should deposit to job with Credit", async () => { + const tx_open = await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + const receipt_open = await tx_open.wait(); + const block_open = await ethers.provider.getBlock(receipt_open.blockHash); + const jobOpenTs = block_open.timestamp; + + // Deposit 25 Credit + await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_DEPOSIT_AMOUNT); + const tx = await marketv1 + .connect(signers[1]) + .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const currentTimestamp = block.timestamp; + // Job after deposit + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + expect(jobInfo.balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST.add(calcAmountToPay(JOB_RATE_1, currentTimestamp - jobOpenTs))).add(ADDITIONAL_DEPOSIT_AMOUNT)); + expect(jobInfo.lastSettled).to.equal(currentTimestamp); + expect(jobInfo.lastSettled).to.be.closeTo(jobOpenTs, 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).sub(ADDITIONAL_DEPOSIT_AMOUNT); + expect(await token.balanceOf(addrs[1])).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = INITIAL_DEPOSIT_AMOUNT.add(ADDITIONAL_DEPOSIT_AMOUNT); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + + it("should revert when depositing to job without approving both Credit and USDC", async () => { + const additionalDepositAmount = usdc(25); + + await token.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + + // Deposit without approving Credit + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT)).to.be.revertedWith("ERC20: insufficient allowance"); + }); + + it("should deposit USDC when Credit credit balance is not enough", async () => { + const initialDeposit = SIGNER1_INITIAL_FUND; + + // Open Job + await token.connect(user2).approve(marketv1.address, SIGNER1_INITIAL_FUND.add(usdc(1000))); + await marketv1 + .connect(user2) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + + // Approve 25 Credit without having enough Credit balance + await creditToken.connect(user2).approve(marketv1.address, ADDITIONAL_DEPOSIT_AMOUNT); + await expect(marketv1 + .connect(user2) + .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT)).to.be.revertedWith("ERC20: transfer amount exceeds balance"); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const creditBalance = await creditToken.balanceOf(await user2.getAddress()); + expect(jobInfo.balance).to.equal(initialDeposit.sub(NOTICE_PERIOD_COST)); + expect(creditBalance).to.equal(0); + }); + + it("should revert when depositing to never registered job", async () => { + await creditToken.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT)).to.be.revertedWith("job not found"); + }); + + it("should revert when depositing to closed job", async () => { + const initialDeposit = usdc(50); + + // Job Open + await token.connect(user).approve(marketv1.address, initialDeposit); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + + // Job Close + await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); + + // Job Deposit + await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_DEPOSIT_AMOUNT); + await expect(marketv1 + .connect(signers[1]) + .jobDeposit(INITIAL_JOB_INDEX, 25)).to.be.revertedWith("job not found"); + }); + }); + + describe("Both Credit and USDC", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + const TOTAL_ADITIONAL_DEPOSIT_AMOUNT = usdc(40); + const ADDITIONAL_CREDIT_DEPOSIT_AMOUNT = usdc(30); + const ADDITIONAL_USDC_DEPOSIT_AMOUNT = usdc(10); + const TOTAL_DEPOSIT_AMOUNT = INITIAL_DEPOSIT_AMOUNT.add(ADDITIONAL_USDC_DEPOSIT_AMOUNT).add(ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); + + it("should deposit 10 USDC and 30 Credit", async () => { + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + expect((await marketv1.jobs(INITIAL_JOB_INDEX)).balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); + + // Deposit 30 Credit and 10 USDC + await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); + await marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, TOTAL_ADITIONAL_DEPOSIT_AMOUNT); + + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + // Job after deposit + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const creditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + expect(jobInfo.balance).to.equal(TOTAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); + expect(jobInfo.lastSettled).to.equal(currentTimestamp); + expect(creditBalance).to.equal(ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); + + // User Balance + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).sub(ADDITIONAL_USDC_DEPOSIT_AMOUNT).sub(ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); + expect(await token.balanceOf(addrs[1])).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + // MarketV1 Balance + const marketv1BalanceExpected = TOTAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + + it("should revert when depositing to job without enough USDC approved", async () => { + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + + // Deposit 30 Credit and 1000 USDC + await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT.add(SIGNER1_INITIAL_FUND))).to.be.revertedWith("ERC20: insufficient allowance"); + }); + + it("should revert when user does not have enough USDC balance", async () => { + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + + // Deposit 30 Credit and 1000 USDC + await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); + await token.connect(user).approve(marketv1.address, SIGNER1_INITIAL_FUND); + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT.add(SIGNER1_INITIAL_FUND))).to.be.revertedWith("ERC20: transfer amount exceeds balance"); + }); + + it("should revert when balance is below notice period cost", async () => { + // Open Job + await token.connect(user).approve(marketv1.address, SIGNER1_INITIAL_FUND.add(usdc(1000))); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, SIGNER1_INITIAL_FUND); + + // 100_000 seconds passed (spend 1000 usdc) + await time.increaseTo(INITIAL_TIMESTAMP + 100_000); + + // Deposit 25 USDC + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, usdc(25))).to.be.revertedWith("insufficient funds to deposit"); + }); + }); + }); + + describe("Job Withdraw", function () { + const TWO_MINUTES = 60 * 2; + const SEVEN_MINUTES = 60 * 7; + + takeSnapshotBeforeAndAfterEveryTest(async () => { }); + + describe("USDC Only", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + const TOTAL_WITHDRAW_AMOUNT = usdc(10); + + beforeEach(async () => { + // Deposit 50 USDC + await token.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + }); + + it("should withdraw from job immediately", async () => { + const withdrawAmount = usdc(10); + + // Job Withdraw + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, withdrawAmount); + + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + let jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + expect(jobInfo.balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawAmount)); + expect(jobInfo.lastSettled).to.equal(currentTimestamp); + + expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(withdrawAmount)); + expect(await token.balanceOf(await provider.getAddress())).to.equal(NOTICE_PERIOD_COST); + expect(await token.balanceOf(marketv1.address)).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawAmount)); + }); + + it("should withdraw from job before lastSettled", async () => { + + // 2 minutes passed + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + + const providerBalanceBefore = await token.balanceOf(await provider.getAddress()); + + // Job Withdraw + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); // withdraw 10 USDC + + const SETTLED_AMOUNT = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); + + // Job info after Withdrawal + let jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + TWO_MINUTES - 3, INITIAL_TIMESTAMP + TWO_MINUTES + 3); + + // Check User USDC balance + const userUSDCBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userUSDCBalanceExpected.sub(JOB_RATE_1), userUSDCBalanceExpected.add(JOB_RATE_1)); + + // Check Provider USDC balance + const providerUSDCBalanceExpected = NOTICE_PERIOD_COST.add(SETTLED_AMOUNT); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerUSDCBalanceExpected.sub(JOB_RATE_1), providerUSDCBalanceExpected.add(JOB_RATE_1)); + + // Check MarketV1 USDC balance + const marketv1BalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + + it("should withdraw from job after lastSettled with settlement", async () => { + const settledAmountExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); + + // 7 minutes passed after Job Open + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); + + expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT)); + + const providerBalanceExpected = calcNoticePeriodCost(JOB_RATE_1).add(settledAmountExpected); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(settledAmountExpected); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + + it("should revert when withdrawing from non existent job", async () => { + const max_uint256_bytes32 = ethers.utils.hexZeroPad(ethers.constants.MaxUint256.toHexString(), 32); + + await expect(marketv1 + .connect(user) + .jobWithdraw(max_uint256_bytes32, usdc(100))).to.be.revertedWith("only job owner"); + }); + + it("should revert when withdrawing from third party job", async () => { + await expect(marketv1 + .connect(signers[3]) // neither owner nor provider + .jobWithdraw(INITIAL_JOB_INDEX, usdc(100))).to.be.revertedWith("only job owner"); + }); + + it("should revert when balance is below notice period cost", async () => { + // deposited 50 USDC + // 0.01 USDC/s + // notice period cost: 0.01 * 300 = 3 USDC + // 47 USDC left + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + 4500); // spend 45 USDC (300 + 4500 seconds passed) + + await expect(marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, usdc(1))).to.be.revertedWith("insufficient funds to withdraw"); + }); + + it("should revert when withdrawal request amount exceeds max withdrawable amount", async () => { + // Current balance: 47 USDC + + await expect(marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, usdc(48))).to.be.revertedWith("withdrawal amount exceeds job balance"); + }); + }); + + describe("Credit Only", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + const CREDIT_WITHDRAW_AMOUNT = usdc(10); + const TOTAL_WITHDRAW_AMOUNT = usdc(10); + + beforeEach(async () => { + // Deposit 50 Credit + await creditToken.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); + await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + }); + + it("should withdraw from job immediately", async () => { + // Job Withdraw + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, CREDIT_WITHDRAW_AMOUNT); // withdraw 10 Credit + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + expect(jobInfo.balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(CREDIT_WITHDRAW_AMOUNT)); + expect(jobInfo.lastSettled).to.equal(currentTimestamp); + + expect(jobCreditBalance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(CREDIT_WITHDRAW_AMOUNT)); + + // User Balance + const userCreditBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(CREDIT_WITHDRAW_AMOUNT); + expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); + + // Provider Balance + const providerCreditBalanceExpected = 0; + const providerUSDCBalanceExpected = NOTICE_PERIOD_COST; + expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); + expect(await token.balanceOf(await provider.getAddress())).to.equal(providerUSDCBalanceExpected); + + const marketv1CreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(CREDIT_WITHDRAW_AMOUNT); + expect(await creditToken.balanceOf(marketv1.address)).to.equal(marketv1CreditBalanceExpected); + }); + + it("should withdraw from job before lastSettled", async () => { + // 2 minutes passed + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + const SETTLED_AMOUNT = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); + + // Job Withdraw + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, CREDIT_WITHDRAW_AMOUNT); // withdraw 10 Credit + + // Job info after Withdrawal + let jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + TWO_MINUTES - 3, INITIAL_TIMESTAMP + TWO_MINUTES + 3); + + const jobCreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); + expect(jobCreditBalance).to.be.within(jobCreditBalanceExpected.sub(JOB_RATE_1), jobCreditBalanceExpected.add(JOB_RATE_1)); + + // User Balance + const userCreditBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT); + const userUSDCBalanceExpected = 0; + expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); + expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND); + + // Provider Balance + const providerCreditBalanceExpected = 0; + const providerUSDCBalanceExpected = NOTICE_PERIOD_COST.add(SETTLED_AMOUNT); + expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerUSDCBalanceExpected.sub(JOB_RATE_1), providerUSDCBalanceExpected.add(JOB_RATE_1)); + + // Check MarketV1 USDC balance + const marketv1CreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); + const marketv1USDCBalanceExpected = 0; + expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); + expect(await token.balanceOf(marketv1.address)).to.equal(marketv1USDCBalanceExpected); + }); + + it("should withdraw from job after lastSettled with settlement", async () => { + const settledAmountExpected = calcAmountToPay(JOB_RATE_1, SEVEN_MINUTES); + + // 7 minutes passed after Job Open + await time.increaseTo(INITIAL_TIMESTAMP + SEVEN_MINUTES); + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); + + + // User Balance + const userCreditBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT); + const userUSDCBalanceExpected = SIGNER1_INITIAL_FUND; + expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); + expect(await token.balanceOf(await user.getAddress())).to.equal(userUSDCBalanceExpected); + + // Provider Balance + const providerCreditBalanceExpected = 0; + const providerUSDCBalanceExpected = NOTICE_PERIOD_COST.add(settledAmountExpected); + expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerUSDCBalanceExpected.sub(JOB_RATE_1), providerUSDCBalanceExpected.add(JOB_RATE_1)); + + // Check MarketV1 Balance + const marketv1CreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(settledAmountExpected).sub(TOTAL_WITHDRAW_AMOUNT); + const marketv1USDCBalanceExpected = 0; + expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); + expect(await token.balanceOf(marketv1.address)).to.equal(marketv1USDCBalanceExpected); + }); + }); + + describe("Both Credit and USDC", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const INITIAL_CREDIT_DEPOSIT_AMOUNT = usdc(40); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + const TOTAL_WITHDRAW_AMOUNT = usdc(20); // 13 USDC + 7 Credit + + beforeEach(async () => { + // Deposit 10 Credit, 40 USDC + await creditToken.connect(user).approve(marketv1.address, INITIAL_CREDIT_DEPOSIT_AMOUNT); + await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + }); + + it("should withdraw only USDC", async () => { + const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); + const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); + + const USDC_WITHDRAWAL_AMOUNT = usdc(5); + + // Job Withdraw + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, USDC_WITHDRAWAL_AMOUNT); // withdraw 5 usdc + + // Job Info + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(TOTAL_WITHDRAW_AMOUNT); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + expect(jobInfo.lastSettled).to.equal(currentTimestamp); + const jobCreditBalanceExpected = INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST); + expect(jobCreditBalance).to.equal(jobCreditBalanceExpected); + + // User Balance + const userCreditBalanceExpected = userCreditBalanceBefore; + const userUSDCBalanceExpected = userUSDCBalanceBefore.sub(USDC_WITHDRAWAL_AMOUNT); + expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userUSDCBalanceExpected.sub(JOB_RATE_1), userUSDCBalanceExpected.add(JOB_RATE_1)); + + // Provider Balance + const providerCreditBalanceExpected = 0; + const providerUSDCBalanceExpected = NOTICE_PERIOD_COST; + expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); + expect(await token.balanceOf(await provider.getAddress())).to.equal(providerUSDCBalanceExpected); + + const marketv1CreditBalanceExpected = INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST); + const marketv1USDCBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(INITIAL_CREDIT_DEPOSIT_AMOUNT).sub(USDC_WITHDRAWAL_AMOUNT); + expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1USDCBalanceExpected.sub(JOB_RATE_1), marketv1USDCBalanceExpected.add(JOB_RATE_1)); + }); + + it("should withdraw both USDC and Credit", async () => { + const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); + const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); + + const withdrawnUSDCAmountExpected = ((await marketv1.jobs(INITIAL_JOB_INDEX)).balance).sub(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)); + const withdrawnCreditAmountExpected = TOTAL_WITHDRAW_AMOUNT.sub(withdrawnUSDCAmountExpected); + // Job Withdraw + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); // withdraw 20 (13 USDC + 7 Credit) + + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + // Job Info + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(TOTAL_WITHDRAW_AMOUNT); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + expect(jobInfo.lastSettled).to.equal(currentTimestamp); + expect(jobCreditBalance).to.equal(INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawnCreditAmountExpected)); + + // User Balance + const userCreditBalanceExpected = userCreditBalanceBefore.add(withdrawnCreditAmountExpected); + const userUSDCBalanceExpected = userUSDCBalanceBefore.add(withdrawnUSDCAmountExpected); + expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); + expect(await token.balanceOf(await user.getAddress())).to.equal(userUSDCBalanceExpected); + + // Provider Balance + const providerCreditBalanceExpected = 0; + const providerUSDCBalanceExpected = NOTICE_PERIOD_COST; + expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); + expect(await token.balanceOf(await provider.getAddress())).to.equal(providerUSDCBalanceExpected); + + // MarketV1 Balance + const marketv1CreditBalanceExpected = INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawnCreditAmountExpected); + expect(await creditToken.balanceOf(marketv1.address)).to.equal(marketv1CreditBalanceExpected); + expect(await token.balanceOf(marketv1.address)).to.equal(0); + }); + }); + }); + + describe("Job Revise Rate", function () { + const JOB_LOWER_RATE = BN.from(5).e16().div(10); + const JOB_HIGHER_RATE = BN.from(2).e16(); + + const initialDeposit = usdc(50); + const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + + takeSnapshotBeforeAndAfterEveryTest(async () => { }); + + beforeEach(async () => { + await token.connect(user).approve(marketv1.address, initialDeposit); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + }); + + it("should revise rate higher", async () => { + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, JOB_HIGHER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(JOB_HIGHER_RATE); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcNoticePeriodCost(JOB_HIGHER_RATE.sub(JOB_RATE_1)))); + + expect(jobInfo.lastSettled).to.equal(currentTimestamp ); + expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(initialDeposit)); + expect(await token.balanceOf(await provider.getAddress())).to.equal(calcNoticePeriodCost(JOB_HIGHER_RATE)); + expect(await token.balanceOf(marketv1.address)).to.equal(initialBalance.sub(calcNoticePeriodCost(JOB_HIGHER_RATE.sub(JOB_RATE_1)))); + }); + + it("should revise rate lower", async () => { + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, JOB_LOWER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(JOB_LOWER_RATE); + expect(jobInfo.balance).to.equal(initialBalance); + expect(jobInfo.lastSettled).to.equal(currentTimestamp ); + expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(initialDeposit)); + expect(await token.balanceOf(await provider.getAddress())).to.equal(calcNoticePeriodCost(JOB_RATE_1)); + expect(await token.balanceOf(marketv1.address)).to.equal(initialBalance); + }); + + it("should revert when initiating rate revision for non existent job", async () => { + await expect(marketv1 + .connect(user) + .jobReviseRate(ethers.utils.hexZeroPad("0x01", 32), JOB_HIGHER_RATE)).to.be.revertedWith("only job owner"); + }); + + it("should revert when initiating rate revision for third party job", async () => { + await expect(marketv1 + .connect(signers[3]) // neither owner nor provider + .jobReviseRate(INITIAL_JOB_INDEX, JOB_HIGHER_RATE)).to.be.revertedWith("only job owner"); + }); + + const HIGHER_RATE = BN.from(2).e16(); // 0.02 USDC/s + const LOWER_RATE = BN.from(5).e15(); // 0.005 USDC/s + + describe("CASE 1: Revising Rate immediately after job open", function () { + + describe("when rate is higher", function () { + + it("should spend notice period cost only", async () => { + const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); + + // Job info after Rate Revision + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(HIGHER_RATE); + const jobBalanceExpected = initialBalance.sub(noticePeriodCostExpected); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(noticePeriodCostExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = noticePeriodCostExpected; + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + + describe("when rate is lower", function () { + it("should spend notice period cost only", async () => { + const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); - const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); - expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); + // Job info after Rate Revision + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(LOWER_RATE); + const jobBalanceExpected = initialDeposit.sub(noticePeriodCostExpected); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(noticePeriodCostExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = noticePeriodCostExpected; + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + }); + + describe("CASE 2: Revising Rate 2 minutes after job open", function () { + const TWO_MINUTES = 60 * 2; + const SEVEN_MINUTES = 60 * 7; + + describe("when rate is higher", function () { + it("should spend notice period cost + 3 minutes worth tokens with higher rate", async () => { + // 5 min * initial rate + 3 min * higher rate + const usdcSpentExpected = calcNoticePeriodCost(JOB_RATE_1).add(calcAmountToPay(HIGHER_RATE, TWO_MINUTES)); + const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(HIGHER_RATE); + + const jobBalanceExpected = initialDeposit.sub(usdcSpentExpected); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp ; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(usdcSpentExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = usdcSpentExpected; + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + + describe("when rate is lower", function () { + it("should spend notice period cost + 3 minutes worth tokens with initial rate", async () => { + // 5 min * initial rate + 3 min * initial rate + const usdcSpentExpected = calcNoticePeriodCost(JOB_RATE_1).add(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); - expect(jobInfo.lastSettled).to.be.closeTo(TIME_JOB_SETTLE_TARGET, 3); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(LOWER_RATE); + + const jobBalanceExpected = initialDeposit.sub(usdcSpentExpected); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp ; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(usdcSpentExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = usdcSpentExpected; + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + }); + + describe("CASE 3: Revising Rate exactly after notice period", function () { + const TEN_MINUTES = 60 * 10; + + describe("when rate is higher", function () { + it("should spend 5 minutes worth tokens with initial rate and 5 minutes worth tokens with higher rate", async () => { + const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + const secondNoticePeriodCost = calcNoticePeriodCost(HIGHER_RATE); + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(HIGHER_RATE); + + const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp ; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + + describe("when rate is lower", function () { + it("should spend 5 minutes worth tokens with initial rate and 5 minutes worth tokens with initial rate", async () => { + const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + const secondNoticePeriodCost = calcNoticePeriodCost(LOWER_RATE); + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(LOWER_RATE); + + const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp ; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + }); + + describe("CASE 4: Revising Rate 2 minutes after notice period", function () { + const TWO_MINUTES = 60 * 2; + const TWELVE_MINUTES = 60 * 12; + + describe("when rate is higher", function () { + it("should spend 7 minutes worth tokens with initial rate and 5 minutes worth tokens with higher rate", async () => { + const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + const secondNoticePeriodCost = calcNoticePeriodCost(HIGHER_RATE); + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(HIGHER_RATE); + + const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp ; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - expect(finalJobCreditBalance).to.equal(0); + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + + describe("when rate is lower", function () { + it("should spend 7 minutes worth tokens with initial rate and 5 minutes worth tokens with initial rate", async () => { + const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + const secondNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(LOWER_RATE); + + const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp ; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + }); + }); + + describe("Job Close", function () { + const initialDeposit = usdc(50); + const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + + takeSnapshotBeforeAndAfterEveryTest(async () => { }); + + describe("USDC Only", function () { + beforeEach(async () => { + await token.connect(user).approve(marketv1.address, initialDeposit); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + }); + + it("should close job", async () => { + // Job Close + await marketv1 + .connect(user) + .jobClose(INITIAL_JOB_INDEX); // here, user should get back (initial deposit - notice period cost) + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal(""); + expect(jobInfo.owner).to.equal(ethers.constants.AddressZero); + expect(jobInfo.provider).to.equal(ethers.constants.AddressZero); + expect(jobInfo.rate).to.equal(0); + expect(jobInfo.balance).to.equal(0); + expect(jobInfo.lastSettled).to.equal(0); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(calcNoticePeriodCost(JOB_RATE_1)); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + }); + + it("should revert when closing non existent job", async () => { + await expect(marketv1 + .connect(user) + .jobClose(ethers.utils.hexZeroPad("0x01", 32))).to.be.revertedWith("only job owner"); + }); + + it("should revert when closing third party job", async () => { + await expect(marketv1 + .connect(signers[3]) // neither owner nor provider + .jobClose(INITIAL_JOB_INDEX)).to.be.revertedWith("only job owner"); + }); + + describe("Scenario 1: Closing Job immediately after opening", function () { + it("should spend notice period cost only", async () => { + await marketv1 + .connect(user) + .jobClose(INITIAL_JOB_INDEX); + + const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); + + // user balance after = initial fund - notice period cost + expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(noticePeriodCostExpected)); + // provider balance after = notice period cost + expect(await token.balanceOf(await provider.getAddress())).to.equal(noticePeriodCostExpected); + // marketv1 balance after = 0 + expect(await token.balanceOf(marketv1.address)).to.equal(0); + }); + }); + + describe("Scenario 2: Closing Job 2 minutes after opening (before notice period)", function () { + it("should spend notice period cost only", async () => { + const TWO_MINUTES = 60 * 2; + + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + + await marketv1 + .connect(user) + .jobClose(INITIAL_JOB_INDEX); + + const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); + + // user balance after = initial fund - 3 minutes worth tokens - notice period cost + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdc(TWO_MINUTES)).sub(noticePeriodCostExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + // provider balance after = 2 minutes worth tokens + notice period cost + const providerBalanceExpected = usdc(TWO_MINUTES).add(noticePeriodCostExpected); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + // marketv1 balance after = 0 + expect(await token.balanceOf(marketv1.address)).to.equal(0); + }); + }); + + describe("Scenario 3: Closing Job exactly after notice period", function () { + it("should spend 10 minutes worth tokens", async () => { + const usdcSpentExpected = calcAmountToPay(JOB_RATE_1, FIVE_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); + + await marketv1 + .connect(user) + .jobClose(INITIAL_JOB_INDEX); + + // user balance after = initial fund - 5 minutes worth tokens - notice period cost + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcSpentExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - const providerPaymentThisSettle = amountPaidThisSettle; - const providerBalanceExpected = noticeCostPaidAtOpen.add(providerPaymentThisSettle); - expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); + // provider balance after = 5 minutes worth tokens + notice period cost + const providerBalanceExpected = usdcSpentExpected; + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.be.closeTo(jobBalanceExpected, 2); - expect(await creditToken.balanceOf(marketv1.address)).to.equal(0); + expect(await token.balanceOf(marketv1.address)).to.equal(0); + }); + }); + + describe("Scenario 4: Closing Job 2 minutes after notice period", function () { + it("should spend 12 minutes worth tokens", async () => { + const SEVEN_MINUTES = 60 * 7; + const usdcSpentExpected = calcAmountToPay(JOB_RATE_1, SEVEN_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + SEVEN_MINUTES); + + await marketv1 + .connect(user) + .jobClose(INITIAL_JOB_INDEX); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcSpentExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = usdcSpentExpected; + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + expect(await token.balanceOf(marketv1.address)).to.equal(0); }); }); }); - }); + + describe("Credit Only", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + + beforeEach(async () => { + // Deposit 50 Credit + await creditToken.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); + await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + }); + + it("should close job and withdraw all credit", async () => { + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); + const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); + const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); + + // Close job + await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); + + const userCreditBalanceExpected = userCreditBalanceBefore.add(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); + const userUSDCBalanceExpected = userUSDCBalanceBefore; + expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); + expect(await token.balanceOf(await user.getAddress())).to.equal(userUSDCBalanceExpected); + }); + }); + + describe("Both Credit and USDC", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const INITIAL_CREDIT_DEPOSIT_AMOUNT = usdc(40); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + + beforeEach(async () => { + // Deposit 40 Credit, 10 USDC + await creditToken.connect(user).approve(marketv1.address, INITIAL_CREDIT_DEPOSIT_AMOUNT); + await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + }); + + it("should close job and withdraw all Credit and USDC", async () => { + const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); + const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); + + // Close job + await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); + + const userCreditBalanceExpected = userCreditBalanceBefore.add(INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); + const userUSDCBalanceExpected = userUSDCBalanceBefore.add(INITIAL_DEPOSIT_AMOUNT.sub(INITIAL_CREDIT_DEPOSIT_AMOUNT)); + expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userUSDCBalanceExpected.sub(JOB_RATE_1), userUSDCBalanceExpected.add(JOB_RATE_1)); + }); + }); + }); + + describe("Metdata Update", function () { + const initialDeposit = usdc(50); + + takeSnapshotBeforeAndAfterEveryTest(async () => { }); + + beforeEach(async () => { + await token.connect(user).approve(marketv1.address, initialDeposit); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + }); + + it("should update metadata", async () => { + await marketv1 + .connect(user) + .jobMetadataUpdate(INITIAL_JOB_INDEX, "some updated metadata"); + + const jobInfo2 = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo2.metadata).to.equal("some updated metadata"); + }); + + it("should revert when updating metadata of other jobs", async () => { + await expect(marketv1 + .connect(signers[3]) // neither owner nor provider + .jobMetadataUpdate(INITIAL_JOB_INDEX, "some updated metadata")).to.be.revertedWith("only job owner"); + }); + }); + + describe("Emergency Withdraw", function () { + const NUM_TOTAL_JOB = 5; + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + let TOTAL_DEPOSIT_AMOUNT = BN.from(0); + let jobs: string[] = []; + let deposits: BN[] = []; + + beforeEach(async () => { + await marketv1.connect(admin).grantRole(await marketv1.EMERGENCY_WITHDRAW_ROLE(), await admin2.getAddress()); + + // open 5 jobs + for (let i = 0; i < NUM_TOTAL_JOB; i++) { + const EXTRA_DEPOSIT_AMOUNT = usdc(i * 10); + const DEPOSIT_AMOUNT = INITIAL_DEPOSIT_AMOUNT.add(EXTRA_DEPOSIT_AMOUNT); + + // list of jobs and deposits + jobs.push(await marketv1.jobIndex()); + deposits.push(DEPOSIT_AMOUNT); + // total credit deposit amount + TOTAL_DEPOSIT_AMOUNT = TOTAL_DEPOSIT_AMOUNT.add(DEPOSIT_AMOUNT); + + // open job only with credit + await creditToken.connect(user).approve(marketv1.address, DEPOSIT_AMOUNT); + await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, DEPOSIT_AMOUNT); + } + }); + + it("should revert when withdrawing to address without EMERGENCY_WITHDRAW_ROLE", async () => { + await expect(marketv1 + .connect(admin) + .emergencyWithdrawCredit(await user.getAddress(), [INITIAL_JOB_INDEX])).to.be.revertedWith("only to emergency withdraw role"); + }); + + it("should revert when non-admin calls emergencyWithdrawCredit", async () => { + await expect(marketv1 + .connect(user) + .emergencyWithdrawCredit(await user.getAddress(), jobs)).to.be.revertedWith("only admin"); + }); + + it("should settle all jobs and withdraw all credit", async () => { + const totalSettledAmountExpected = calcNoticePeriodCost(JOB_RATE_1).mul(NUM_TOTAL_JOB); + + await marketv1.connect(admin).emergencyWithdrawCredit(await admin2.getAddress(), jobs, { gasLimit: 10000000 }); + + const CURRENT_TIMESTAMP = (await ethers.provider.getBlock('latest')).timestamp; + for (let i = 0; i < NUM_TOTAL_JOB; i++) { + // fetch job info + const jobInfo = await marketv1.jobs(jobs[i]); + + // should settle all jobs + expect(jobInfo.lastSettled).to.equal((CURRENT_TIMESTAMP).toString()); + + // job credit balance should be 0 + expect(await marketv1.jobCreditBalance(jobs[i])).to.equal(0); + } + + // withdrawal recipient + const withdrawalAmountExpected = TOTAL_DEPOSIT_AMOUNT.sub(totalSettledAmountExpected); + expect(await creditToken.balanceOf(await admin2.getAddress())).to.be.within(withdrawalAmountExpected.sub(JOB_RATE_1), withdrawalAmountExpected.add(JOB_RATE_1)); + + // Provider + expect(await token.balanceOf(await provider.getAddress())).to.be.within(totalSettledAmountExpected.sub(JOB_RATE_1), totalSettledAmountExpected.add(JOB_RATE_1)); + + // MarketV1 + expect(await creditToken.balanceOf(marketv1.address)).to.equal(0); + }); + }) }); \ No newline at end of file