Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds mta redeemer #380

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions contracts/shared/MetaTokenRedeemer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.6;

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;

address public immutable MTA;
naddison36 marked this conversation as resolved.
Show resolved Hide resolved
address public immutable WETH;
uint256 public immutable PERIOD_DURATION;
uint256 public periodStart;
naddison36 marked this conversation as resolved.
Show resolved Hide resolved
uint256 public periodEnd;
uint256 public totalFunded;
naddison36 marked this conversation as resolved.
Show resolved Hide resolved
uint256 public totalRegistered;
mapping(address => uint256) public balances;

/**
* @notice Emitted when the redeemer is funded.
*/
event Funded(address indexed sender, uint256 amount);
/**
* @notice Emitted when a user register MTA.
*/
event Register(address indexed sender, uint256 amount);

/**
* @notice Emitted when a user claims WETH for the registered amount.
*/
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 _periodDuration The lenght of the registration period.
*/
constructor(
address _mta,
address _weth,
uint256 _periodDuration
) {
MTA = _mta;
WETH = _weth;
PERIOD_DURATION = _periodDuration;
}

/**
* @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 {
naddison36 marked this conversation as resolved.
Show resolved Hide resolved
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 Allos user to register and transfer a given amount of MTA
naddison36 marked this conversation as resolved.
Show resolved Hide resolved
* 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);
}

/// @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");

// MTA and WETH both have 18 decimal points, no need for scaling.
redeemedAmount = (registeredAmount * totalRegistered) / totalFunded;
naddison36 marked this conversation as resolved.
Show resolved Hide resolved
balances[msg.sender] = 0;

IERC20(WETH).safeTransfer(msg.sender, redeemedAmount);

emit Redeemed(msg.sender, registeredAmount, redeemedAmount);
}
}
32 changes: 32 additions & 0 deletions tasks/deployShared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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"
import { getChain, getChainAddress } from "./utils/networkAddressFactory"

task("deploy-MetaTokenRedeemer")
.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)
const chain = getChain(hre)
const mtaAddr = MTA.address
const wethAddr = getChainAddress("UniswapEthToken", chain)

const metaTokenRedeemer = await deployContract(new MetaTokenRedeemer__factory(signer), "MetaTokenRedeemer", [
mtaAddr,
wethAddr,
BigNumber.from(taskArgs.duration),
])

await verifyEtherscan(hre, {
address: metaTokenRedeemer.address,
contract: "contracts/shared/MetaTokenRedeemer.sol:MetaTokenRedeemer",
})
})
module.exports = {}
173 changes: 173 additions & 0 deletions test/shared/meta-token-redeemer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
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 { 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

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",
"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, ONE_DAY.mul(90))
// send mta to alice
mta.transfer(aliceAddress, simpleToExactAmount(10_000))
mta.transfer(await bob.getAddress(), simpleToExactAmount(10_000))
})
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())
const redeemerWethBalance = await weth.balanceOf(redeemer.address)
await weth.approve(redeemer.address, wethAmount)
const now = await getTimestamp()
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("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 totalRegistered = await redeemer.totalRegistered()
const totalFunded = await redeemer.totalFunded()

const expectedWeth = registeredAmount.mul(totalRegistered).div(totalFunded)

expect(registeredAmount, "registeredAmount").to.be.gt(ZERO)

const tx = await redeemer.connect(alice).redeem()
expect(tx).to.emit(redeemer, "Redeemed").withArgs(aliceAddress, registeredAmount, expectedWeth)

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(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 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")
})
})