From 8fff7b5d89cd3606be663dc6a9c1089e1bb503e9 Mon Sep 17 00:00:00 2001 From: doncesarts Date: Thu, 9 Mar 2023 10:15:03 +0000 Subject: [PATCH 1/6] feat: adds mta redeemer --- contracts/shared/MetaTokenRedeemer.sol | 72 ++++++++++++++++++++++ tasks/deployShared.ts | 30 +++++++++ test/shared/meta-token-redeemer.spec.ts | 81 +++++++++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 contracts/shared/MetaTokenRedeemer.sol create mode 100644 tasks/deployShared.ts create mode 100644 test/shared/meta-token-redeemer.spec.ts diff --git a/contracts/shared/MetaTokenRedeemer.sol b/contracts/shared/MetaTokenRedeemer.sol new file mode 100644 index 00000000..d73d04c0 --- /dev/null +++ b/contracts/shared/MetaTokenRedeemer.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.6; + +// External +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @notice Allows to redeem MTA for WETH at a fixed rate. + * @author mStable + * @dev VERSION: 1.0 + * DATE: 2023-03-08 + */ +contract MetaTokenRedeemer { + using SafeERC20 for IERC20; + + uint256 public constant RATE_SCALE = 1e18; + address public immutable MTA; + address public immutable WETH; + uint256 public immutable RATE; + + /** + * @notice Emits event whenever a user funds and amount. + */ + event Funded(address indexed from, uint256 amount); + + /** + * @notice Emits event whenever a user redeems an amount. + */ + event Redeemed(address indexed sender, uint256 fromAssetAmount, uint256 toAssetAmount); + + /** + * @notice Crates a new instance of the contract + * @param _mta MTA Token Address + * @param _weth WETH Token Address + * @param _rate The exchange rate with 18 decimal numbers, for example 1 MTA = 0.00002 ETH rate is 20000000000000; + */ + constructor( + address _mta, + address _weth, + uint256 _rate + ) { + MTA = _mta; + WETH = _weth; + RATE = _rate; + } + + /// @notice Funds the contract with WETH. + /// @param amount Amount of WETH to be transfer to the contract + function fund(uint256 amount) external { + IERC20(WETH).safeTransferFrom(msg.sender, address(this), amount); + emit Funded(msg.sender, amount); + } + + /// @notice Redeems MTA for WETH at a fixed rate. + /// @param fromAssetAmount a parameter just like in doxygen (must be followed by parameter name) + /// @return toAssetAmount The amount of WETH received. + function redeem(uint256 fromAssetAmount) external returns (uint256 toAssetAmount) { + IERC20(MTA).safeTransferFrom(msg.sender, address(this), fromAssetAmount); + + // calculate to asset amount + toAssetAmount = (fromAssetAmount * RATE) / RATE_SCALE; + + // transfer out the to asset + require(IERC20(WETH).balanceOf(address(this)) >= toAssetAmount, "not enough WETH"); + + IERC20(WETH).safeTransfer(msg.sender, toAssetAmount); + + emit Redeemed(msg.sender, fromAssetAmount, toAssetAmount); + } +} diff --git a/tasks/deployShared.ts b/tasks/deployShared.ts new file mode 100644 index 00000000..9767c5e8 --- /dev/null +++ b/tasks/deployShared.ts @@ -0,0 +1,30 @@ +import "ts-node/register" +import "tsconfig-paths/register" +import { task, types } from "hardhat/config" +import { MetaTokenRedeemer__factory } from "types/generated" +import { BigNumber } from "ethers" +import { deployContract } from "./utils/deploy-utils" +import { getSigner } from "./utils/signerFactory" +import { verifyEtherscan } from "./utils/etherscan" +import { MTA } from "./utils" + +task("deploy-MetaTokenRedeemer") + .addParam("rate", "Redemption rate with 18 decimal points", types.string) + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .setAction(async (taskArgs, hre) => { + const signer = await getSigner(hre, taskArgs.speed) + const mtaAddr = MTA.address + const wethAddr = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + + const metaTokenRedeemer = await deployContract(new MetaTokenRedeemer__factory(signer), "MetaTokenRedeemer", [ + mtaAddr, + wethAddr, + BigNumber.from(taskArgs.rate), + ]) + + await verifyEtherscan(hre, { + address: metaTokenRedeemer.address, + contract: "contracts/shared/MetaTokenRedeemer.sol:MetaTokenRedeemer", + }) + }) +module.exports = {} diff --git a/test/shared/meta-token-redeemer.spec.ts b/test/shared/meta-token-redeemer.spec.ts new file mode 100644 index 00000000..7af2586d --- /dev/null +++ b/test/shared/meta-token-redeemer.spec.ts @@ -0,0 +1,81 @@ +import { BN, simpleToExactAmount } from "@utils/math" +import { ethers } from "hardhat" +import { ERC20, MetaTokenRedeemer, MetaTokenRedeemer__factory, MockERC20__factory, MockRoot__factory } from "types/generated" +import { expect } from "chai" +import { Signer } from "ethers" +import { ZERO } from "@utils/constants" + +describe("MetaTokenRedeemer", () => { + let redeemer: MetaTokenRedeemer + let deployer: Signer + let alice: Signer + let aliceAddress: string + let mta: ERC20 + let weth: ERC20 + const rate = BN.from("20000000000000") // 1 MTA = 0.00002 ETH (Rate to simplify tests) + const wethAmount = simpleToExactAmount(20) + + before(async () => { + const accounts = await ethers.getSigners() + deployer = accounts[0] + alice = accounts[1] + aliceAddress = await alice.getAddress() + mta = await new MockERC20__factory(deployer).deploy( + "Meta Token", + "mta", + 18, + await deployer.getAddress(), + simpleToExactAmount(10_000_000), + ) + weth = await new MockERC20__factory(deployer).deploy( + "WETH Token", + "weth", + 18, + await deployer.getAddress(), + simpleToExactAmount(1_000_000), + ) + redeemer = await new MetaTokenRedeemer__factory(deployer).deploy(mta.address, weth.address, rate) + // send mta to alice + mta.transfer(aliceAddress, simpleToExactAmount(10_000)) + }) + it("deposits WETH into redeemer", async () => { + await weth.approve(redeemer.address, wethAmount) + const tx = await redeemer.fund(wethAmount) + expect(tx) + .to.emit(redeemer, "Funded") + .withArgs(await deployer.getAddress(), wethAmount) + }) + it("anyone can redeem MTA multiple times", async () => { + const aliceBalanceBefore = await mta.balanceOf(aliceAddress) + const aliceWethBalanceBefore = await weth.balanceOf(aliceAddress) + const redeemerWethBalanceBefore = await weth.balanceOf(redeemer.address) + + const amount = aliceBalanceBefore.div(2) + const wethAmount = amount.mul(rate).div(simpleToExactAmount(1)) + + expect(aliceBalanceBefore, "balance").to.be.gt(ZERO) + await mta.connect(alice).approve(redeemer.address, ethers.constants.MaxUint256) + + const tx1 = await redeemer.connect(alice).redeem(amount) + expect(tx1).to.emit(redeemer, "Redeemed").withArgs(aliceAddress, amount, wethAmount) + + const tx2 = await redeemer.connect(alice).redeem(amount) + expect(tx2).to.emit(redeemer, "Redeemed").withArgs(aliceAddress, amount, wethAmount) + + const aliceBalanceAfter = await mta.balanceOf(aliceAddress) + const aliceWethBalanceAfter = await weth.balanceOf(aliceAddress) + const redeemerWethBalanceAfter = await weth.balanceOf(redeemer.address) + + expect(aliceBalanceAfter, "alice mta balance").to.be.eq(ZERO) + expect(aliceWethBalanceAfter, "alice weth balance").to.be.eq(aliceWethBalanceBefore.add(wethAmount.mul(2))) + expect(redeemerWethBalanceAfter, "redeemer weth balance").to.be.eq(redeemerWethBalanceBefore.sub(wethAmount.mul(2))) + }) + it("fails if there is not enough WETH (non realistic example) ", async () => { + const mtaAmount = await mta.balanceOf(await deployer.getAddress()) + const wethAmount = mtaAmount.mul(rate).div(simpleToExactAmount(1)) + expect(wethAmount).to.be.gt(simpleToExactAmount(20)) + await mta.approve(redeemer.address, mtaAmount) + + await expect(redeemer.redeem(mtaAmount)).to.be.revertedWith("not enough WETH") + }) +}) From b45b544a47c7ab0a9599187f8dbe12dc1841c726 Mon Sep 17 00:00:00 2001 From: doncesarts Date: Thu, 9 Mar 2023 10:59:33 +0000 Subject: [PATCH 2/6] feat: adds mta redeemer --- test/shared/meta-token-redeemer.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/shared/meta-token-redeemer.spec.ts b/test/shared/meta-token-redeemer.spec.ts index 7af2586d..fe991378 100644 --- a/test/shared/meta-token-redeemer.spec.ts +++ b/test/shared/meta-token-redeemer.spec.ts @@ -1,6 +1,6 @@ import { BN, simpleToExactAmount } from "@utils/math" import { ethers } from "hardhat" -import { ERC20, MetaTokenRedeemer, MetaTokenRedeemer__factory, MockERC20__factory, MockRoot__factory } from "types/generated" +import { ERC20, MetaTokenRedeemer, MetaTokenRedeemer__factory, MockERC20__factory } from "types/generated" import { expect } from "chai" import { Signer } from "ethers" import { ZERO } from "@utils/constants" @@ -51,29 +51,29 @@ describe("MetaTokenRedeemer", () => { const redeemerWethBalanceBefore = await weth.balanceOf(redeemer.address) const amount = aliceBalanceBefore.div(2) - const wethAmount = amount.mul(rate).div(simpleToExactAmount(1)) + const expectedWeth = amount.mul(rate).div(simpleToExactAmount(1)) expect(aliceBalanceBefore, "balance").to.be.gt(ZERO) await mta.connect(alice).approve(redeemer.address, ethers.constants.MaxUint256) const tx1 = await redeemer.connect(alice).redeem(amount) - expect(tx1).to.emit(redeemer, "Redeemed").withArgs(aliceAddress, amount, wethAmount) + expect(tx1).to.emit(redeemer, "Redeemed").withArgs(aliceAddress, amount, expectedWeth) const tx2 = await redeemer.connect(alice).redeem(amount) - expect(tx2).to.emit(redeemer, "Redeemed").withArgs(aliceAddress, amount, wethAmount) + expect(tx2).to.emit(redeemer, "Redeemed").withArgs(aliceAddress, amount, expectedWeth) const aliceBalanceAfter = await mta.balanceOf(aliceAddress) const aliceWethBalanceAfter = await weth.balanceOf(aliceAddress) const redeemerWethBalanceAfter = await weth.balanceOf(redeemer.address) expect(aliceBalanceAfter, "alice mta balance").to.be.eq(ZERO) - expect(aliceWethBalanceAfter, "alice weth balance").to.be.eq(aliceWethBalanceBefore.add(wethAmount.mul(2))) - expect(redeemerWethBalanceAfter, "redeemer weth balance").to.be.eq(redeemerWethBalanceBefore.sub(wethAmount.mul(2))) + expect(aliceWethBalanceAfter, "alice weth balance").to.be.eq(aliceWethBalanceBefore.add(expectedWeth.mul(2))) + expect(redeemerWethBalanceAfter, "redeemer weth balance").to.be.eq(redeemerWethBalanceBefore.sub(expectedWeth.mul(2))) }) it("fails if there is not enough WETH (non realistic example) ", async () => { const mtaAmount = await mta.balanceOf(await deployer.getAddress()) - const wethAmount = mtaAmount.mul(rate).div(simpleToExactAmount(1)) - expect(wethAmount).to.be.gt(simpleToExactAmount(20)) + const expectedWeth = mtaAmount.mul(rate).div(simpleToExactAmount(1)) + expect(expectedWeth).to.be.gt(simpleToExactAmount(20)) await mta.approve(redeemer.address, mtaAmount) await expect(redeemer.redeem(mtaAmount)).to.be.revertedWith("not enough WETH") From b19e0fcfb92d7b0ecd138f7117996449705cc90c Mon Sep 17 00:00:00 2001 From: doncesarts Date: Fri, 17 Mar 2023 01:23:31 +0000 Subject: [PATCH 3/6] feat: adds periods to mta token redeemer --- contracts/shared/MetaTokenRedeemer.sol | 79 +++++++++---- test/shared/meta-token-redeemer.spec.ts | 148 +++++++++++++++++++----- 2 files changed, 176 insertions(+), 51 deletions(-) diff --git a/contracts/shared/MetaTokenRedeemer.sol b/contracts/shared/MetaTokenRedeemer.sol index d73d04c0..e90aa03e 100644 --- a/contracts/shared/MetaTokenRedeemer.sol +++ b/contracts/shared/MetaTokenRedeemer.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.6; -// External -import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -15,58 +13,91 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s contract MetaTokenRedeemer { using SafeERC20 for IERC20; - uint256 public constant RATE_SCALE = 1e18; address public immutable MTA; address public immutable WETH; - uint256 public immutable RATE; + uint256 public immutable PERIOD_DURATION; + uint256 public periodStart; + uint256 public periodEnd; + uint256 public totalFunded; + uint256 public totalRegistered; + mapping(address => uint256) public balances; /** - * @notice Emits event whenever a user funds and amount. + * @notice Emitted when the redeemer is funded. */ - event Funded(address indexed from, uint256 amount); + event Funded(address indexed sender, uint256 amount); + /** + * @notice Emitted when a user register MTA. + */ + event Register(address indexed sender, uint256 amount); /** - * @notice Emits event whenever a user redeems an amount. + * @notice Emitted when a user claims WETH for the registered amount. */ - event Redeemed(address indexed sender, uint256 fromAssetAmount, uint256 toAssetAmount); + event Redeemed(address indexed sender, uint256 registeredAmount, uint256 redeemedAmount); /** * @notice Crates a new instance of the contract * @param _mta MTA Token Address * @param _weth WETH Token Address - * @param _rate The exchange rate with 18 decimal numbers, for example 1 MTA = 0.00002 ETH rate is 20000000000000; + * @param _periodDuration The lenght of the registration period. */ constructor( address _mta, address _weth, - uint256 _rate + uint256 _periodDuration ) { MTA = _mta; WETH = _weth; - RATE = _rate; + PERIOD_DURATION = _periodDuration; } - /// @notice Funds the contract with WETH. - /// @param amount Amount of WETH to be transfer to the contract + /** + * @notice Funds the contract with WETH, and initialize the funding period. + * It only allows to fund during the funding period. + * @param amount The Amount of WETH to be transfer to the contract + */ function fund(uint256 amount) external { + require(periodStart == 0 || block.timestamp <= periodEnd, "Funding period ended"); + IERC20(WETH).safeTransferFrom(msg.sender, address(this), amount); + if (periodStart == 0) { + periodStart = block.timestamp; + periodEnd = periodStart + PERIOD_DURATION; + } + totalFunded += amount; + emit Funded(msg.sender, amount); } - /// @notice Redeems MTA for WETH at a fixed rate. - /// @param fromAssetAmount a parameter just like in doxygen (must be followed by parameter name) - /// @return toAssetAmount The amount of WETH received. - function redeem(uint256 fromAssetAmount) external returns (uint256 toAssetAmount) { - IERC20(MTA).safeTransferFrom(msg.sender, address(this), fromAssetAmount); + /** + * @notice Allos user to register and transfer a given amount of MTA + * It only allows to register during the registration period. + * @param amount The Amount of MTA to register. + */ + function register(uint256 amount) external { + require(periodStart > 0, "Registration period not started"); + require(block.timestamp <= periodEnd, "Registration period ended"); + + IERC20(MTA).safeTransferFrom(msg.sender, address(this), amount); + balances[msg.sender] += amount; + totalRegistered += amount; + emit Register(msg.sender, amount); + } - // calculate to asset amount - toAssetAmount = (fromAssetAmount * RATE) / RATE_SCALE; + /// @notice Redeems all user MTA balance for WETH at a fixed rate. + /// @return redeemedAmount The amount of WETH to receive. + function redeem() external returns (uint256 redeemedAmount) { + require(periodEnd <= block.timestamp, "Redeem period not started"); + uint256 registeredAmount = balances[msg.sender]; + require(registeredAmount > 0, "No balance"); - // transfer out the to asset - require(IERC20(WETH).balanceOf(address(this)) >= toAssetAmount, "not enough WETH"); + // MTA and WETH both have 18 decimal points, no need for scaling. + redeemedAmount = (registeredAmount * totalRegistered) / totalFunded; + balances[msg.sender] = 0; - IERC20(WETH).safeTransfer(msg.sender, toAssetAmount); + IERC20(WETH).safeTransfer(msg.sender, redeemedAmount); - emit Redeemed(msg.sender, fromAssetAmount, toAssetAmount); + emit Redeemed(msg.sender, registeredAmount, redeemedAmount); } } diff --git a/test/shared/meta-token-redeemer.spec.ts b/test/shared/meta-token-redeemer.spec.ts index fe991378..1982c080 100644 --- a/test/shared/meta-token-redeemer.spec.ts +++ b/test/shared/meta-token-redeemer.spec.ts @@ -1,24 +1,25 @@ -import { BN, simpleToExactAmount } from "@utils/math" +import { simpleToExactAmount } from "@utils/math" import { ethers } from "hardhat" import { ERC20, MetaTokenRedeemer, MetaTokenRedeemer__factory, MockERC20__factory } from "types/generated" import { expect } from "chai" import { Signer } from "ethers" -import { ZERO } from "@utils/constants" +import { ONE_DAY, ZERO } from "@utils/constants" +import { getTimestamp, increaseTime } from "@utils/time" describe("MetaTokenRedeemer", () => { let redeemer: MetaTokenRedeemer let deployer: Signer let alice: Signer + let bob: Signer let aliceAddress: string let mta: ERC20 let weth: ERC20 - const rate = BN.from("20000000000000") // 1 MTA = 0.00002 ETH (Rate to simplify tests) - const wethAmount = simpleToExactAmount(20) before(async () => { const accounts = await ethers.getSigners() deployer = accounts[0] alice = accounts[1] + bob = accounts[2] aliceAddress = await alice.getAddress() mta = await new MockERC20__factory(deployer).deploy( "Meta Token", @@ -34,48 +35,141 @@ describe("MetaTokenRedeemer", () => { await deployer.getAddress(), simpleToExactAmount(1_000_000), ) - redeemer = await new MetaTokenRedeemer__factory(deployer).deploy(mta.address, weth.address, rate) + redeemer = await new MetaTokenRedeemer__factory(deployer).deploy(mta.address, weth.address, ONE_DAY.mul(90)) // send mta to alice mta.transfer(aliceAddress, simpleToExactAmount(10_000)) + mta.transfer(await bob.getAddress(), simpleToExactAmount(10_000)) }) - it("deposits WETH into redeemer", async () => { + it("constructor parameters are correct", async () => { + expect(await redeemer.MTA(), "MTA").to.be.eq(mta.address) + expect(await redeemer.WETH(), "WETH").to.be.eq(weth.address) + expect(await redeemer.PERIOD_DURATION(), "PERIOD_DURATION").to.be.eq(ONE_DAY.mul(90)) + expect(await redeemer.periodStart(), "periodStart").to.be.eq(ZERO) + expect(await redeemer.periodEnd(), "periodEnd").to.be.eq(ZERO) + expect(await redeemer.totalFunded(), "totalFunded").to.be.eq(ZERO) + expect(await redeemer.totalRegistered(), "totalRegistered").to.be.eq(ZERO) + expect(await redeemer.balances(aliceAddress), "balances").to.be.eq(ZERO) + }) + it("fails to register if period has not started", async () => { + expect(await redeemer.periodStart(), "periodStart").to.be.eq(ZERO) + + await expect(redeemer.register(ZERO), "register").to.be.revertedWith("Registration period not started") + }) + it("funds WETH into redeemer", async () => { + const wethAmount = await weth.balanceOf(await deployer.getAddress()) + console.log("🚀 ~ file: meta-token-redeemer.spec.ts:60 ~ it ~ wethAmount:", wethAmount.toString()) + const redeemerWethBalance = await weth.balanceOf(redeemer.address) + await weth.approve(redeemer.address, wethAmount) + const now = await getTimestamp() + console.log("🚀 ~ file: meta-token-redeemer.spec.ts:64 ~ it ~ now:", now) + const tx = await redeemer.fund(wethAmount.div(2)) + expect(tx) + .to.emit(redeemer, "Funded") + .withArgs(await deployer.getAddress(), wethAmount.div(2)) + // Check total funded increases + expect(await redeemer.totalFunded(), "total funded").to.be.eq(wethAmount.div(2)) + expect(await weth.balanceOf(redeemer.address), "weth balance").to.be.eq(redeemerWethBalance.add(wethAmount.div(2))) + // Fist time it is invoked , period details are set + expect(await redeemer.periodStart(), "period start").to.be.eq(now.add(1)) + expect(await redeemer.periodEnd(), "period end").to.be.eq(now.add(1).add(await redeemer.PERIOD_DURATION())) + }) + it("funds again WETH into redeemer", async () => { + const wethAmount = await weth.balanceOf(await deployer.getAddress()) + + const periodStart = await redeemer.periodStart() + const periodEnd = await redeemer.periodEnd() + const totalFunded = await redeemer.totalFunded() + const redeemerWethBalance = await weth.balanceOf(redeemer.address) + await weth.approve(redeemer.address, wethAmount) const tx = await redeemer.fund(wethAmount) expect(tx) .to.emit(redeemer, "Funded") .withArgs(await deployer.getAddress(), wethAmount) + // Check total funded increases + expect(await redeemer.totalFunded(), "total funded").to.be.eq(totalFunded.add(wethAmount)) + expect(await weth.balanceOf(redeemer.address), "weth balance").to.be.eq(redeemerWethBalance.add(wethAmount)) + // After first time, period details do not change + expect(await redeemer.periodStart(), "period start").to.be.eq(periodStart) + expect(await redeemer.periodEnd(), "period end").to.be.eq(periodEnd) + }) + const registerTests = [{ user: "alice" }, { user: "bob" }] + registerTests.forEach((test, i) => + it(`${test.user} can register MTA multiple times`, async () => { + const accounts = await ethers.getSigners() + const signer = accounts[i + 1] + const signerAddress = await signer.getAddress() + const signerBalanceBefore = await mta.balanceOf(signerAddress) + const redeemerMTABalance = await mta.balanceOf(redeemer.address) + + const amount = signerBalanceBefore.div(2) + expect(signerBalanceBefore, "balance").to.be.gt(ZERO) + await mta.connect(signer).approve(redeemer.address, ethers.constants.MaxUint256) + + const tx1 = await redeemer.connect(signer).register(amount) + expect(tx1).to.emit(redeemer, "Register").withArgs(signerAddress, amount) + + const tx2 = await redeemer.connect(signer).register(amount) + expect(tx2).to.emit(redeemer, "Register").withArgs(signerAddress, amount) + + const signerBalanceAfter = await mta.balanceOf(signerAddress) + const redeemerMTABalanceAfter = await mta.balanceOf(redeemer.address) + + expect(signerBalanceAfter, "user mta balance").to.be.eq(ZERO) + expect(redeemerMTABalanceAfter, "redeemer mta balance").to.be.eq(redeemerMTABalance.add(signerBalanceBefore)) + }), + ) + it("fails to redeem if Redeem period not started", async () => { + const now = await getTimestamp() + const periodEnd = await redeemer.periodEnd() + + expect(now, "now < periodEnd").to.be.lt(periodEnd) + + await expect(redeemer.redeem(), "redeem").to.be.revertedWith("Redeem period not started") }) - it("anyone can redeem MTA multiple times", async () => { - const aliceBalanceBefore = await mta.balanceOf(aliceAddress) + it("fails to fund or register if register period ended", async () => { + await increaseTime(ONE_DAY.mul(91)) + const periodEnd = await redeemer.periodEnd() + const now = await getTimestamp() + + expect(now, "now > periodEnd").to.be.gt(periodEnd) + + await expect(redeemer.fund(ZERO), "fund").to.be.revertedWith("Funding period ended") + await expect(redeemer.register(ZERO), "register").to.be.revertedWith("Registration period ended") + }) + + it("anyone can redeem WETH", async () => { const aliceWethBalanceBefore = await weth.balanceOf(aliceAddress) const redeemerWethBalanceBefore = await weth.balanceOf(redeemer.address) + const redeemerMTABalanceBefore = await mta.balanceOf(redeemer.address) + const registeredAmount = await redeemer.balances(aliceAddress) - const amount = aliceBalanceBefore.div(2) - const expectedWeth = amount.mul(rate).div(simpleToExactAmount(1)) + const totalRegistered = await redeemer.totalRegistered() + const totalFunded = await redeemer.totalFunded() - expect(aliceBalanceBefore, "balance").to.be.gt(ZERO) - await mta.connect(alice).approve(redeemer.address, ethers.constants.MaxUint256) + const expectedWeth = registeredAmount.mul(totalRegistered).div(totalFunded) - const tx1 = await redeemer.connect(alice).redeem(amount) - expect(tx1).to.emit(redeemer, "Redeemed").withArgs(aliceAddress, amount, expectedWeth) + expect(registeredAmount, "registeredAmount").to.be.gt(ZERO) - const tx2 = await redeemer.connect(alice).redeem(amount) - expect(tx2).to.emit(redeemer, "Redeemed").withArgs(aliceAddress, amount, expectedWeth) + const tx = await redeemer.connect(alice).redeem() + expect(tx).to.emit(redeemer, "Redeemed").withArgs(aliceAddress, registeredAmount, expectedWeth) - const aliceBalanceAfter = await mta.balanceOf(aliceAddress) + const redeemerMTABalanceAfter = await mta.balanceOf(redeemer.address) const aliceWethBalanceAfter = await weth.balanceOf(aliceAddress) const redeemerWethBalanceAfter = await weth.balanceOf(redeemer.address) + const registeredAmountAfter = await redeemer.balances(aliceAddress) - expect(aliceBalanceAfter, "alice mta balance").to.be.eq(ZERO) - expect(aliceWethBalanceAfter, "alice weth balance").to.be.eq(aliceWethBalanceBefore.add(expectedWeth.mul(2))) - expect(redeemerWethBalanceAfter, "redeemer weth balance").to.be.eq(redeemerWethBalanceBefore.sub(expectedWeth.mul(2))) + expect(registeredAmountAfter, "alice register balance").to.be.eq(ZERO) + expect(aliceWethBalanceAfter, "alice weth balance").to.be.eq(aliceWethBalanceBefore.add(expectedWeth)) + expect(redeemerWethBalanceAfter, "redeemer weth balance").to.be.eq(redeemerWethBalanceBefore.sub(expectedWeth)) + // invariants + expect(redeemerMTABalanceAfter, "no mta is transferred").to.be.eq(redeemerMTABalanceBefore) + expect(totalRegistered, "register amount").to.be.eq(await redeemer.totalRegistered()) + expect(totalFunded, "funded amount ").to.be.eq(await redeemer.totalFunded()) }) - it("fails if there is not enough WETH (non realistic example) ", async () => { - const mtaAmount = await mta.balanceOf(await deployer.getAddress()) - const expectedWeth = mtaAmount.mul(rate).div(simpleToExactAmount(1)) - expect(expectedWeth).to.be.gt(simpleToExactAmount(20)) - await mta.approve(redeemer.address, mtaAmount) - - await expect(redeemer.redeem(mtaAmount)).to.be.revertedWith("not enough WETH") + it("fails if sender did not register", async () => { + const registeredAmount = await redeemer.balances(await deployer.getAddress()) + expect(registeredAmount).to.be.eq(ZERO) + await expect(redeemer.connect(deployer).redeem()).to.be.revertedWith("No balance") }) }) From 12eebdc8ee55356e0eecc5e92968fecd9b7fc46d Mon Sep 17 00:00:00 2001 From: doncesarts Date: Fri, 17 Mar 2023 01:36:42 +0000 Subject: [PATCH 4/6] fix: lint issues on mta redeemer --- tasks/deployShared.ts | 4 +++- test/shared/meta-token-redeemer.spec.ts | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tasks/deployShared.ts b/tasks/deployShared.ts index 9767c5e8..91b7a92b 100644 --- a/tasks/deployShared.ts +++ b/tasks/deployShared.ts @@ -7,14 +7,16 @@ import { deployContract } from "./utils/deploy-utils" import { getSigner } from "./utils/signerFactory" import { verifyEtherscan } from "./utils/etherscan" import { MTA } from "./utils" +import { getChain, getChainAddress } from "./utils/networkAddressFactory" task("deploy-MetaTokenRedeemer") .addParam("rate", "Redemption rate with 18 decimal points", types.string) .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .setAction(async (taskArgs, hre) => { const signer = await getSigner(hre, taskArgs.speed) + const chain = getChain(hre) const mtaAddr = MTA.address - const wethAddr = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + const wethAddr = getChainAddress("UniswapEthToken", chain) const metaTokenRedeemer = await deployContract(new MetaTokenRedeemer__factory(signer), "MetaTokenRedeemer", [ mtaAddr, diff --git a/test/shared/meta-token-redeemer.spec.ts b/test/shared/meta-token-redeemer.spec.ts index 1982c080..0836f69f 100644 --- a/test/shared/meta-token-redeemer.spec.ts +++ b/test/shared/meta-token-redeemer.spec.ts @@ -57,11 +57,9 @@ describe("MetaTokenRedeemer", () => { }) it("funds WETH into redeemer", async () => { const wethAmount = await weth.balanceOf(await deployer.getAddress()) - console.log("🚀 ~ file: meta-token-redeemer.spec.ts:60 ~ it ~ wethAmount:", wethAmount.toString()) const redeemerWethBalance = await weth.balanceOf(redeemer.address) await weth.approve(redeemer.address, wethAmount) const now = await getTimestamp() - console.log("🚀 ~ file: meta-token-redeemer.spec.ts:64 ~ it ~ now:", now) const tx = await redeemer.fund(wethAmount.div(2)) expect(tx) .to.emit(redeemer, "Funded") From 827d46580d6032679f3a14de921414606f454e52 Mon Sep 17 00:00:00 2001 From: doncesarts Date: Fri, 17 Mar 2023 01:41:38 +0000 Subject: [PATCH 5/6] fix: lint issues on mta redeemer --- tasks/deployShared.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tasks/deployShared.ts b/tasks/deployShared.ts index 91b7a92b..25175f66 100644 --- a/tasks/deployShared.ts +++ b/tasks/deployShared.ts @@ -10,7 +10,7 @@ import { MTA } from "./utils" import { getChain, getChainAddress } from "./utils/networkAddressFactory" task("deploy-MetaTokenRedeemer") - .addParam("rate", "Redemption rate with 18 decimal points", types.string) + .addParam("duration", "Registration period duration, default value 90 days (7776000)", 7776000, types.int) .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .setAction(async (taskArgs, hre) => { const signer = await getSigner(hre, taskArgs.speed) @@ -21,7 +21,7 @@ task("deploy-MetaTokenRedeemer") const metaTokenRedeemer = await deployContract(new MetaTokenRedeemer__factory(signer), "MetaTokenRedeemer", [ mtaAddr, wethAddr, - BigNumber.from(taskArgs.rate), + BigNumber.from(taskArgs.duration), ]) await verifyEtherscan(hre, { From aa3f2c420b6ae1dcae6a151f9c5cc9deb4901ec0 Mon Sep 17 00:00:00 2001 From: doncesarts Date: Mon, 20 Mar 2023 17:10:58 +0100 Subject: [PATCH 6/6] fix: mta redeemer improves gass --- contracts/shared/MetaTokenRedeemer.sol | 39 +++++++++++------- test/shared/meta-token-redeemer.spec.ts | 53 ++++++++++--------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/contracts/shared/MetaTokenRedeemer.sol b/contracts/shared/MetaTokenRedeemer.sol index e90aa03e..7e66dba1 100644 --- a/contracts/shared/MetaTokenRedeemer.sol +++ b/contracts/shared/MetaTokenRedeemer.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.6; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeCastExtended } from "../shared/SafeCastExtended.sol"; /** * @notice Allows to redeem MTA for WETH at a fixed rate. @@ -12,14 +13,17 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s */ contract MetaTokenRedeemer { using SafeERC20 for IERC20; + struct RegisterPeriod { + uint32 start; + uint32 end; + } address public immutable MTA; address public immutable WETH; uint256 public immutable PERIOD_DURATION; - uint256 public periodStart; - uint256 public periodEnd; - uint256 public totalFunded; - uint256 public totalRegistered; + RegisterPeriod public registerPeriod; + uint128 public totalFunded; + uint128 public totalRegistered; mapping(address => uint256) public balances; /** @@ -58,42 +62,47 @@ contract MetaTokenRedeemer { * @param amount The Amount of WETH to be transfer to the contract */ function fund(uint256 amount) external { - require(periodStart == 0 || block.timestamp <= periodEnd, "Funding period ended"); + require( + registerPeriod.start == 0 || block.timestamp <= registerPeriod.end, + "Funding period ended" + ); IERC20(WETH).safeTransferFrom(msg.sender, address(this), amount); - if (periodStart == 0) { - periodStart = block.timestamp; - periodEnd = periodStart + PERIOD_DURATION; + if (registerPeriod.start == 0) { + registerPeriod = RegisterPeriod( + SafeCastExtended.toUint32(block.timestamp), + SafeCastExtended.toUint32(block.timestamp + PERIOD_DURATION) + ); } - totalFunded += amount; + totalFunded += SafeCastExtended.toUint128(amount); emit Funded(msg.sender, amount); } /** - * @notice Allos user to register and transfer a given amount of MTA + * @notice Allows user to register and transfer a given amount of MTA * It only allows to register during the registration period. * @param amount The Amount of MTA to register. */ function register(uint256 amount) external { - require(periodStart > 0, "Registration period not started"); - require(block.timestamp <= periodEnd, "Registration period ended"); + require(registerPeriod.start > 0, "Registration period not started"); + require(block.timestamp <= registerPeriod.end, "Registration period ended"); IERC20(MTA).safeTransferFrom(msg.sender, address(this), amount); balances[msg.sender] += amount; - totalRegistered += amount; + totalRegistered += SafeCastExtended.toUint128(amount); emit Register(msg.sender, amount); } /// @notice Redeems all user MTA balance for WETH at a fixed rate. /// @return redeemedAmount The amount of WETH to receive. function redeem() external returns (uint256 redeemedAmount) { - require(periodEnd <= block.timestamp, "Redeem period not started"); + require(block.timestamp > registerPeriod.end, "Redeem period not started"); uint256 registeredAmount = balances[msg.sender]; require(registeredAmount > 0, "No balance"); // MTA and WETH both have 18 decimal points, no need for scaling. - redeemedAmount = (registeredAmount * totalRegistered) / totalFunded; + redeemedAmount = (registeredAmount * totalFunded) / totalRegistered; balances[msg.sender] = 0; IERC20(WETH).safeTransfer(msg.sender, redeemedAmount); diff --git a/test/shared/meta-token-redeemer.spec.ts b/test/shared/meta-token-redeemer.spec.ts index 0836f69f..2f8e053f 100644 --- a/test/shared/meta-token-redeemer.spec.ts +++ b/test/shared/meta-token-redeemer.spec.ts @@ -21,37 +21,26 @@ describe("MetaTokenRedeemer", () => { alice = accounts[1] bob = accounts[2] aliceAddress = await alice.getAddress() - mta = await new MockERC20__factory(deployer).deploy( - "Meta Token", - "mta", - 18, - await deployer.getAddress(), - simpleToExactAmount(10_000_000), - ) - weth = await new MockERC20__factory(deployer).deploy( - "WETH Token", - "weth", - 18, - await deployer.getAddress(), - simpleToExactAmount(1_000_000), - ) + mta = await new MockERC20__factory(deployer).deploy("Meta Token", "mta", 18, await deployer.getAddress(), 100_000_000) + weth = await new MockERC20__factory(deployer).deploy("WETH Token", "weth", 18, await deployer.getAddress(), 3_000) redeemer = await new MetaTokenRedeemer__factory(deployer).deploy(mta.address, weth.address, ONE_DAY.mul(90)) // send mta to alice - mta.transfer(aliceAddress, simpleToExactAmount(10_000)) - mta.transfer(await bob.getAddress(), simpleToExactAmount(10_000)) + mta.transfer(aliceAddress, simpleToExactAmount(20_000_000)) + mta.transfer(await bob.getAddress(), simpleToExactAmount(20_000_000)) }) it("constructor parameters are correct", async () => { + const registerPeriod = await redeemer.registerPeriod() expect(await redeemer.MTA(), "MTA").to.be.eq(mta.address) expect(await redeemer.WETH(), "WETH").to.be.eq(weth.address) expect(await redeemer.PERIOD_DURATION(), "PERIOD_DURATION").to.be.eq(ONE_DAY.mul(90)) - expect(await redeemer.periodStart(), "periodStart").to.be.eq(ZERO) - expect(await redeemer.periodEnd(), "periodEnd").to.be.eq(ZERO) + expect(registerPeriod.start, "periodStart").to.be.eq(ZERO) + expect(registerPeriod.end, "periodEnd").to.be.eq(ZERO) expect(await redeemer.totalFunded(), "totalFunded").to.be.eq(ZERO) expect(await redeemer.totalRegistered(), "totalRegistered").to.be.eq(ZERO) expect(await redeemer.balances(aliceAddress), "balances").to.be.eq(ZERO) }) it("fails to register if period has not started", async () => { - expect(await redeemer.periodStart(), "periodStart").to.be.eq(ZERO) + expect((await redeemer.registerPeriod()).start, "periodStart").to.be.eq(ZERO) await expect(redeemer.register(ZERO), "register").to.be.revertedWith("Registration period not started") }) @@ -68,14 +57,16 @@ describe("MetaTokenRedeemer", () => { expect(await redeemer.totalFunded(), "total funded").to.be.eq(wethAmount.div(2)) expect(await weth.balanceOf(redeemer.address), "weth balance").to.be.eq(redeemerWethBalance.add(wethAmount.div(2))) // Fist time it is invoked , period details are set - expect(await redeemer.periodStart(), "period start").to.be.eq(now.add(1)) - expect(await redeemer.periodEnd(), "period end").to.be.eq(now.add(1).add(await redeemer.PERIOD_DURATION())) + const registerPeriod = await redeemer.registerPeriod() + expect(registerPeriod.start, "period start").to.be.eq(now.add(1)) + expect(registerPeriod.end, "period end").to.be.eq(now.add(1).add(await redeemer.PERIOD_DURATION())) }) it("funds again WETH into redeemer", async () => { const wethAmount = await weth.balanceOf(await deployer.getAddress()) + let registerPeriod = await redeemer.registerPeriod() - const periodStart = await redeemer.periodStart() - const periodEnd = await redeemer.periodEnd() + const periodStart = registerPeriod.start + const periodEnd = registerPeriod.end const totalFunded = await redeemer.totalFunded() const redeemerWethBalance = await weth.balanceOf(redeemer.address) @@ -88,8 +79,9 @@ describe("MetaTokenRedeemer", () => { expect(await redeemer.totalFunded(), "total funded").to.be.eq(totalFunded.add(wethAmount)) expect(await weth.balanceOf(redeemer.address), "weth balance").to.be.eq(redeemerWethBalance.add(wethAmount)) // After first time, period details do not change - expect(await redeemer.periodStart(), "period start").to.be.eq(periodStart) - expect(await redeemer.periodEnd(), "period end").to.be.eq(periodEnd) + registerPeriod = await redeemer.registerPeriod() + expect(registerPeriod.start, "period start").to.be.eq(periodStart) + expect(registerPeriod.end, "period end").to.be.eq(periodEnd) }) const registerTests = [{ user: "alice" }, { user: "bob" }] registerTests.forEach((test, i) => @@ -119,18 +111,17 @@ describe("MetaTokenRedeemer", () => { ) it("fails to redeem if Redeem period not started", async () => { const now = await getTimestamp() - const periodEnd = await redeemer.periodEnd() - - expect(now, "now < periodEnd").to.be.lt(periodEnd) + const registerPeriod = await redeemer.registerPeriod() + expect(now, "now < periodEnd").to.be.lt(registerPeriod.end) await expect(redeemer.redeem(), "redeem").to.be.revertedWith("Redeem period not started") }) it("fails to fund or register if register period ended", async () => { await increaseTime(ONE_DAY.mul(91)) - const periodEnd = await redeemer.periodEnd() + const registerPeriod = await redeemer.registerPeriod() const now = await getTimestamp() - expect(now, "now > periodEnd").to.be.gt(periodEnd) + expect(now, "now > periodEnd").to.be.gt(registerPeriod.end) await expect(redeemer.fund(ZERO), "fund").to.be.revertedWith("Funding period ended") await expect(redeemer.register(ZERO), "register").to.be.revertedWith("Registration period ended") @@ -145,7 +136,7 @@ describe("MetaTokenRedeemer", () => { const totalRegistered = await redeemer.totalRegistered() const totalFunded = await redeemer.totalFunded() - const expectedWeth = registeredAmount.mul(totalRegistered).div(totalFunded) + const expectedWeth = registeredAmount.mul(totalFunded).div(totalRegistered) expect(registeredAmount, "registeredAmount").to.be.gt(ZERO)