diff --git a/contracts/mock/WBTCMock.sol b/contracts/mock/WBTCMock.sol index 984278e..3926e52 100644 --- a/contracts/mock/WBTCMock.sol +++ b/contracts/mock/WBTCMock.sol @@ -10,7 +10,7 @@ import { ERC20Upgradeable, IERC20 } from "@openzeppelin/contracts-upgradeable/to * @notice Use only for testing */ contract WBTCMock is ERC20Upgradeable { - + uint8 _decimals; /// @dev https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#initializing_the_implementation_contract /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -19,6 +19,11 @@ contract WBTCMock is ERC20Upgradeable { function initialize() external initializer { __ERC20_init("Wrapped BTC Mock", "WBTCMOCK"); + _decimals = 8; + } + + function setDecimals(uint8 decimals_) external { + _decimals = decimals_; } function mint(address to, uint256 amount) external { @@ -26,6 +31,6 @@ contract WBTCMock is ERC20Upgradeable { } function decimals() public override view virtual returns (uint8) { - return 8; + return _decimals; } } diff --git a/contracts/pmm/BTCB/BTCB.sol b/contracts/pmm/BTCB/BTCB.sol index dbb6ddc..f635d5a 100644 --- a/contracts/pmm/BTCB/BTCB.sol +++ b/contracts/pmm/BTCB/BTCB.sol @@ -3,20 +3,20 @@ pragma solidity 0.8.24; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; - -interface IMinteable { +interface ILBTC { function mint(address to, uint256 amount) external; + function decimals() external view returns (uint256); } contract BTCBPMM is PausableUpgradeable, AccessControlUpgradeable { - using SafeERC20 for IERC20; + using SafeERC20 for IERC20Metadata; struct PMMStorage { - IERC20 btcb; - IMinteable lbtc; + IERC20Metadata btcb; + ILBTC lbtc; uint256 stakeLimit; uint256 totalStake; @@ -31,7 +31,7 @@ contract BTCBPMM is PausableUpgradeable, AccessControlUpgradeable { error StakeLimitExceeded(); error UnauthorizedAccount(address account); - + error ZeroAmount(); event StakeLimitSet(uint256 newStakeLimit); event WithdrawalAddressSet(address newWithdrawAddress); @@ -48,8 +48,8 @@ contract BTCBPMM is PausableUpgradeable, AccessControlUpgradeable { $.stakeLimit = _stakeLimit; $.withdrawAddress = withdrawAddress; - $.lbtc = IMinteable(_lbtc); - $.btcb = IERC20(_btcb); + $.lbtc = ILBTC(_lbtc); + $.btcb = IERC20Metadata(_btcb); } function initialize(address _lbtc, address _btcb, address admin,uint256 _stakeLimit, address withdrawAddress) external initializer { @@ -60,11 +60,18 @@ contract BTCBPMM is PausableUpgradeable, AccessControlUpgradeable { function swapBTCBToLBTC(uint256 amount) external whenNotPaused { PMMStorage storage $ = _getPMMStorage(); - if ($.totalStake + amount > $.stakeLimit) revert StakeLimitExceeded(); - $.totalStake += amount; - $.btcb.safeTransferFrom(_msgSender(), address(this), amount); - $.lbtc.mint(_msgSender(), amount); + ILBTC lbtc = $.lbtc; + IERC20Metadata btcb = $.btcb; + + uint256 decimalsDifference = 10 ** (btcb.decimals() - lbtc.decimals()); + uint256 amountLBTC = (amount / decimalsDifference); + if(amountLBTC == 0) revert ZeroAmount(); + if ($.totalStake + amountLBTC > $.stakeLimit) revert StakeLimitExceeded(); + + $.totalStake += amountLBTC; + btcb.safeTransferFrom(_msgSender(), address(this), amountLBTC * decimalsDifference); + lbtc.mint(_msgSender(), amountLBTC); } function withdrawBTCB(uint256 amount) external whenNotPaused onlyRole(DEFAULT_ADMIN_ROLE) { diff --git a/test/BTCBPMM.ts b/test/BTCBPMM.ts index d866537..4e4d45a 100644 --- a/test/BTCBPMM.ts +++ b/test/BTCBPMM.ts @@ -37,10 +37,13 @@ describe("BTCBPMM", function () { await lbtc.getAddress(), await btcb.getAddress(), await deployer.getAddress(), - 30, + ethers.parseUnits("30", 8), await withdrawalAddress.getAddress() ]); + // use btcb decimals of 8 + await btcb.setDecimals(18); + snapshot = await takeSnapshot(); }); @@ -135,56 +138,62 @@ describe("BTCBPMM", function () { await pmm.grantRole(await pmm.TIMELOCK_ROLE(), await timeLock.getAddress()); // some btcb for signers - await btcb.mint(await signer1.getAddress(), 100); - await btcb.mint(await signer2.getAddress(), 100); + await btcb.mint(await signer1.getAddress(), ethers.parseUnits("100", 18)); + await btcb.mint(await signer2.getAddress(), ethers.parseUnits("100", 18)); }); - it("should fail to swap is PMM is not whitelisted as minter", async function () { - await btcb.connect(signer1).approve(await pmm.getAddress(), 1); - await expect(pmm.connect(signer1).swapBTCBToLBTC(1)) + it("should fail to swap if PMM is not whitelisted as minter", async function () { + await btcb.connect(signer1).approve(await pmm.getAddress(), ethers.parseUnits("10", 10)); + await expect(pmm.connect(signer1).swapBTCBToLBTC(ethers.parseUnits("10", 10))) .to.be.revertedWithCustomError(lbtc, "UnauthorizedAccount") .withArgs(await pmm.getAddress()); }); + it("should fail to swap if amount is to low and will result in 0 LBTC", async function () { + await btcb.connect(signer1).approve(await pmm.getAddress(), 1); + await expect(pmm.connect(signer1).swapBTCBToLBTC(1)) + .to.be.revertedWithCustomError(lbtc, "ZeroAmount"); + }); + describe("With whitelisted minter", function () { beforeEach(async function () { await lbtc.addMinter(await pmm.getAddress()); }); it("should allow swaps", async function () { - await btcb.connect(signer1).approve(await pmm.getAddress(), 1); - await expect(pmm.connect(signer1).swapBTCBToLBTC(1)) + await btcb.connect(signer1).approve(await pmm.getAddress(), ethers.parseUnits("11", 18) + 10n); + await expect(pmm.connect(signer1).swapBTCBToLBTC(ethers.parseUnits("11", 18) + 10n)) .to.emit(btcb, "Transfer") - .withArgs(signer1.address, await pmm.getAddress(), 1) + .withArgs(signer1.address, await pmm.getAddress(), ethers.parseUnits("11", 18)) .to.emit(lbtc, "Transfer") - .withArgs(ethers.ZeroAddress, signer1.address, 1); - expect(await pmm.remainingStake()).to.equal(29); - expect(await lbtc.balanceOf(signer1.address)).to.equal(1); - expect(await btcb.balanceOf(signer1.address)).to.equal(99); - expect(await btcb.balanceOf(await pmm.getAddress())).to.equal(1); + .withArgs(ethers.ZeroAddress, signer1.address, ethers.parseUnits("11", 8)); + expect(await pmm.remainingStake()).to.equal(ethers.parseUnits("19", 8)); + expect(await lbtc.balanceOf(signer1.address)).to.equal(ethers.parseUnits("11", 8)); + expect(await btcb.balanceOf(signer1.address)).to.equal(ethers.parseUnits("89", 18)); + expect(await btcb.balanceOf(await pmm.getAddress())).to.equal(ethers.parseUnits("11", 18)); }); it("should fail to swap more than limit", async function () { - await btcb.connect(signer1).approve(await pmm.getAddress(), 35); - await expect(pmm.connect(signer1).swapBTCBToLBTC(35)) + await btcb.connect(signer1).approve(await pmm.getAddress(), ethers.parseUnits("35", 18)); + await expect(pmm.connect(signer1).swapBTCBToLBTC(ethers.parseUnits("35", 18))) .to.be.revertedWithCustomError(pmm, "StakeLimitExceeded"); }); - it("should allow more swaos if limit is increased", async function () { - await btcb.connect(signer1).approve(await pmm.getAddress(), 30); - await pmm.connect(signer1).swapBTCBToLBTC(30); + it("should allow more swaps if limit is increased", async function () { + await btcb.connect(signer1).approve(await pmm.getAddress(), ethers.parseUnits("30", 18)); + await pmm.connect(signer1).swapBTCBToLBTC(ethers.parseUnits("30", 18)); expect(await pmm.remainingStake()).to.equal(0); - await pmm.connect(timeLock).setStakeLimit(40); - expect(await pmm.remainingStake()).to.equal(10); - await btcb.connect(signer2).approve(await pmm.getAddress(), 10); - await pmm.connect(signer2).swapBTCBToLBTC(10); + await pmm.connect(timeLock).setStakeLimit(ethers.parseUnits("40", 8)); + expect(await pmm.remainingStake()).to.equal(ethers.parseUnits("10", 8)); + await btcb.connect(signer2).approve(await pmm.getAddress(), ethers.parseUnits("10", 18)); + await pmm.connect(signer2).swapBTCBToLBTC(ethers.parseUnits("10", 18)); expect(await pmm.remainingStake()).to.equal(0); }); it("should allow withdrawals", async function () { - await btcb.connect(signer1).approve(await pmm.getAddress(), 30); - await pmm.connect(signer1).swapBTCBToLBTC(30); + await btcb.connect(signer1).approve(await pmm.getAddress(), ethers.parseUnits("30", 18)); + await pmm.connect(signer1).swapBTCBToLBTC(ethers.parseUnits("30", 18)); await expect(pmm.withdrawBTCB(1)) .to.emit(btcb, "Transfer") @@ -193,10 +202,10 @@ describe("BTCBPMM", function () { }); it("should have zero remaining stake if total stake is greater than limit", async function () { - await btcb.connect(signer1).approve(await pmm.getAddress(), 30); - await pmm.connect(signer1).swapBTCBToLBTC(30); + await btcb.connect(signer1).approve(await pmm.getAddress(), ethers.parseUnits("30", 18)); + await pmm.connect(signer1).swapBTCBToLBTC(ethers.parseUnits("30", 18)); - await pmm.connect(timeLock).setStakeLimit(20); + await pmm.connect(timeLock).setStakeLimit(ethers.parseUnits("20", 8)); expect(await pmm.remainingStake()).to.equal(0); });