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/Collateral_V2.sol b/contracts/Collateral_V2.sol new file mode 100644 index 0000000..54a271a --- /dev/null +++ b/contracts/Collateral_V2.sol @@ -0,0 +1,148 @@ +// 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_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 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 { + __Ownable_init(initialOwner); + __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; + totalDelegatedCollateral += 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(); + + contributorBalance[miner][contributor] = bal - amount; + minerTotalCollateral[miner] -= amount; + totalDelegatedCollateral -= 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(); + + contributorBalance[miner][contributor] = bal - amount; + minerTotalCollateral[miner] -= amount; + minerSlashedCollateral[miner] += 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); + }); + }); + }); });