From 76c811978a34267ffb96322cc40c5c10211747d5 Mon Sep 17 00:00:00 2001 From: Sean Darcy Date: Thu, 8 May 2025 11:19:13 +1000 Subject: [PATCH] add pause functionality --- contracts/utils/TokenConverter.sol | 15 ++++-- test/unit-js/TokenConverter.js | 76 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/contracts/utils/TokenConverter.sol b/contracts/utils/TokenConverter.sol index 4a0243c..66968c7 100644 --- a/contracts/utils/TokenConverter.sol +++ b/contracts/utils/TokenConverter.sol @@ -4,8 +4,9 @@ pragma solidity ^0.8.26; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; -contract TokenConverter is Ownable, ReentrancyGuard { +contract TokenConverter is Ownable, ReentrancyGuard, Pausable { using SafeERC20 for IERC20; IERC20 public immutable tokenA; @@ -23,7 +24,7 @@ contract TokenConverter is Ownable, ReentrancyGuard { address _tokenB, uint256 _initialNumerator, uint256 _initialDenominator - ) Ownable(msg.sender) { + ) Ownable(msg.sender) Pausable() { require(_tokenA != address(0) && _tokenB != address(0), "Invalid token address"); require(_initialNumerator > 0 && _initialDenominator > 0, "Conversion rate must be greater than 0"); @@ -53,7 +54,7 @@ contract TokenConverter is Ownable, ReentrancyGuard { tokenB.safeTransfer(msg.sender, _amount); } - function convertTokens(uint256 _amountA) external nonReentrant { + function convertTokens(uint256 _amountA) external nonReentrant whenNotPaused { require(_amountA > 0, "Amount must be greater than 0"); uint256 amountB = (_amountA * conversionRateNumerator) / conversionRateDenominator; require(tokenB.balanceOf(address(this)) >= amountB, "Insufficient Token B in contract"); @@ -61,4 +62,12 @@ contract TokenConverter is Ownable, ReentrancyGuard { tokenA.safeTransferFrom(msg.sender, address(this), _amountA); tokenB.safeTransfer(msg.sender, amountB); } + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } } diff --git a/test/unit-js/TokenConverter.js b/test/unit-js/TokenConverter.js index d5c2109..778781c 100644 --- a/test/unit-js/TokenConverter.js +++ b/test/unit-js/TokenConverter.js @@ -166,4 +166,80 @@ describe("TokenConverter Contract Tests", function () { .to.be.revertedWith("Insufficient Token B in contract"); }); }); + + describe("Pausable functionality", function () { + beforeEach(async function () { + // Ensure contract is seeded with TokenB for conversion tests + let testAmountInContract = ethers.parseUnits("10000", decimalsTokenB); + await tokenBERC20.approve(tokenConverter, bigAtomicTestAmountInContract); + await tokenConverter.depositTokenB(bigAtomicTestAmountInContract); + }); + + it("Should allow owner to pause and unpause", async function () { + expect(await tokenConverter.paused()).to.be.false; + await expect(tokenConverter.connect(owner).pause()) + .to.emit(tokenConverter, "Paused") + .withArgs(owner.address); + expect(await tokenConverter.paused()).to.be.true; + await expect(tokenConverter.connect(owner).unpause()) + .to.emit(tokenConverter, "Unpaused") + .withArgs(owner.address); + expect(await tokenConverter.paused()).to.be.false; + }); + + it("Should not allow non-owner to pause or unpause", async function () { + await expect(tokenConverter.connect(user).pause()) + .to.be.revertedWithCustomError(tokenConverter, "OwnableUnauthorizedAccount") + .withArgs(user.address); + await expect(tokenConverter.connect(user).unpause()) + .to.be.revertedWithCustomError(tokenConverter, "OwnableUnauthorizedAccount") + .withArgs(user.address); + }); + + it("convertTokens should revert when paused", async function () { + await tokenConverter.connect(owner).pause(); // Pause the contract + expect(await tokenConverter.paused()).to.be.true; + await expect(tokenConverter.connect(user).convertTokens(bigAtomicTestAmount)) + .to.be.revertedWithCustomError(tokenConverter, "EnforcedPause"); + }); + + it("convertTokens should work when unpaused", async function () { + await tokenConverter.connect(owner).pause(); // Pause + expect(await tokenConverter.paused()).to.be.true; + await tokenConverter.connect(owner).unpause(); // Unpause + expect(await tokenConverter.paused()).to.be.false; + + const initialTokenBBalance = await tokenBERC20.balanceOf(user.address); + await tokenConverter.connect(user).convertTokens(bigAtomicTestAmount); + const amountB = bigAtomicTestAmount * firstRate.numerator / firstRate.denominator; + expect(await tokenBERC20.balanceOf(user.address)).to.equal(initialTokenBBalance + amountB); + }); + + it("depositTokenB should still work when paused", async function () { + await tokenConverter.connect(owner).pause(); // Pause the contract + expect(await tokenConverter.paused()).to.be.true; + + const depositAmount = ethers.parseUnits("100", decimalsTokenB); + const initialContractTokenBBalance = await tokenBERC20.balanceOf(tokenConverter.getAddress()); + + await tokenBERC20.connect(owner).approve(tokenConverter.getAddress(), depositAmount); + await expect(tokenConverter.connect(owner).depositTokenB(depositAmount)).to.not.be.reverted; + + expect(await tokenBERC20.balanceOf(tokenConverter.getAddress())).to.equal(initialContractTokenBBalance + depositAmount); + }); + + it("withdrawTokenB should still work when paused", async function () { + // Deposit some tokens first to withdraw + const depositAmount = ethers.parseUnits("100", decimalsTokenB); + await tokenBERC20.connect(owner).approve(tokenConverter.getAddress(), depositAmount); + await tokenConverter.connect(owner).depositTokenB(depositAmount); + const initialOwnerTokenBBalance = await tokenBERC20.balanceOf(owner.address); + + await tokenConverter.connect(owner).pause(); // Pause the contract + expect(await tokenConverter.paused()).to.be.true; + + await expect(tokenConverter.connect(owner).withdrawTokenB(depositAmount)).to.not.be.reverted; + expect(await tokenBERC20.balanceOf(owner.address)).to.equal(initialOwnerTokenBBalance + depositAmount); + }); + }); });