-
Notifications
You must be signed in to change notification settings - Fork 75
feat(contracts): Add LP logic #6
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
Changes from all commits
42179cb
1e37ad4
a0189b6
f2871b3
d6e771f
f28bb71
5e3facf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,13 @@ pragma solidity ^0.8.0; | |
| import "@uma/core/contracts/common/implementation/Testable.sol"; | ||
| import "@uma/core/contracts/common/implementation/Lockable.sol"; | ||
| import "@uma/core/contracts/common/implementation/MultiCaller.sol"; | ||
| import "@uma/core/contracts/common/implementation/ExpandedERC20.sol"; | ||
|
|
||
| import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | ||
| import "@openzeppelin/contracts/access/Ownable.sol"; | ||
| import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
| import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
| import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
| import "@openzeppelin/contracts/utils/Address.sol"; | ||
|
|
||
| interface WETH9Like { | ||
| function withdraw(uint256 wad) external; | ||
|
|
@@ -15,29 +19,50 @@ interface WETH9Like { | |
| } | ||
|
|
||
| contract HubPool is Testable, Lockable, MultiCaller, Ownable { | ||
| using SafeERC20 for IERC20; | ||
| using Address for address; | ||
| struct LPToken { | ||
| address lpToken; | ||
| bool isWeth; | ||
| ExpandedERC20 lpToken; | ||
| bool isEnabled; | ||
| } | ||
|
|
||
| WETH9Like public l1Weth; | ||
|
|
||
| mapping(address => LPToken) public lpTokens; // Mapping of L1TokenAddress to the associated LPToken. | ||
|
|
||
| constructor(address timerAddress) Testable(timerAddress) {} | ||
| event LiquidityAdded( | ||
| address indexed l1Token, | ||
| uint256 amount, | ||
| uint256 lpTokensMinted, | ||
| address indexed liquidityProvider | ||
| ); | ||
| event LiquidityRemoved( | ||
| address indexed l1Token, | ||
| uint256 amount, | ||
| uint256 lpTokensBurnt, | ||
| address indexed liquidityProvider | ||
| ); | ||
|
|
||
| constructor(address _l1Weth, address _timerAddress) Testable(_timerAddress) { | ||
| l1Weth = WETH9Like(_l1Weth); | ||
| } | ||
|
|
||
| /************************************************* | ||
| * ADMIN FUNCTIONS * | ||
| *************************************************/ | ||
|
|
||
| // TODO: the two functions below should be called by the Admin contract. | ||
| function enableL1TokenForLiquidityProvision( | ||
| address l1Token, | ||
| bool isWeth, | ||
| string memory lpTokenName, | ||
| string memory lpTokenSymbol | ||
| ) public onlyOwner { | ||
| ERC20 lpToken = new ERC20(lpTokenName, lpTokenSymbol); | ||
| lpTokens[l1Token] = LPToken({ lpToken: address(lpToken), isWeth: isWeth, isEnabled: true }); | ||
| function enableL1TokenForLiquidityProvision(address l1Token) public onlyOwner { | ||
| // NOTE: if we run out of bytecode this logic could be refactored into a custom token factory that does the | ||
| // appends and permission setting. | ||
| ExpandedERC20 lpToken = new ExpandedERC20( | ||
| append("Across ", IERC20Metadata(l1Token).name(), " LP Token"), // LP Token Name | ||
| append("Av2-", IERC20Metadata(l1Token).symbol(), "-LP"), // LP Token Symbol | ||
| IERC20Metadata(l1Token).decimals() // LP Token Decimals | ||
| ); | ||
|
Comment on lines
+58
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor note: we could use a contract like the TokenFactory here down the line so the ERC20 bytecode doesn't need to live in this contract (and thus, we give ourselves more room in the bytecode limit). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a good point. we can make this change if we run out of space. I'll add a comment. |
||
| lpToken.addMember(1, address(this)); // Set this contract as the LP Token's minter. | ||
| lpToken.addMember(2, address(this)); // Set this contract as the LP Token's burner. | ||
| lpTokens[l1Token] = LPToken({ lpToken: lpToken, isEnabled: true }); | ||
| } | ||
|
|
||
| function disableL1TokenForLiquidityProvision(address l1Token) public onlyOwner { | ||
|
|
@@ -51,9 +76,49 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable { | |
| * LIQUIDITY PROVIDER FUNCTIONS * | ||
| *************************************************/ | ||
|
|
||
| function addLiquidity(address token, uint256 amount) public {} | ||
| function addLiquidity(address l1Token, uint256 l1TokenAmount) public payable { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this all new code? Or is it all copied? Or is it a mix? If a mix, can you comment on the parts that you have changed (throughout the file)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's copied but changed to include the new mapping structure. the v1 version of this did not specify the |
||
| require(lpTokens[l1Token].isEnabled); | ||
| // If this is the weth pool and the caller sends msg.value then the msg.value must match the l1TokenAmount. | ||
| // Else, msg.value must be set to 0. | ||
| require((address(l1Token) == address(l1Weth) && msg.value == l1TokenAmount) || msg.value == 0, "Bad msg.value"); | ||
|
|
||
| // Since `exchangeRateCurrent()` reads this contract's balance and updates contract state using it, | ||
| // we must call it first before transferring any tokens to this contract. | ||
| uint256 lpTokensToMint = (l1TokenAmount * 1e18) / _exchangeRateCurrent(); | ||
| ExpandedERC20(lpTokens[l1Token].lpToken).mint(msg.sender, lpTokensToMint); | ||
| // liquidReserves += l1TokenAmount; //TODO: Add this when we have the liquidReserves variable implemented. | ||
|
|
||
| if (address(l1Token) == address(l1Weth) && msg.value > 0) | ||
| WETH9Like(address(l1Token)).deposit{ value: msg.value }(); | ||
| else IERC20(l1Token).safeTransferFrom(msg.sender, address(this), l1TokenAmount); | ||
|
|
||
| emit LiquidityAdded(l1Token, l1TokenAmount, lpTokensToMint, msg.sender); | ||
| } | ||
|
|
||
| function removeLiquidity( | ||
| address l1Token, | ||
| uint256 lpTokenAmount, | ||
| bool sendEth | ||
| ) public nonReentrant { | ||
| // Can only send eth on withdrawing liquidity iff this is the WETH pool. | ||
| require(l1Token == address(l1Weth) || !sendEth, "Cant send eth"); | ||
| uint256 l1TokensToReturn = (lpTokenAmount * _exchangeRateCurrent()) / 1e18; | ||
|
|
||
| // Check that there is enough liquid reserves to withdraw the requested amount. | ||
| // require(liquidReserves >= (pendingReserves + l1TokensToReturn), "Utilization too high to remove"); // TODO: add this when we have liquid reserves variable implemented. | ||
|
|
||
| ExpandedERC20(lpTokens[l1Token].lpToken).burnFrom(msg.sender, lpTokenAmount); | ||
| // liquidReserves -= l1TokensToReturn; // TODO: add this when we have liquid reserves variable implemented. | ||
|
|
||
| function exchangeRateCurrent(address token) public returns (uint256) {} | ||
| if (sendEth) _unwrapWETHTo(payable(msg.sender), l1TokensToReturn); | ||
| else IERC20(l1Token).safeTransfer(msg.sender, l1TokensToReturn); | ||
|
|
||
| emit LiquidityRemoved(l1Token, l1TokensToReturn, lpTokenAmount, msg.sender); | ||
| } | ||
|
|
||
| function exchangeRateCurrent() public nonReentrant returns (uint256) { | ||
| return _exchangeRateCurrent(); | ||
| } | ||
|
|
||
| function liquidityUtilizationPostRelay(address token, uint256 relayedAmount) public returns (uint256) {} | ||
|
|
||
|
|
@@ -72,4 +137,29 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable { | |
| uint256[] memory netSendAmounts, | ||
| bytes32[] memory inclusionProof | ||
| ) public {} | ||
|
|
||
| function _exchangeRateCurrent() internal pure returns (uint256) { | ||
| return 1e18; | ||
| } | ||
|
|
||
| // Unwraps ETH and does a transfer to a recipient address. If the recipient is a smart contract then sends WETH. | ||
| function _unwrapWETHTo(address payable to, uint256 amount) internal { | ||
| if (address(to).isContract()) { | ||
| IERC20(address(l1Weth)).safeTransfer(to, amount); | ||
| } else { | ||
| l1Weth.withdraw(amount); | ||
| to.transfer(amount); | ||
| } | ||
| } | ||
|
|
||
| function append( | ||
| string memory a, | ||
| string memory b, | ||
| string memory c | ||
| ) internal pure returns (string memory) { | ||
| return string(abi.encodePacked(a, b, c)); | ||
| } | ||
|
|
||
| // Added to enable the BridgePool to receive ETH. used when unwrapping Weth. | ||
| receive() external payable {} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we revert if this comes from an address that is not the WETH contract? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's an interesting idea but its somewhat inconsistant. we dont prevent users from sending tokens so why should we prevent them from sending eth? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see what you're saying. ETH transfers work differently, so I would argue it's more likely to send ETH accidentally. However, this would incur a slight gas cost, so it probably isn't worth the cost of the check. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,12 +23,11 @@ task("accounts", "Prints the list of accounts", async (taskArgs, hre) => { | |
| // Go to https://hardhat.org/config/ to learn more | ||
|
|
||
| const config: HardhatUserConfig = { | ||
| solidity: "0.8.11", | ||
| solidity: { compilers: [{ version: "0.8.11", settings: { optimizer: { enabled: true, runs: 200 } } }] }, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Set runs to 1_000_000? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||
| networks: { | ||
| ropsten: { | ||
| url: process.env.ROPSTEN_URL || "", | ||
| accounts: | ||
| process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], | ||
| accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use a mnemonic? Isn't that the preferred way of generating accounts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did not change this, this was just a listing change. I think we should ignore this until we get there but ye I agree a mnemonic would be better |
||
| }, | ||
| }, | ||
| gasReporter: { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { TokenRolesEnum, ZERO_ADDRESS } from "@uma/common"; | ||
| import { getContractFactory, toWei } from "./utils"; | ||
| import { Contract, BigNumber } from "ethers"; | ||
|
|
||
| export async function deployHubPoolTestHelperContracts(deployerWallet: any) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||
| // Useful contracts. | ||
| const timer = await (await getContractFactory("Timer", deployerWallet)).deploy(); | ||
|
|
||
| // Create 3 tokens: WETH for wrapping unwrapping and 2 ERC20s with different decimals. | ||
| const weth = await (await getContractFactory("WETH9", deployerWallet)).deploy(); | ||
| const usdc = await (await getContractFactory("ExpandedERC20", deployerWallet)).deploy("USD Coin", "USDC", 6); | ||
| await usdc.addMember(TokenRolesEnum.MINTER, deployerWallet.address); | ||
| const dai = await (await getContractFactory("ExpandedERC20", deployerWallet)).deploy("DAI Stablecoin", "DAI", 18); | ||
| await dai.addMember(TokenRolesEnum.MINTER, deployerWallet.address); | ||
|
|
||
| // Deploy the hubPool | ||
| const hubPool = await (await getContractFactory("HubPool", deployerWallet)).deploy(weth.address, timer.address); | ||
|
|
||
| return { timer, weth, usdc, dai, hubPool }; | ||
| } | ||
|
|
||
| export async function seedWallet( | ||
| walletToFund: any, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. |
||
| tokens: Contract[], | ||
| weth: Contract | undefined, | ||
| amountToSeedWith: number | BigNumber | ||
| ) { | ||
| for (const token of tokens) await token.mint(walletToFund.address, amountToSeedWith); | ||
|
|
||
| if (weth) await weth.connect(walletToFund).deposit({ value: amountToSeedWith }); | ||
| } | ||
|
|
||
| export async function enableTokensForLiquidityProvision(owner: any, hubPool: Contract, tokens: Contract[]) { | ||
| let lpTokens = []; | ||
| for (const token of tokens) { | ||
| await hubPool.enableL1TokenForLiquidityProvision(token.address); | ||
| lpTokens.push( | ||
| await ( | ||
| await getContractFactory("ExpandedERC20", owner) | ||
| ).attach((await hubPool.callStatic.lpTokens(token.address)).lpToken) | ||
| ); | ||
| } | ||
| return lpTokens; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,124 @@ | ||
| import { expect } from "chai"; | ||
| import { Contract } from "ethers"; | ||
| import { deployContract, MockProvider } from "ethereum-waffle"; | ||
| import { getContract } from "./utils"; | ||
| import { ethers } from "hardhat"; | ||
| import { getContractFactory, fromWei, toBN, SignerWithAddress } from "./utils"; | ||
| import { deployHubPoolTestHelperContracts, enableTokensForLiquidityProvision, seedWallet } from "./HubPool.Fixture"; | ||
| import { amountToSeedWallets, amountToLp } from "./HubPool.constants"; | ||
|
|
||
| let hubPool: Contract; | ||
| let timer: Contract; | ||
| let hubPool: Contract, weth: Contract, usdc: Contract, dai: Contract; | ||
| let wethLpToken: Contract, usdcLpToken: Contract, daiLpToken: Contract; | ||
| let owner: SignerWithAddress, liquidityProvider: SignerWithAddress, other: SignerWithAddress; | ||
|
|
||
| describe("HubPool LiquidityProvision", async function () { | ||
| const [owner] = new MockProvider().getWallets(); | ||
| describe("HubPool Liquidity Provision", function () { | ||
| beforeEach(async function () { | ||
| [owner, liquidityProvider, other] = await ethers.getSigners(); | ||
| ({ weth, usdc, dai, hubPool } = await deployHubPoolTestHelperContracts(owner)); | ||
| [wethLpToken, usdcLpToken, daiLpToken] = await enableTokensForLiquidityProvision(owner, hubPool, [weth, usdc, dai]); | ||
|
|
||
| before(async function () { | ||
| timer = await deployContract(owner, await getContract("Timer")); | ||
| // mint some fresh tokens and deposit ETH for weth for the liquidity provider. | ||
| await seedWallet(liquidityProvider, [usdc, dai], weth, amountToSeedWallets); | ||
| }); | ||
|
|
||
| it("Adding ER20 liquidity correctly pulls tokens and mints LP tokens", async function () { | ||
| const daiLpToken = await ( | ||
| await getContractFactory("ExpandedERC20", owner) | ||
| ).attach((await hubPool.callStatic.lpTokens(dai.address)).lpToken); | ||
|
|
||
| // Balances of collateral before should equal the seed amount and there should be 0 outstanding LP tokens. | ||
| expect(await dai.balanceOf(liquidityProvider.address)).to.equal(amountToSeedWallets); | ||
| expect(await daiLpToken.balanceOf(liquidityProvider.address)).to.equal(0); | ||
|
|
||
| await dai.connect(liquidityProvider).approve(hubPool.address, amountToLp); | ||
| await hubPool.connect(liquidityProvider).addLiquidity(dai.address, amountToLp); | ||
|
|
||
| //The balance of the collateral should be equal to the original amount minus the LPed amount. The balance of LP | ||
| // tokens should be equal to the amount of LP tokens divided by the exchange rate current. This rate starts at 1e18, | ||
| // so this should equal the amount minted. | ||
| expect(await dai.balanceOf(liquidityProvider.address)).to.equal(amountToSeedWallets.sub(amountToLp)); | ||
| expect(await daiLpToken.balanceOf(liquidityProvider.address)).to.equal(amountToLp); | ||
| expect(await daiLpToken.totalSupply()).to.equal(amountToLp); | ||
| }); | ||
| it("Removing ER20 liquidity burns LP tokens and returns collateral", async function () { | ||
| await dai.connect(liquidityProvider).approve(hubPool.address, amountToLp); | ||
| await hubPool.connect(liquidityProvider).addLiquidity(dai.address, amountToLp); | ||
|
|
||
| // Next, try remove half the liquidity. This should modify the balances, as expected. | ||
| await hubPool.connect(liquidityProvider).removeLiquidity(dai.address, amountToLp.div(2), false); | ||
|
|
||
| expect(await dai.balanceOf(liquidityProvider.address)).to.equal(amountToSeedWallets.sub(amountToLp.div(2))); | ||
| expect(await daiLpToken.balanceOf(liquidityProvider.address)).to.equal(amountToLp.div(2)); | ||
| expect(await daiLpToken.totalSupply()).to.equal(amountToLp.div(2)); | ||
|
|
||
| // Removing more than the total balance of LP tokens should throw. | ||
| await expect(hubPool.connect(other).removeLiquidity(dai.address, amountToLp, false)).to.be.reverted; | ||
|
|
||
| // Cant try receive ETH if the token is pool token is not WETH. Try redeem 1/3 of the original amount added. This is | ||
| // less than the total amount the wallet has left (since we removed half the amount before). | ||
| await expect(hubPool.connect(other).removeLiquidity(dai.address, amountToLp.div(3), true)).to.be.reverted; | ||
|
|
||
| hubPool = await deployContract(owner, await getContract("HubPool"), [timer.address]); | ||
| //Can remove the remaining LP tokens for a balance of 0. | ||
| await hubPool.connect(liquidityProvider).removeLiquidity(dai.address, amountToLp.div(2), false); | ||
| expect(await dai.balanceOf(liquidityProvider.address)).to.equal(amountToSeedWallets); // back to starting balance. | ||
| expect(await daiLpToken.balanceOf(liquidityProvider.address)).to.equal(0); // All LP tokens burnt. | ||
| expect(await daiLpToken.totalSupply()).to.equal(0); | ||
| }); | ||
| it("Only owner can enable L1 Tokens for liquidity provision", async function () { | ||
| expect(await hubPool.callStatic.timerAddress()).to.equal(timer.address); | ||
| it("Adding ETH liquidity correctly wraps to WETH and mints LP tokens", async function () { | ||
| // Depositor can send WETH, if they have. Explicitly set the value to 0 to ensure we dont send any eth with the tx. | ||
| await weth.connect(liquidityProvider).approve(hubPool.address, amountToLp); | ||
| await hubPool.connect(liquidityProvider).addLiquidity(weth.address, amountToLp, { value: 0 }); | ||
| expect(await weth.balanceOf(liquidityProvider.address)).to.equal(amountToSeedWallets.sub(amountToLp)); | ||
| expect(await wethLpToken.balanceOf(liquidityProvider.address)).to.equal(amountToLp); | ||
|
|
||
| // Next, try depositing ETH with the transaction. No WETH should be sent. The ETH send with the TX should be | ||
| // wrapped for the user and LP tokens minted. Send the deposit and check the ether balance changes as expected. | ||
| await expect(() => | ||
| hubPool.connect(liquidityProvider).addLiquidity(weth.address, amountToLp, { value: amountToLp }) | ||
| ).to.changeEtherBalance(weth, amountToLp); // WETH's Ether balance should increase by the amount LPed. | ||
| // The weth Token balance should have stayed the same as no weth was spent. | ||
| expect(await weth.balanceOf(liquidityProvider.address)).to.equal(amountToSeedWallets.sub(amountToLp)); | ||
| // However, the WETH LP token should have increase by the amount of LP tokens minted, as 2 x amountToLp. | ||
| expect(await wethLpToken.balanceOf(liquidityProvider.address)).to.equal(amountToLp.mul(2)); | ||
| // Equally, the total WETH supply should have increased by the amount of LP tokens minted as they were deposited. | ||
| expect(await wethLpToken.totalSupply()).to.equal(amountToLp.mul(2)); | ||
| expect(await weth.totalSupply()).to.equal(amountToLp.add(amountToSeedWallets)); | ||
| }); | ||
|
|
||
| it("Removing ETH liquidity can send back WETH or ETH depending on the users choice", async function () { | ||
| await weth.connect(liquidityProvider).approve(hubPool.address, amountToLp); | ||
| await hubPool.connect(liquidityProvider).addLiquidity(weth.address, amountToLp); | ||
|
|
||
| // Remove half the liquidity as WETH (set sendETH = false). This should modify the weth bal and not the eth bal. | ||
| await expect(() => | ||
| hubPool.connect(liquidityProvider).removeLiquidity(weth.address, amountToLp.div(2), false) | ||
| ).to.changeEtherBalance(liquidityProvider, 0); | ||
|
|
||
| expect(await weth.balanceOf(liquidityProvider.address)).to.equal(amountToSeedWallets.sub(amountToLp.div(2))); // WETH balance should increase by the amount removed. | ||
|
|
||
| // Next, remove half the liquidity as ETH (set sendETH = true). This should modify the eth bal but not the weth bal. | ||
| await expect(() => | ||
| hubPool.connect(liquidityProvider).removeLiquidity(weth.address, amountToLp.div(2), true) | ||
| ).to.changeEtherBalance(liquidityProvider, amountToLp.div(2)); // There should be ETH transferred, not WETH. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this syntax is great! unfortunately it cant be chained along with a |
||
| expect(await weth.balanceOf(liquidityProvider.address)).to.equal(amountToSeedWallets.sub(amountToLp.div(2))); // weth balance stayed the same. | ||
|
|
||
| // There should be no LP tokens left outstanding: | ||
| expect(await wethLpToken.balanceOf(liquidityProvider.address)).to.equal(0); | ||
| }); | ||
| it("Adding and removing non-18 decimal collateral mints the commensurate amount of LP tokens", async function () { | ||
| // USDC is 6 decimal places. Scale the amountToLp back to a normal number then up by 6 decimal places to get a 1e6 | ||
| // scaled number. i.e amountToLp is 1000 so this number will be 1e6. | ||
| const scaledAmountToLp = toBN(fromWei(amountToLp)).mul(1e6); // USDC is 6 decimal places. | ||
| await usdc.connect(liquidityProvider).approve(hubPool.address, amountToLp); | ||
| await hubPool.connect(liquidityProvider).addLiquidity(usdc.address, scaledAmountToLp); | ||
|
|
||
| // Check the balances are correct. | ||
| expect(await usdcLpToken.balanceOf(liquidityProvider.address)).to.equal(scaledAmountToLp); | ||
| expect(await usdc.balanceOf(liquidityProvider.address)).to.equal(amountToSeedWallets.sub(scaledAmountToLp)); | ||
| expect(await usdc.balanceOf(hubPool.address)).to.equal(scaledAmountToLp); | ||
|
|
||
| // Redemption should work as normal, just scaled. | ||
| await hubPool.connect(liquidityProvider).removeLiquidity(usdc.address, scaledAmountToLp.div(2), false); | ||
| expect(await usdcLpToken.balanceOf(liquidityProvider.address)).to.equal(scaledAmountToLp.div(2)); | ||
| expect(await usdc.balanceOf(liquidityProvider.address)).to.equal(amountToSeedWallets.sub(scaledAmountToLp.div(2))); | ||
| expect(await usdc.balanceOf(hubPool.address)).to.equal(scaledAmountToLp.div(2)); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: something we might consider for a follow-up. We could swap this out for an ERC20 contract that's optimized for this use case (immutable and singular minter/burner).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point. will add a comment.