From 0be331a7eab6da89aa408c4f1f4304b5efce6559 Mon Sep 17 00:00:00 2001 From: slywizz Date: Sat, 18 Oct 2025 02:53:22 +0900 Subject: [PATCH 1/2] add CollateralContributor contract --- contracts/CollateralContributor.sol | 75 +++++++++ test/CollateralContributor.test.ts | 248 ++++++++++++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 contracts/CollateralContributor.sol create mode 100644 test/CollateralContributor.test.ts diff --git a/contracts/CollateralContributor.sol b/contracts/CollateralContributor.sol new file mode 100644 index 0000000..030a046 --- /dev/null +++ b/contracts/CollateralContributor.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.30; + +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +contract CollateralContributor is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable { + mapping(address => mapping(address => uint256)) public contributorBalance; // miner => contributor => amount + mapping(address => uint256) public minerTotalCollateral; + mapping(address => uint256) public minerSlashedCollateral; + + uint256 public totalCollateral; + uint256 public slashedCollateral; + + error InsufficientBalance(); + error InsufficientTotalCollateral(); + error InvalidAddress(); + error InvalidAmount(); + + event ContributorDeposited(address indexed miner, address indexed contributor, uint256 amount); + event ContributorWithdrawn(address indexed miner, address indexed contributor, uint256 amount); + event ContributorSlashed(address indexed miner, address indexed contributor, uint256 amount); + + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + + function initialize(address initialOwner) public initializer { + __Ownable_init(initialOwner); + __UUPSUpgradeable_init(); + } + + function depositFor(address miner, address contributor, uint256 amount) external onlyOwner { + if (miner == address(0) || contributor == address(0)) revert InvalidAddress(); + if (amount == 0) revert InvalidAmount(); + + contributorBalance[miner][contributor] += amount; + minerTotalCollateral[miner] += amount; + totalCollateral += amount; + + emit ContributorDeposited(miner, contributor, amount); + } + + function withdrawFor(address miner, address contributor, uint256 amount) external onlyOwner { + if (miner == address(0) || contributor == address(0)) revert InvalidAddress(); + if (amount == 0) revert InvalidAmount(); + + uint256 bal = contributorBalance[miner][contributor]; + if (bal < amount) revert InsufficientBalance(); + if (totalCollateral < amount) revert InsufficientTotalCollateral(); + + contributorBalance[miner][contributor] = bal - amount; + minerTotalCollateral[miner] -= amount; + totalCollateral -= amount; + + emit ContributorWithdrawn(miner, contributor, amount); + } + + function slashFromContributor(address miner, address contributor, uint256 amount) external onlyOwner { + if (miner == address(0) || contributor == address(0)) revert InvalidAddress(); + if (amount == 0) revert InvalidAmount(); + + uint256 bal = contributorBalance[miner][contributor]; + if (bal < amount) revert InsufficientBalance(); + if (totalCollateral < amount) revert InsufficientTotalCollateral(); + + contributorBalance[miner][contributor] = bal - amount; + minerTotalCollateral[miner] -= amount; + minerSlashedCollateral[miner] += amount; + + totalCollateral -= amount; + slashedCollateral += amount; + + emit ContributorSlashed(miner, contributor, amount); + } +} diff --git a/test/CollateralContributor.test.ts b/test/CollateralContributor.test.ts new file mode 100644 index 0000000..e1f137e --- /dev/null +++ b/test/CollateralContributor.test.ts @@ -0,0 +1,248 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { ethers, upgrades } from "hardhat"; +import { CollateralContributor } from "../typechain-types"; + +describe("CollateralContributor", function () { + async function deployCollateralContributor() { + const [owner, miner1, miner2, contributor1, contributor2] = await ethers.getSigners(); + + // @ts-ignore + const Contract = await ethers.getContractFactory("CollateralContributor", owner); + const proxy = (await upgrades.deployProxy(Contract, [owner.address], { + initializer: "initialize", + })) as CollateralContributor; + + return { contract: proxy, owner, miner1, miner2, contributor1, contributor2 }; + } + + describe("Deployment", function () { + it("Should set the right owner", async function () { + const { contract, owner } = await loadFixture(deployCollateralContributor); + + expect(await contract.owner()).to.equal(owner.address); + }); + }); + + describe("Deposit", function () { + it("Should fail with unauthorized account", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); + + await expect(contract.connect(contributor1).depositFor(miner1.address, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount") + .withArgs(contributor1.address); + }); + + it("Should fail with invalid address", async function () { + const { contract, contributor1 } = await loadFixture(deployCollateralContributor); + + await expect(contract.depositFor(ethers.ZeroAddress, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "InvalidAddress"); + + await expect(contract.depositFor(contributor1.address, ethers.ZeroAddress, 100)) + .to.be.revertedWithCustomError(contract, "InvalidAddress"); + }); + + it("Should fail with invalid amount", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); + + await expect(contract.depositFor(miner1.address, contributor1.address, 0)) + .to.be.revertedWithCustomError(contract, "InvalidAmount"); + }); + + it("Should work", async function () { + const { contract, owner, miner1, miner2, contributor1, contributor2 } = await loadFixture(deployCollateralContributor); + + // First deposit + await contract.depositFor(miner1.address, contributor1.address, 100); + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(100); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(100); + expect(await contract.totalCollateral()).to.equal(100); + + // Second deposit for same miner by different sponsor + await contract.depositFor(miner1.address, contributor2.address, 200); + expect(await contract.contributorBalance(miner1.address, contributor2.address)).to.equal(200); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(300); + expect(await contract.totalCollateral()).to.equal(300); + + // Deposit for different miner + await contract.depositFor(miner2.address, contributor1.address, 150); + expect(await contract.contributorBalance(miner2.address, contributor1.address)).to.equal(150); + expect(await contract.minerTotalCollateral(miner2.address)).to.equal(150); + expect(await contract.totalCollateral()).to.equal(450); + + // Additional deposit for same miner-sponsor pair + await contract.depositFor(miner1.address, contributor1.address, 50); + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(150); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(350); + expect(await contract.totalCollateral()).to.equal(500); + }); + }); + + describe("Withdraw", function () { + it("Should fail with unauthorized account", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); + + await expect(contract.connect(contributor1).withdrawFor(miner1.address, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount") + .withArgs(contributor1.address); + }); + + it("Should fail with invalid address", async function () { + const { contract, contributor1 } = await loadFixture(deployCollateralContributor); + + await expect(contract.withdrawFor(ethers.ZeroAddress, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "InvalidAddress"); + + await expect(contract.withdrawFor(contributor1.address, ethers.ZeroAddress, 100)) + .to.be.revertedWithCustomError(contract, "InvalidAddress"); + }); + + it("Should fail with invalid amount", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); + + await expect(contract.withdrawFor(miner1.address, contributor1.address, 0)) + .to.be.revertedWithCustomError(contract, "InvalidAmount"); + }); + + it("Should fail with insufficient balance", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); + + await expect(contract.withdrawFor(miner1.address, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "InsufficientBalance"); + }); + + it("Should work", async function () { + const { contract, owner, miner1, miner2, contributor1, contributor2 } = await loadFixture(deployCollateralContributor); + + // Setup initial deposits + await contract.depositFor(miner1.address, contributor1.address, 200); + await contract.depositFor(miner1.address, contributor2.address, 200); + await contract.depositFor(miner2.address, contributor1.address, 150); + + // Withdraw from contributor1 for miner1 + await contract.withdrawFor(miner1.address, contributor1.address, 100); + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(100); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(300); + expect(await contract.totalCollateral()).to.equal(450); + + // Withdraw from contributor2 for miner1 + await contract.withdrawFor(miner1.address, contributor2.address, 50); + expect(await contract.contributorBalance(miner1.address, contributor2.address)).to.equal(150); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(250); + expect(await contract.totalCollateral()).to.equal(400); + + // Withdraw from contributor1 for miner2 + await contract.withdrawFor(miner2.address, contributor1.address, 150); + expect(await contract.contributorBalance(miner2.address, contributor1.address)).to.equal(0); + expect(await contract.minerTotalCollateral(miner2.address)).to.equal(0); + expect(await contract.totalCollateral()).to.equal(250); + + // Withdraw remaining from contributor1 for miner1 + await contract.withdrawFor(miner1.address, contributor1.address, 100); + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(0); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(150); + expect(await contract.totalCollateral()).to.equal(150); + }); + }); + + describe("Slash", function () { + it("Should fail with unauthorized account", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); + + await expect(contract.connect(contributor1).slashFromContributor(miner1.address, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount") + .withArgs(contributor1.address); + }); + + it("Should fail with invalid address", async function () { + const { contract, contributor1 } = await loadFixture(deployCollateralContributor); + + await expect(contract.slashFromContributor(ethers.ZeroAddress, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "InvalidAddress"); + + await expect(contract.slashFromContributor(contributor1.address, ethers.ZeroAddress, 100)) + .to.be.revertedWithCustomError(contract, "InvalidAddress"); + }); + + it("Should fail with invalid amount", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); + + await expect(contract.slashFromContributor(miner1.address, contributor1.address, 0)) + .to.be.revertedWithCustomError(contract, "InvalidAmount"); + }); + + it("Should fail with insufficient balance", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); + + await expect(contract.slashFromContributor(miner1.address, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "InsufficientBalance"); + }); + + it("Should work", async function () { + const { contract, owner, miner1, miner2, contributor1, contributor2 } = await loadFixture(deployCollateralContributor); + + // Setup initial deposits + await contract.depositFor(miner1.address, contributor1.address, 200); + await contract.depositFor(miner1.address, contributor2.address, 200); + await contract.depositFor(miner2.address, contributor1.address, 150); + + // Initial state: miner1=400, miner2=150, total=550 + expect(await contract.totalCollateral()).to.equal(550); + + // Slash from contributor1 for miner1 (100) + await contract.slashFromContributor(miner1.address, contributor1.address, 100); + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(100); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(300); + expect(await contract.minerSlashedCollateral(miner1.address)).to.equal(100); + expect(await contract.slashedCollateral()).to.equal(100); + expect(await contract.totalCollateral()).to.equal(450); + + // Slash from contributor2 for miner1 (50) + await contract.slashFromContributor(miner1.address, contributor2.address, 50); + expect(await contract.contributorBalance(miner1.address, contributor2.address)).to.equal(150); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(250); + expect(await contract.minerSlashedCollateral(miner1.address)).to.equal(150); + expect(await contract.slashedCollateral()).to.equal(150); + expect(await contract.totalCollateral()).to.equal(400); + + // Slash from contributor1 for miner2 (150) + await contract.slashFromContributor(miner2.address, contributor1.address, 150); + expect(await contract.contributorBalance(miner2.address, contributor1.address)).to.equal(0); + expect(await contract.minerTotalCollateral(miner2.address)).to.equal(0); + expect(await contract.minerSlashedCollateral(miner2.address)).to.equal(150); + expect(await contract.slashedCollateral()).to.equal(300); + expect(await contract.totalCollateral()).to.equal(250); + + // Slash remaining from contributor1 for miner1 (100) + await contract.slashFromContributor(miner1.address, contributor1.address, 100); + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(0); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(150); + expect(await contract.minerSlashedCollateral(miner1.address)).to.equal(250); + expect(await contract.slashedCollateral()).to.equal(400); + expect(await contract.totalCollateral()).to.equal(150); + }); + }); + + describe("View Functions", function () { + it("Should return correct balances", async function () { + const { contract, miner1, contributor1, contributor2 } = await loadFixture(deployCollateralContributor); + + await contract.depositFor(miner1.address, contributor1.address, 100); + await contract.depositFor(miner1.address, contributor2.address, 200); + + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(100); + expect(await contract.contributorBalance(miner1.address, contributor2.address)).to.equal(200); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(300); + expect(await contract.totalCollateral()).to.equal(300); + }); + + it("Should return zero for non-existent balances", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); + + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(0); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(0); + expect(await contract.minerSlashedCollateral(miner1.address)).to.equal(0); + }); + }); +}); From 372dd60ebadb643e8b46c6e284b7fa712cf59e2d Mon Sep 17 00:00:00 2001 From: slywizz Date: Fri, 31 Oct 2025 02:09:48 +0900 Subject: [PATCH 2/2] merge CollateralContributor into Collateral --- contracts/Collateral.sol | 78 +---- contracts/Collateral_V1.sol | 77 +++++ ...teralContributor.sol => Collateral_V2.sol} | 91 +++++- scripts/deploy.ts | 3 +- scripts/local/simulate-upgrade.ts | 39 +++ scripts/local/validate-upgrade.ts | 24 ++ test/Collateral.test.ts | 309 ++++++++++++++++++ test/CollateralContributor.test.ts | 248 -------------- 8 files changed, 536 insertions(+), 333 deletions(-) create mode 100644 contracts/Collateral_V1.sol rename contracts/{CollateralContributor.sol => Collateral_V2.sol} (51%) create mode 100644 scripts/local/simulate-upgrade.ts create mode 100644 scripts/local/validate-upgrade.ts delete mode 100644 test/CollateralContributor.test.ts diff --git a/contracts/Collateral.sol b/contracts/Collateral.sol index 32fd22a..23691d6 100644 --- a/contracts/Collateral.sol +++ b/contracts/Collateral.sol @@ -1,77 +1,5 @@ -// SPDX-License-Identifier: UNLINCENSED +// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.30; -import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; - -contract Collateral is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable { - mapping(address => uint256) public collateralBalances; - uint256 public slashedCollateral; - uint256 public totalCollateral; - - error InsufficientBalance(); - error InsufficientTotalCollateral(); - error InvalidAddress(); - error InvalidAmount(); - - event CollateralDeposited(address indexed account, uint256 amount); - event CollateralSlashed(address indexed account, uint256 amount); - event CollateralWithdrawn(address indexed account, uint256 amount); - - // solhint-disable-next-line no-empty-blocks - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} - - function initialize(address initialOwner) public initializer { - __Ownable_init(initialOwner); - __UUPSUpgradeable_init(); - } - - function balanceOf(address account) external view returns (uint256) { - require(account != address(0), InvalidAddress()); - - return collateralBalances[account]; - } - - function deposit(address account, uint256 amount) external onlyOwner { - require(account != address(0), InvalidAddress()); - require(amount > 0, InvalidAmount()); - - collateralBalances[account] += amount; - totalCollateral += amount; - - emit CollateralDeposited(account, amount); - } - - function getSlashedCollateral() external view returns (uint256) { - return slashedCollateral; - } - - function getTotalCollateral() external view returns (uint256) { - return totalCollateral; - } - - function slash(address account, uint256 amount) external onlyOwner { - require(account != address(0), InvalidAddress()); - require(amount > 0, InvalidAmount()); - require(collateralBalances[account] >= amount, InsufficientBalance()); - - collateralBalances[account] -= amount; - slashedCollateral += amount; - totalCollateral -= amount; - - emit CollateralSlashed(account, amount); - } - - function withdraw(address account, uint256 amount) external onlyOwner { - require(account != address(0), InvalidAddress()); - require(amount > 0, InvalidAmount()); - require(collateralBalances[account] >= amount, InsufficientBalance()); - require(totalCollateral >= amount, InsufficientTotalCollateral()); - - collateralBalances[account] -= amount; - totalCollateral -= amount; - - emit CollateralWithdrawn(account, amount); - } -} +import "./Collateral_V2.sol"; +contract Collateral is Collateral_V2 {} \ No newline at end of file diff --git a/contracts/Collateral_V1.sol b/contracts/Collateral_V1.sol new file mode 100644 index 0000000..790bad8 --- /dev/null +++ b/contracts/Collateral_V1.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLINCENSED +pragma solidity 0.8.30; + +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +contract Collateral_V1 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable { + mapping(address => uint256) public collateralBalances; + uint256 public slashedCollateral; + uint256 public totalCollateral; + + error InsufficientBalance(); + error InsufficientTotalCollateral(); + error InvalidAddress(); + error InvalidAmount(); + + event CollateralDeposited(address indexed account, uint256 amount); + event CollateralSlashed(address indexed account, uint256 amount); + event CollateralWithdrawn(address indexed account, uint256 amount); + + // solhint-disable-next-line no-empty-blocks + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + + function initialize(address initialOwner) public initializer { + __Ownable_init(initialOwner); + __UUPSUpgradeable_init(); + } + + function balanceOf(address account) external view returns (uint256) { + require(account != address(0), InvalidAddress()); + + return collateralBalances[account]; + } + + function deposit(address account, uint256 amount) external onlyOwner { + require(account != address(0), InvalidAddress()); + require(amount > 0, InvalidAmount()); + + collateralBalances[account] += amount; + totalCollateral += amount; + + emit CollateralDeposited(account, amount); + } + + function getSlashedCollateral() external view returns (uint256) { + return slashedCollateral; + } + + function getTotalCollateral() external view returns (uint256) { + return totalCollateral; + } + + function slash(address account, uint256 amount) external onlyOwner { + require(account != address(0), InvalidAddress()); + require(amount > 0, InvalidAmount()); + require(collateralBalances[account] >= amount, InsufficientBalance()); + + collateralBalances[account] -= amount; + slashedCollateral += amount; + totalCollateral -= amount; + + emit CollateralSlashed(account, amount); + } + + function withdraw(address account, uint256 amount) external onlyOwner { + require(account != address(0), InvalidAddress()); + require(amount > 0, InvalidAmount()); + require(collateralBalances[account] >= amount, InsufficientBalance()); + require(totalCollateral >= amount, InsufficientTotalCollateral()); + + collateralBalances[account] -= amount; + totalCollateral -= amount; + + emit CollateralWithdrawn(account, amount); + } +} diff --git a/contracts/CollateralContributor.sol b/contracts/Collateral_V2.sol similarity index 51% rename from contracts/CollateralContributor.sol rename to contracts/Collateral_V2.sol index 030a046..54a271a 100644 --- a/contracts/CollateralContributor.sol +++ b/contracts/Collateral_V2.sol @@ -5,23 +5,43 @@ import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -contract CollateralContributor is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable { +contract Collateral_V2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable { + // ============================================================= + // ======================= STORAGE LAYOUT ====================== + // ============================================================= + + // --- [V1] Direct collateral storage --- + mapping(address => uint256) public collateralBalances; + uint256 public slashedCollateral; + uint256 public totalCollateral; + + // --- [V2] Delegated collateral storage --- mapping(address => mapping(address => uint256)) public contributorBalance; // miner => contributor => amount mapping(address => uint256) public minerTotalCollateral; mapping(address => uint256) public minerSlashedCollateral; - uint256 public totalCollateral; - uint256 public slashedCollateral; + uint256 public totalDelegatedCollateral; + uint256 public totalDelegatedSlashedCollateral; + + // --- [Reserved for future upgrades] --- + uint256[44] private __gap; + + // ======================= STORAGE LAYOUT END ================== error InsufficientBalance(); error InsufficientTotalCollateral(); error InvalidAddress(); error InvalidAmount(); + event CollateralDeposited(address indexed account, uint256 amount); + event CollateralSlashed(address indexed account, uint256 amount); + event CollateralWithdrawn(address indexed account, uint256 amount); + event ContributorDeposited(address indexed miner, address indexed contributor, uint256 amount); event ContributorWithdrawn(address indexed miner, address indexed contributor, uint256 amount); event ContributorSlashed(address indexed miner, address indexed contributor, uint256 amount); + // solhint-disable-next-line no-empty-blocks function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} function initialize(address initialOwner) public initializer { @@ -29,13 +49,68 @@ contract CollateralContributor is Initializable, Ownable2StepUpgradeable, UUPSUp __UUPSUpgradeable_init(); } + function balanceOf(address account) external view returns (uint256) { + if (account == address(0)) revert InvalidAddress(); + + return collateralBalances[account]; + } + + function deposit(address account, uint256 amount) external onlyOwner { + if (account == address(0)) revert InvalidAddress(); + if (amount == 0) revert InvalidAmount(); + + collateralBalances[account] += amount; + totalCollateral += amount; + + emit CollateralDeposited(account, amount); + } + + function getSlashedCollateral() external view returns (uint256) { + return slashedCollateral; + } + + function getTotalCollateral() external view returns (uint256) { + return totalCollateral; + } + + function totalAllCollateral() external view returns (uint256) { + return totalCollateral + totalDelegatedCollateral; + } + function totalAllSlashed() external view returns (uint256) { + return slashedCollateral + totalDelegatedSlashedCollateral; + } + + function slash(address account, uint256 amount) external onlyOwner { + if (account == address(0)) revert InvalidAddress(); + if (amount == 0) revert InvalidAmount(); + if (collateralBalances[account] < amount) revert InsufficientBalance(); + + collateralBalances[account] -= amount; + slashedCollateral += amount; + totalCollateral -= amount; + + emit CollateralSlashed(account, amount); + } + + function withdraw(address account, uint256 amount) external onlyOwner { + if (account == address(0)) revert InvalidAddress(); + if (amount == 0) revert InvalidAmount(); + if (collateralBalances[account] < amount) revert InsufficientBalance(); + if (totalCollateral < amount) revert InsufficientTotalCollateral(); + + collateralBalances[account] -= amount; + totalCollateral -= amount; + + emit CollateralWithdrawn(account, amount); + } + function depositFor(address miner, address contributor, uint256 amount) external onlyOwner { if (miner == address(0) || contributor == address(0)) revert InvalidAddress(); if (amount == 0) revert InvalidAmount(); contributorBalance[miner][contributor] += amount; minerTotalCollateral[miner] += amount; - totalCollateral += amount; + totalDelegatedCollateral += amount; emit ContributorDeposited(miner, contributor, amount); } @@ -46,11 +121,10 @@ contract CollateralContributor is Initializable, Ownable2StepUpgradeable, UUPSUp uint256 bal = contributorBalance[miner][contributor]; if (bal < amount) revert InsufficientBalance(); - if (totalCollateral < amount) revert InsufficientTotalCollateral(); contributorBalance[miner][contributor] = bal - amount; minerTotalCollateral[miner] -= amount; - totalCollateral -= amount; + totalDelegatedCollateral -= amount; emit ContributorWithdrawn(miner, contributor, amount); } @@ -61,14 +135,13 @@ contract CollateralContributor is Initializable, Ownable2StepUpgradeable, UUPSUp uint256 bal = contributorBalance[miner][contributor]; if (bal < amount) revert InsufficientBalance(); - if (totalCollateral < amount) revert InsufficientTotalCollateral(); contributorBalance[miner][contributor] = bal - amount; minerTotalCollateral[miner] -= amount; minerSlashedCollateral[miner] += amount; - totalCollateral -= amount; - slashedCollateral += amount; + totalDelegatedCollateral -= amount; + totalDelegatedSlashedCollateral += amount; emit ContributorSlashed(miner, contributor, amount); } diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 4015e54..cff6f08 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -4,7 +4,8 @@ async function main() { const [owner] = await ethers.getSigners(); const Contract = await ethers.getContractFactory("Collateral"); - const proxy = await upgrades.deployProxy(Contract, [owner.address], { initializer: "initialize" }); + const proxy = await upgrades.deployProxy(Contract, [owner.address], { initializer: "initialize", + kind: "uups", }); await proxy.waitForDeployment(); console.log("Contract deployed at: ", await proxy.getAddress()); diff --git a/scripts/local/simulate-upgrade.ts b/scripts/local/simulate-upgrade.ts new file mode 100644 index 0000000..d2da0b2 --- /dev/null +++ b/scripts/local/simulate-upgrade.ts @@ -0,0 +1,39 @@ +import { ethers, upgrades } from "hardhat"; + +async function main() { + const [deployer] = await ethers.getSigners(); + + // 1. 기존 V1 배포 + const CollateralV1 = await ethers.getContractFactory("Collateral_V1", deployer); + const proxy = await upgrades.deployProxy(CollateralV1, [deployer.address], { + initializer: "initialize", + kind: "uups", + }); + await proxy.waitForDeployment(); + + console.log("✅ Deployed Collateral V1 Proxy:", await proxy.getAddress()); + + // 2. 상태 세팅 + await proxy.deposit(deployer.address, 100n); + console.log("Before upgrade: total =", (await proxy.getTotalCollateral()).toString()); + + // 3. 새 구현 업그레이드 시뮬레이션 (같은 컨트랙트를 V2로 가정) + const CollateralV2 = await ethers.getContractFactory("Collateral", deployer); + const upgraded = await upgrades.upgradeProxy(await proxy.getAddress(), CollateralV2, { kind: "uups" }); + console.log("✅ Proxy upgraded to new implementation"); + + // 4. 업그레이드 후 상태 유지 확인 + const totalAfter = await upgraded.getTotalCollateral(); + console.log("After upgrade: total =", totalAfter.toString()); + + if (totalAfter === 100n) { + console.log("✅ State preserved successfully"); + } else { + console.error("❌ State mismatch!"); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/local/validate-upgrade.ts b/scripts/local/validate-upgrade.ts new file mode 100644 index 0000000..9101e57 --- /dev/null +++ b/scripts/local/validate-upgrade.ts @@ -0,0 +1,24 @@ +import { ethers, upgrades } from "hardhat"; + +async function main() { + // 1. 먼저 V1 배포 + const [deployer] = await ethers.getSigners(); + const CollateralV1 = await ethers.getContractFactory("Collateral_V1", deployer); + const proxy = await upgrades.deployProxy(CollateralV1, [deployer.address], { + initializer: "initialize", + kind: "uups", + }); + await proxy.waitForDeployment(); + + console.log("✅ Deployed Collateral V1 Proxy at:", await proxy.getAddress()); + + const CollateralV2 = await ethers.getContractFactory("Collateral", deployer); + await upgrades.validateUpgrade(await proxy.getAddress(), CollateralV2, { kind: "uups" }); + + console.log("✅ Storage layout validation passed (local)"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); \ No newline at end of file diff --git a/test/Collateral.test.ts b/test/Collateral.test.ts index fdfca87..c4b5153 100644 --- a/test/Collateral.test.ts +++ b/test/Collateral.test.ts @@ -130,4 +130,313 @@ describe("Collateral", function () { expect(await contract.getTotalCollateral()).to.equal(0); }); }); + + // ======================= Delegated (Contributor) path tests ======================= + async function deployCollateralForDelegated() { + const [owner, miner1, miner2, contributor1, contributor2] = await ethers.getSigners(); + + // @ts-ignore + const Contract = await ethers.getContractFactory("Collateral", owner); + const proxy = (await upgrades.deployProxy(Contract, [owner.address], { + initializer: "initialize", + })) as Collateral; + + return { contract: proxy, owner, miner1, miner2, contributor1, contributor2 }; + } + + describe("Delegated Collateral", function () { + describe("Deployment", function () { + it("Should set the right owner", async function () { + const { contract, owner } = await loadFixture(deployCollateralForDelegated); + + expect(await contract.owner()).to.equal(owner.address); + }); + }); + + describe("DepositFor (delegated)", function () { + it("Should fail with unauthorized account", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralForDelegated); + + await expect(contract.connect(contributor1).depositFor(miner1.address, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount") + .withArgs(contributor1.address); + }); + + it("Should fail with invalid address", async function () { + const { contract, contributor1 } = await loadFixture(deployCollateralForDelegated); + + await expect(contract.depositFor(ethers.ZeroAddress, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "InvalidAddress"); + + await expect(contract.depositFor(contributor1.address, ethers.ZeroAddress, 100)) + .to.be.revertedWithCustomError(contract, "InvalidAddress"); + }); + + it("Should fail with invalid amount", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralForDelegated); + + await expect(contract.depositFor(miner1.address, contributor1.address, 0)) + .to.be.revertedWithCustomError(contract, "InvalidAmount"); + }); + + it("Should work", async function () { + const { contract, miner1, miner2, contributor1, contributor2 } = await loadFixture(deployCollateralForDelegated); + + // First deposit + await contract.depositFor(miner1.address, contributor1.address, 100); + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(100); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(100); + expect(await contract.totalDelegatedCollateral()).to.equal(100); + + // Second deposit for same miner by different contributor + await contract.depositFor(miner1.address, contributor2.address, 200); + expect(await contract.contributorBalance(miner1.address, contributor2.address)).to.equal(200); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(300); + expect(await contract.totalDelegatedCollateral()).to.equal(300); + + // Deposit for different miner + await contract.depositFor(miner2.address, contributor1.address, 150); + expect(await contract.contributorBalance(miner2.address, contributor1.address)).to.equal(150); + expect(await contract.minerTotalCollateral(miner2.address)).to.equal(150); + expect(await contract.totalDelegatedCollateral()).to.equal(450); + + // Additional deposit for same miner-contributor pair + await contract.depositFor(miner1.address, contributor1.address, 50); + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(150); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(350); + expect(await contract.totalDelegatedCollateral()).to.equal(500); + }); + }); + + describe("WithdrawFor (delegated)", function () { + it("Should fail with unauthorized account", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralForDelegated); + + await expect(contract.connect(contributor1).withdrawFor(miner1.address, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount") + .withArgs(contributor1.address); + }); + + it("Should fail with invalid address", async function () { + const { contract, contributor1 } = await loadFixture(deployCollateralForDelegated); + + await expect(contract.withdrawFor(ethers.ZeroAddress, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "InvalidAddress"); + + await expect(contract.withdrawFor(contributor1.address, ethers.ZeroAddress, 100)) + .to.be.revertedWithCustomError(contract, "InvalidAddress"); + }); + + it("Should fail with invalid amount", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralForDelegated); + + await expect(contract.withdrawFor(miner1.address, contributor1.address, 0)) + .to.be.revertedWithCustomError(contract, "InvalidAmount"); + }); + + it("Should fail with insufficient balance", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralForDelegated); + + await expect(contract.withdrawFor(miner1.address, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "InsufficientBalance"); + }); + + it("Should work", async function () { + const { contract, miner1, miner2, contributor1, contributor2 } = await loadFixture(deployCollateralForDelegated); + + // Setup initial deposits + await contract.depositFor(miner1.address, contributor1.address, 200); + await contract.depositFor(miner1.address, contributor2.address, 200); + await contract.depositFor(miner2.address, contributor1.address, 150); + + // Withdraw from contributor1 for miner1 + await contract.withdrawFor(miner1.address, contributor1.address, 100); + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(100); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(300); + expect(await contract.totalDelegatedCollateral()).to.equal(450); + + // Withdraw from contributor2 for miner1 + await contract.withdrawFor(miner1.address, contributor2.address, 50); + expect(await contract.contributorBalance(miner1.address, contributor2.address)).to.equal(150); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(250); + expect(await contract.totalDelegatedCollateral()).to.equal(400); + + // Withdraw from contributor1 for miner2 + await contract.withdrawFor(miner2.address, contributor1.address, 150); + expect(await contract.contributorBalance(miner2.address, contributor1.address)).to.equal(0); + expect(await contract.minerTotalCollateral(miner2.address)).to.equal(0); + expect(await contract.totalDelegatedCollateral()).to.equal(250); + + // Withdraw remaining from contributor1 for miner1 + await contract.withdrawFor(miner1.address, contributor1.address, 100); + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(0); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(150); + expect(await contract.totalDelegatedCollateral()).to.equal(150); + }); + }); + + describe("SlashFromContributor (delegated)", function () { + it("Should fail with unauthorized account", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralForDelegated); + + await expect(contract.connect(contributor1).slashFromContributor(miner1.address, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount") + .withArgs(contributor1.address); + }); + + it("Should fail with invalid address", async function () { + const { contract, contributor1 } = await loadFixture(deployCollateralForDelegated); + + await expect(contract.slashFromContributor(ethers.ZeroAddress, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "InvalidAddress"); + + await expect(contract.slashFromContributor(contributor1.address, ethers.ZeroAddress, 100)) + .to.be.revertedWithCustomError(contract, "InvalidAddress"); + }); + + it("Should fail with invalid amount", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralForDelegated); + + await expect(contract.slashFromContributor(miner1.address, contributor1.address, 0)) + .to.be.revertedWithCustomError(contract, "InvalidAmount"); + }); + + it("Should fail with insufficient balance", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralForDelegated); + + await expect(contract.slashFromContributor(miner1.address, contributor1.address, 100)) + .to.be.revertedWithCustomError(contract, "InsufficientBalance"); + }); + + it("Should work", async function () { + const { contract, miner1, miner2, contributor1, contributor2 } = await loadFixture(deployCollateralForDelegated); + + // Setup initial deposits + await contract.depositFor(miner1.address, contributor1.address, 200); + await contract.depositFor(miner1.address, contributor2.address, 200); + await contract.depositFor(miner2.address, contributor1.address, 150); + + // Initial state: miner1=400, miner2=150, totalDelegated=550 + expect(await contract.totalDelegatedCollateral()).to.equal(550); + + // Slash from contributor1 for miner1 (100) + await contract.slashFromContributor(miner1.address, contributor1.address, 100); + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(100); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(300); + expect(await contract.minerSlashedCollateral(miner1.address)).to.equal(100); + expect(await contract.totalDelegatedSlashedCollateral()).to.equal(100); + expect(await contract.totalDelegatedCollateral()).to.equal(450); + + // Slash from contributor2 for miner1 (50) + await contract.slashFromContributor(miner1.address, contributor2.address, 50); + expect(await contract.contributorBalance(miner1.address, contributor2.address)).to.equal(150); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(250); + expect(await contract.minerSlashedCollateral(miner1.address)).to.equal(150); + expect(await contract.totalDelegatedSlashedCollateral()).to.equal(150); + expect(await contract.totalDelegatedCollateral()).to.equal(400); + + // Slash from contributor1 for miner2 (150) + await contract.slashFromContributor(miner2.address, contributor1.address, 150); + expect(await contract.contributorBalance(miner2.address, contributor1.address)).to.equal(0); + expect(await contract.minerTotalCollateral(miner2.address)).to.equal(0); + expect(await contract.minerSlashedCollateral(miner2.address)).to.equal(150); + expect(await contract.totalDelegatedSlashedCollateral()).to.equal(300); + expect(await contract.totalDelegatedCollateral()).to.equal(250); + + // Slash remaining from contributor1 for miner1 (100) + await contract.slashFromContributor(miner1.address, contributor1.address, 100); + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(0); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(150); + expect(await contract.minerSlashedCollateral(miner1.address)).to.equal(250); + expect(await contract.totalDelegatedSlashedCollateral()).to.equal(400); + expect(await contract.totalDelegatedCollateral()).to.equal(150); + }); + }); + + describe("View Functions (delegated)", function () { + it("Should return correct balances", async function () { + const { contract, miner1, contributor1, contributor2 } = await loadFixture(deployCollateralForDelegated); + + await contract.depositFor(miner1.address, contributor1.address, 100); + await contract.depositFor(miner1.address, contributor2.address, 200); + + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(100); + expect(await contract.contributorBalance(miner1.address, contributor2.address)).to.equal(200); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(300); + expect(await contract.totalDelegatedCollateral()).to.equal(300); + }); + + it("Should return zero for non-existent balances", async function () { + const { contract, miner1, contributor1 } = await loadFixture(deployCollateralForDelegated); + + expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(0); + expect(await contract.minerTotalCollateral(miner1.address)).to.equal(0); + expect(await contract.minerSlashedCollateral(miner1.address)).to.equal(0); + }); + }); + + describe("Aggregate View Functions", function () { + async function deployCollateralWithMixedDeposits() { + const [owner, user1, miner1, contributor1] = await ethers.getSigners(); + + // @ts-ignore + const Contract = await ethers.getContractFactory("Collateral", owner); + const proxy = (await upgrades.deployProxy(Contract, [owner.address], { + initializer: "initialize", + })) as Collateral; + + return { contract: proxy, owner, user1, miner1, contributor1 }; + } + + it("Should return combined totals for direct and delegated collateral", async function () { + const { contract, owner, user1, miner1, contributor1 } = await loadFixture(deployCollateralWithMixedDeposits); + + // Deposit direct collateral + await contract.deposit(user1.address, 200); + expect(await contract.getTotalCollateral()).to.equal(200); + expect(await contract.totalDelegatedCollateral()).to.equal(0); + expect(await contract.totalAllCollateral()).to.equal(200); + + // Deposit delegated collateral + await contract.depositFor(miner1.address, contributor1.address, 100); + expect(await contract.getTotalCollateral()).to.equal(200); + expect(await contract.totalDelegatedCollateral()).to.equal(100); + expect(await contract.totalAllCollateral()).to.equal(300); + + // Add more of both + await contract.deposit(user1.address, 50); + await contract.depositFor(miner1.address, contributor1.address, 150); + expect(await contract.getTotalCollateral()).to.equal(250); + expect(await contract.totalDelegatedCollateral()).to.equal(250); + expect(await contract.totalAllCollateral()).to.equal(500); + }); + + it("Should return combined slashed totals", async function () { + const { contract, owner, user1, miner1, contributor1 } = await loadFixture(deployCollateralWithMixedDeposits); + + // Setup deposits + await contract.deposit(user1.address, 200); + await contract.depositFor(miner1.address, contributor1.address, 200); + + // Slash from direct collateral + await contract.slash(user1.address, 50); + expect(await contract.getSlashedCollateral()).to.equal(50); + expect(await contract.totalDelegatedSlashedCollateral()).to.equal(0); + expect(await contract.totalAllSlashed()).to.equal(50); + + // Slash from delegated collateral + await contract.slashFromContributor(miner1.address, contributor1.address, 100); + expect(await contract.getSlashedCollateral()).to.equal(50); + expect(await contract.totalDelegatedSlashedCollateral()).to.equal(100); + expect(await contract.totalAllSlashed()).to.equal(150); + + // More slashing + await contract.slash(user1.address, 100); + await contract.slashFromContributor(miner1.address, contributor1.address, 50); + expect(await contract.getSlashedCollateral()).to.equal(150); + expect(await contract.totalDelegatedSlashedCollateral()).to.equal(150); + expect(await contract.totalAllSlashed()).to.equal(300); + }); + }); + }); }); diff --git a/test/CollateralContributor.test.ts b/test/CollateralContributor.test.ts deleted file mode 100644 index e1f137e..0000000 --- a/test/CollateralContributor.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; -import { expect } from "chai"; -import { ethers, upgrades } from "hardhat"; -import { CollateralContributor } from "../typechain-types"; - -describe("CollateralContributor", function () { - async function deployCollateralContributor() { - const [owner, miner1, miner2, contributor1, contributor2] = await ethers.getSigners(); - - // @ts-ignore - const Contract = await ethers.getContractFactory("CollateralContributor", owner); - const proxy = (await upgrades.deployProxy(Contract, [owner.address], { - initializer: "initialize", - })) as CollateralContributor; - - return { contract: proxy, owner, miner1, miner2, contributor1, contributor2 }; - } - - describe("Deployment", function () { - it("Should set the right owner", async function () { - const { contract, owner } = await loadFixture(deployCollateralContributor); - - expect(await contract.owner()).to.equal(owner.address); - }); - }); - - describe("Deposit", function () { - it("Should fail with unauthorized account", async function () { - const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); - - await expect(contract.connect(contributor1).depositFor(miner1.address, contributor1.address, 100)) - .to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount") - .withArgs(contributor1.address); - }); - - it("Should fail with invalid address", async function () { - const { contract, contributor1 } = await loadFixture(deployCollateralContributor); - - await expect(contract.depositFor(ethers.ZeroAddress, contributor1.address, 100)) - .to.be.revertedWithCustomError(contract, "InvalidAddress"); - - await expect(contract.depositFor(contributor1.address, ethers.ZeroAddress, 100)) - .to.be.revertedWithCustomError(contract, "InvalidAddress"); - }); - - it("Should fail with invalid amount", async function () { - const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); - - await expect(contract.depositFor(miner1.address, contributor1.address, 0)) - .to.be.revertedWithCustomError(contract, "InvalidAmount"); - }); - - it("Should work", async function () { - const { contract, owner, miner1, miner2, contributor1, contributor2 } = await loadFixture(deployCollateralContributor); - - // First deposit - await contract.depositFor(miner1.address, contributor1.address, 100); - expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(100); - expect(await contract.minerTotalCollateral(miner1.address)).to.equal(100); - expect(await contract.totalCollateral()).to.equal(100); - - // Second deposit for same miner by different sponsor - await contract.depositFor(miner1.address, contributor2.address, 200); - expect(await contract.contributorBalance(miner1.address, contributor2.address)).to.equal(200); - expect(await contract.minerTotalCollateral(miner1.address)).to.equal(300); - expect(await contract.totalCollateral()).to.equal(300); - - // Deposit for different miner - await contract.depositFor(miner2.address, contributor1.address, 150); - expect(await contract.contributorBalance(miner2.address, contributor1.address)).to.equal(150); - expect(await contract.minerTotalCollateral(miner2.address)).to.equal(150); - expect(await contract.totalCollateral()).to.equal(450); - - // Additional deposit for same miner-sponsor pair - await contract.depositFor(miner1.address, contributor1.address, 50); - expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(150); - expect(await contract.minerTotalCollateral(miner1.address)).to.equal(350); - expect(await contract.totalCollateral()).to.equal(500); - }); - }); - - describe("Withdraw", function () { - it("Should fail with unauthorized account", async function () { - const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); - - await expect(contract.connect(contributor1).withdrawFor(miner1.address, contributor1.address, 100)) - .to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount") - .withArgs(contributor1.address); - }); - - it("Should fail with invalid address", async function () { - const { contract, contributor1 } = await loadFixture(deployCollateralContributor); - - await expect(contract.withdrawFor(ethers.ZeroAddress, contributor1.address, 100)) - .to.be.revertedWithCustomError(contract, "InvalidAddress"); - - await expect(contract.withdrawFor(contributor1.address, ethers.ZeroAddress, 100)) - .to.be.revertedWithCustomError(contract, "InvalidAddress"); - }); - - it("Should fail with invalid amount", async function () { - const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); - - await expect(contract.withdrawFor(miner1.address, contributor1.address, 0)) - .to.be.revertedWithCustomError(contract, "InvalidAmount"); - }); - - it("Should fail with insufficient balance", async function () { - const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); - - await expect(contract.withdrawFor(miner1.address, contributor1.address, 100)) - .to.be.revertedWithCustomError(contract, "InsufficientBalance"); - }); - - it("Should work", async function () { - const { contract, owner, miner1, miner2, contributor1, contributor2 } = await loadFixture(deployCollateralContributor); - - // Setup initial deposits - await contract.depositFor(miner1.address, contributor1.address, 200); - await contract.depositFor(miner1.address, contributor2.address, 200); - await contract.depositFor(miner2.address, contributor1.address, 150); - - // Withdraw from contributor1 for miner1 - await contract.withdrawFor(miner1.address, contributor1.address, 100); - expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(100); - expect(await contract.minerTotalCollateral(miner1.address)).to.equal(300); - expect(await contract.totalCollateral()).to.equal(450); - - // Withdraw from contributor2 for miner1 - await contract.withdrawFor(miner1.address, contributor2.address, 50); - expect(await contract.contributorBalance(miner1.address, contributor2.address)).to.equal(150); - expect(await contract.minerTotalCollateral(miner1.address)).to.equal(250); - expect(await contract.totalCollateral()).to.equal(400); - - // Withdraw from contributor1 for miner2 - await contract.withdrawFor(miner2.address, contributor1.address, 150); - expect(await contract.contributorBalance(miner2.address, contributor1.address)).to.equal(0); - expect(await contract.minerTotalCollateral(miner2.address)).to.equal(0); - expect(await contract.totalCollateral()).to.equal(250); - - // Withdraw remaining from contributor1 for miner1 - await contract.withdrawFor(miner1.address, contributor1.address, 100); - expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(0); - expect(await contract.minerTotalCollateral(miner1.address)).to.equal(150); - expect(await contract.totalCollateral()).to.equal(150); - }); - }); - - describe("Slash", function () { - it("Should fail with unauthorized account", async function () { - const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); - - await expect(contract.connect(contributor1).slashFromContributor(miner1.address, contributor1.address, 100)) - .to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount") - .withArgs(contributor1.address); - }); - - it("Should fail with invalid address", async function () { - const { contract, contributor1 } = await loadFixture(deployCollateralContributor); - - await expect(contract.slashFromContributor(ethers.ZeroAddress, contributor1.address, 100)) - .to.be.revertedWithCustomError(contract, "InvalidAddress"); - - await expect(contract.slashFromContributor(contributor1.address, ethers.ZeroAddress, 100)) - .to.be.revertedWithCustomError(contract, "InvalidAddress"); - }); - - it("Should fail with invalid amount", async function () { - const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); - - await expect(contract.slashFromContributor(miner1.address, contributor1.address, 0)) - .to.be.revertedWithCustomError(contract, "InvalidAmount"); - }); - - it("Should fail with insufficient balance", async function () { - const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); - - await expect(contract.slashFromContributor(miner1.address, contributor1.address, 100)) - .to.be.revertedWithCustomError(contract, "InsufficientBalance"); - }); - - it("Should work", async function () { - const { contract, owner, miner1, miner2, contributor1, contributor2 } = await loadFixture(deployCollateralContributor); - - // Setup initial deposits - await contract.depositFor(miner1.address, contributor1.address, 200); - await contract.depositFor(miner1.address, contributor2.address, 200); - await contract.depositFor(miner2.address, contributor1.address, 150); - - // Initial state: miner1=400, miner2=150, total=550 - expect(await contract.totalCollateral()).to.equal(550); - - // Slash from contributor1 for miner1 (100) - await contract.slashFromContributor(miner1.address, contributor1.address, 100); - expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(100); - expect(await contract.minerTotalCollateral(miner1.address)).to.equal(300); - expect(await contract.minerSlashedCollateral(miner1.address)).to.equal(100); - expect(await contract.slashedCollateral()).to.equal(100); - expect(await contract.totalCollateral()).to.equal(450); - - // Slash from contributor2 for miner1 (50) - await contract.slashFromContributor(miner1.address, contributor2.address, 50); - expect(await contract.contributorBalance(miner1.address, contributor2.address)).to.equal(150); - expect(await contract.minerTotalCollateral(miner1.address)).to.equal(250); - expect(await contract.minerSlashedCollateral(miner1.address)).to.equal(150); - expect(await contract.slashedCollateral()).to.equal(150); - expect(await contract.totalCollateral()).to.equal(400); - - // Slash from contributor1 for miner2 (150) - await contract.slashFromContributor(miner2.address, contributor1.address, 150); - expect(await contract.contributorBalance(miner2.address, contributor1.address)).to.equal(0); - expect(await contract.minerTotalCollateral(miner2.address)).to.equal(0); - expect(await contract.minerSlashedCollateral(miner2.address)).to.equal(150); - expect(await contract.slashedCollateral()).to.equal(300); - expect(await contract.totalCollateral()).to.equal(250); - - // Slash remaining from contributor1 for miner1 (100) - await contract.slashFromContributor(miner1.address, contributor1.address, 100); - expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(0); - expect(await contract.minerTotalCollateral(miner1.address)).to.equal(150); - expect(await contract.minerSlashedCollateral(miner1.address)).to.equal(250); - expect(await contract.slashedCollateral()).to.equal(400); - expect(await contract.totalCollateral()).to.equal(150); - }); - }); - - describe("View Functions", function () { - it("Should return correct balances", async function () { - const { contract, miner1, contributor1, contributor2 } = await loadFixture(deployCollateralContributor); - - await contract.depositFor(miner1.address, contributor1.address, 100); - await contract.depositFor(miner1.address, contributor2.address, 200); - - expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(100); - expect(await contract.contributorBalance(miner1.address, contributor2.address)).to.equal(200); - expect(await contract.minerTotalCollateral(miner1.address)).to.equal(300); - expect(await contract.totalCollateral()).to.equal(300); - }); - - it("Should return zero for non-existent balances", async function () { - const { contract, miner1, contributor1 } = await loadFixture(deployCollateralContributor); - - expect(await contract.contributorBalance(miner1.address, contributor1.address)).to.equal(0); - expect(await contract.minerTotalCollateral(miner1.address)).to.equal(0); - expect(await contract.minerSlashedCollateral(miner1.address)).to.equal(0); - }); - }); -});