From e04543684a7618c2146560484ef7d07accc29704 Mon Sep 17 00:00:00 2001 From: Fennel <0xfennel@proton.me> Date: Mon, 16 Oct 2023 22:13:11 -0700 Subject: [PATCH 1/4] Wrapper --- contracts/interfaces/IWrapper.sol | 7 + contracts/mock/MockEmptySetReserve.sol | 41 ++++ contracts/wrapper/Wrapper.sol | 85 ++++++++ package.json | 1 + .../wrapper/WrapperIntegration.test.ts | 196 ++++++++++++++++++ yarn.lock | 18 ++ 6 files changed, 348 insertions(+) create mode 100644 contracts/interfaces/IWrapper.sol create mode 100644 contracts/mock/MockEmptySetReserve.sol create mode 100644 contracts/wrapper/Wrapper.sol create mode 100644 test/integration/wrapper/WrapperIntegration.test.ts diff --git a/contracts/interfaces/IWrapper.sol b/contracts/interfaces/IWrapper.sol new file mode 100644 index 0000000..ef1975a --- /dev/null +++ b/contracts/interfaces/IWrapper.sol @@ -0,0 +1,7 @@ +//SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.17; + +interface IWrapper { + function wrap(address to) external; + function unwrap(address to) external; +} diff --git a/contracts/mock/MockEmptySetReserve.sol b/contracts/mock/MockEmptySetReserve.sol new file mode 100644 index 0000000..050d8d2 --- /dev/null +++ b/contracts/mock/MockEmptySetReserve.sol @@ -0,0 +1,41 @@ +//SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.17; + +import "@equilibria/root/token/types/Token18.sol"; +import "@equilibria/root/token/types/Token6.sol"; +import "../interfaces/IEmptySetReserve.sol"; + +/// @title MockEmptySetReserve +/// @notice Mock contract that allows the user to simulate the EmptySetReserve having partial solvency. +contract MockEmptySetReserve is IEmptySetReserve { + /// @dev DSU address + Token18 public immutable DSU; // solhint-disable-line var-name-mixedcase + + /// @dev USDC address + Token6 public immutable USDC; // solhint-disable-line var-name-mixedcase + + UFixed18 public immutable mintRatio; + UFixed18 public immutable redeemRatio; + + constructor(Token18 dsu_, Token6 usdc_, UFixed18 mintRatio_, UFixed18 redeemRatio_) { + DSU = dsu_; + USDC = usdc_; + + mintRatio = mintRatio_; + redeemRatio = redeemRatio_; + } + + function debt(address) external pure returns (UFixed18) { + return UFixed18Lib.ZERO; + } + + function repay(address borrower, UFixed18 amount) external {} + + function mint(UFixed18 amount) external { + DSU.push(msg.sender, amount.mul(mintRatio)); + } + + function redeem(UFixed18 amount) external { + USDC.push(msg.sender, amount.mul(redeemRatio)); + } +} diff --git a/contracts/wrapper/Wrapper.sol b/contracts/wrapper/Wrapper.sol new file mode 100644 index 0000000..9ce1873 --- /dev/null +++ b/contracts/wrapper/Wrapper.sol @@ -0,0 +1,85 @@ +//SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.17; + +import "@equilibria/root/token/types/Token18.sol"; +import "@equilibria/root/token/types/Token6.sol"; +import "@equilibria/root/control/unstructured/UReentrancyGuard.sol"; +import "@equilibria/root/control/unstructured/UOwnable.sol"; +import "../interfaces/ITwoWayBatcher.sol"; +import "../interfaces/IWrapper.sol"; + +/// @title Wrapper +/// @notice Helper contract for wrapping and unwrapping USDC and DSU without worrying about approvals or dependences. +/// @dev To wrap USDC, the caller must first send USDC to this contract, then call `wrap`. +/// Similarly, to unwrap DSU, the caller must first send DSU to this contract, then call +/// `unwrap`. This contract can be optionally supplied with a TwoWayBatcher to save gas. It is +/// recommended to only use this contract in contexts where the caller can atomically bundle +/// the USDC/DSU transfer with the `wrap`/`unwrap` call (e.g. if the caller is a contract that +/// can bundle the transfer with the call in a single transaction, or if the caller is an EOA +/// using flashbots-style bundles). +contract Wrapper is IWrapper, UReentrancyGuard, UOwnable { + /// @dev Reserve address + IEmptySetReserve public immutable RESERVE; // solhint-disable-line var-name-mixedcase + + /// @dev DSU address + Token18 public immutable DSU; // solhint-disable-line var-name-mixedcase + + /// @dev USDC address + Token6 public immutable USDC; // solhint-disable-line var-name-mixedcase + + /// @dev Batcher address + ITwoWayBatcher public batcher; + + /// @notice Initializes the Wrapper + /// @param reserve_ EmptySet Reserve address + /// @param batcher_ Optional TwoWayBatcher address (can be set to address(0) if doesn't exist) + /// @param dsu_ DSU Token address + /// @param usdc_ USDC Token Address + constructor(IEmptySetReserve reserve_, ITwoWayBatcher batcher_, Token18 dsu_, Token6 usdc_) { + __UReentrancyGuard__initialize(); + __UOwnable__initialize(); + + DSU = dsu_; + USDC = usdc_; + RESERVE = reserve_; + DSU.approve(address(RESERVE)); + USDC.approve(address(RESERVE)); + if (address(batcher_) != address(0)) setBatcher(batcher_); + } + + /// @notice Updates the Batcher address + /// @param batcher_ TwoWayBatcher address + function setBatcher(ITwoWayBatcher batcher_) public onlyOwner { + batcher = batcher_; + DSU.approve(address(batcher)); + USDC.approve(address(batcher)); + } + + /// @notice Wraps all USDC owned by this contract and sends the DSU to `to` + /// @dev Falls back on the non-batcher wrapping flow if no batcher is set or the batcher has + /// little DSU. + /// @param to Receiving address of resulting DSU + function wrap(address to) external nonReentrant { + UFixed18 usdcBalance = USDC.balanceOf(); + if (address(batcher) != address(0) && DSU.balanceOf(address(batcher)).gte(usdcBalance)) { + batcher.wrap(usdcBalance, to); + } else { + RESERVE.mint(usdcBalance); + DSU.push(to, usdcBalance); + } + } + + /// @notice Unwraps all DSU owned by this contract and sends the USDC to `to` + /// @dev Falls back on the non-batcher wrapping flow if no batcher is set or the batcher has + /// little USDC. + /// @param to Receiving address of resulting USDC + function unwrap(address to) external nonReentrant { + UFixed18 dsuBalance = DSU.balanceOf(); + if (address(batcher) != address(0) && USDC.balanceOf(address(batcher)).gte(dsuBalance)) { + batcher.unwrap(dsuBalance, to); + } else { + RESERVE.redeem(dsuBalance); + USDC.push(to, dsuBalance); + } + } +} diff --git a/package.json b/package.json index f2f1b98..8ed01c7 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@ethersproject/bytes": "^5.0.0", "@ethersproject/providers": "^5.3.1", "@nomicfoundation/hardhat-chai-matchers": "^1.0.4", + "@nomicfoundation/hardhat-network-helpers": "^1.0.9", "@nomiclabs/hardhat-ethers": "^2.2.1", "@nomiclabs/hardhat-etherscan": "^2.1.1", "@openzeppelin/contracts": "4.6.0", diff --git a/test/integration/wrapper/WrapperIntegration.test.ts b/test/integration/wrapper/WrapperIntegration.test.ts new file mode 100644 index 0000000..28fc6a5 --- /dev/null +++ b/test/integration/wrapper/WrapperIntegration.test.ts @@ -0,0 +1,196 @@ +import { reset } from '@nomicfoundation/hardhat-network-helpers' +import { expect } from 'chai' +import HRE from 'hardhat' +import { utils, constants, BigNumber } from 'ethers' + +import { + IERC20Metadata, + IERC20Metadata__factory, + MockEmptySetReserve, + MockEmptySetReserve__factory, + TwoWayBatcher, + TwoWayBatcher__factory, + Wrapper, + Wrapper__factory, +} from '../../../types/generated' +import { impersonateWithBalance } from '../../testutil/impersonate' +import { getContracts } from '../constant' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { parseEther, parseUnits } from 'ethers/lib/utils' + +const { ethers } = HRE + +const BATCHER_ADDRESS = '0xAEf566ca7E84d1E736f999765a804687f39D9094' +const RESERVE_ADDRESS = '0xD05aCe63789cCb35B9cE71d01e4d632a0486Da4B' + +describe('Wrapper', () => { + let dsu: IERC20Metadata + let usdc: IERC20Metadata + let owner: SignerWithAddress + let user: SignerWithAddress + let batcher: TwoWayBatcher + let wrapper: Wrapper + let wrapperNoBatcher: Wrapper + let wrapperInsolventReserve: Wrapper + let insolventReserve: MockEmptySetReserve + + beforeEach(async () => { + await reset(process.env.MAINNET_NODE_URL || '', 18333333) + ;[owner, user] = await ethers.getSigners() + const contracts = getContracts('mainnet')! + dsu = IERC20Metadata__factory.connect(contracts.DSU, owner) + usdc = IERC20Metadata__factory.connect(contracts.USDC, owner) + + batcher = TwoWayBatcher__factory.connect(BATCHER_ADDRESS, owner) + wrapper = await new Wrapper__factory(owner).deploy(RESERVE_ADDRESS, BATCHER_ADDRESS, dsu.address, usdc.address) + wrapperNoBatcher = await new Wrapper__factory(owner).deploy( + RESERVE_ADDRESS, + constants.AddressZero, + dsu.address, + usdc.address, + ) + + insolventReserve = await new MockEmptySetReserve__factory(owner).deploy( + dsu.address, + usdc.address, + parseEther('0.9'), + parseEther('0.9'), + ) + wrapperInsolventReserve = await new Wrapper__factory(owner).deploy( + insolventReserve.address, + constants.AddressZero, + dsu.address, + usdc.address, + ) + }) + + describe('#constructor', async () => { + it('sets fields', async () => { + expect(await wrapper.RESERVE()).to.equal(RESERVE_ADDRESS) + expect(await wrapper.batcher()).to.equal(BATCHER_ADDRESS) + expect(await wrapper.DSU()).to.equal(dsu.address) + expect(await wrapper.USDC()).to.equal(usdc.address) + + expect(await wrapperNoBatcher.RESERVE()).to.equal(RESERVE_ADDRESS) + expect(await wrapperNoBatcher.batcher()).to.equal(constants.AddressZero) + expect(await wrapperNoBatcher.DSU()).to.equal(dsu.address) + expect(await wrapperNoBatcher.USDC()).to.equal(usdc.address) + }) + + it('approves batcher', async () => { + expect(await dsu.allowance(wrapper.address, BATCHER_ADDRESS)).to.equal(ethers.constants.MaxUint256) + expect(await usdc.allowance(wrapper.address, BATCHER_ADDRESS)).to.equal(ethers.constants.MaxUint256) + }) + }) + + describe('#setBatcher', async () => { + let newBatcher: TwoWayBatcher + + beforeEach(async () => { + newBatcher = await new TwoWayBatcher__factory(owner).deploy(RESERVE_ADDRESS, dsu.address, usdc.address) + }) + + it('sets batcher', async () => { + await wrapper.setBatcher(newBatcher.address) + await wrapperNoBatcher.setBatcher(newBatcher.address) + expect(await wrapper.batcher()).to.equal(newBatcher.address) + expect(await wrapperNoBatcher.batcher()).to.equal(newBatcher.address) + }) + + it('blocks non-owners from setting batcher', async () => { + await expect(wrapper.connect(user).setBatcher(newBatcher.address)).to.be.revertedWithCustomError( + wrapper, + 'UOwnableNotOwnerError', + ) + await expect(wrapperNoBatcher.connect(user).setBatcher(newBatcher.address)).to.be.revertedWithCustomError( + wrapperNoBatcher, + 'UOwnableNotOwnerError', + ) + }) + }) + + describe('#wrap', async () => { + const usdcBalance = '1000' + const highUsdcBalance = '2000000' + let usdcHolder: SignerWithAddress + let originalDsuBalance: BigNumber + + beforeEach(async () => { + usdcHolder = await impersonateWithBalance(getContracts('mainnet')!.USDC_HOLDER, utils.parseEther('10')) + await usdc.connect(usdcHolder).transfer(user.address, parseUnits(usdcBalance, 6)) + originalDsuBalance = await dsu.balanceOf(user.address) + }) + + it('wraps with batcher', async () => { + await usdc.connect(user).transfer(wrapper.address, parseUnits(usdcBalance, 6)) + await expect(wrapper.connect(user).wrap(user.address)) + .to.emit(batcher, 'Wrap') + .withArgs(user.address, parseEther(usdcBalance)) + expect((await dsu.balanceOf(user.address)).sub(originalDsuBalance)).to.equal(parseEther(usdcBalance)) + }) + + it('wraps without batcher (no batcher)', async () => { + await usdc.connect(user).transfer(wrapperNoBatcher.address, parseUnits(usdcBalance, 6)) + await expect(wrapperNoBatcher.connect(user).wrap(user.address)).to.not.emit(batcher, 'Wrap') + expect((await dsu.balanceOf(user.address)).sub(originalDsuBalance)).to.equal(parseEther(usdcBalance)) + }) + + it('wraps without batcher (fall back)', async () => { + await usdc.connect(usdcHolder).transfer(user.address, parseUnits(highUsdcBalance, 6)) + await usdc.connect(user).transfer(wrapper.address, parseUnits(highUsdcBalance, 6)) + await expect(wrapper.connect(user).wrap(user.address)).to.not.emit(batcher, 'Wrap') + expect((await dsu.balanceOf(user.address)).sub(originalDsuBalance)).to.equal(parseEther(highUsdcBalance)) + }) + + it('does not wrap if partial solvency (reserve)', async () => { + await usdc.connect(usdcHolder).transfer(wrapperNoBatcher.address, parseUnits('1000', 6)) + await wrapperNoBatcher.wrap(insolventReserve.address) + + await usdc.connect(user).transfer(wrapperInsolventReserve.address, parseUnits('10', 6)) + await expect(wrapperInsolventReserve.connect(user).wrap(user.address)).to.reverted + }) + }) + + describe('#unwrap', async () => { + const dsuBalance = '1000' + const highDsuBalance = '2000000' + let usdcHolder: SignerWithAddress + let originalUsdcBalance: BigNumber + + beforeEach(async () => { + usdcHolder = await impersonateWithBalance(getContracts('mainnet')!.USDC_HOLDER, utils.parseEther('10')) + await usdc.connect(usdcHolder).transfer(wrapperNoBatcher.address, parseUnits(dsuBalance, 6)) + await wrapperNoBatcher.wrap(user.address) + originalUsdcBalance = await usdc.balanceOf(user.address) + }) + + it('unwraps with batcher', async () => { + await dsu.connect(user).transfer(wrapper.address, parseEther(dsuBalance)) + await expect(wrapper.connect(user).unwrap(user.address)) + .to.emit(batcher, 'Unwrap') + .withArgs(user.address, parseEther(dsuBalance)) + expect((await usdc.balanceOf(user.address)).sub(originalUsdcBalance)).to.equal(parseUnits(dsuBalance, 6)) + }) + + it('unwraps without batcher (no batcher)', async () => { + await dsu.connect(user).transfer(wrapperNoBatcher.address, parseEther(dsuBalance)) + await expect(wrapperNoBatcher.connect(user).unwrap(user.address)).to.not.emit(batcher, 'Unwrap') + expect((await usdc.balanceOf(user.address)).sub(originalUsdcBalance)).to.equal(parseUnits(dsuBalance, 6)) + }) + + it('unwraps without batcher (fall back)', async () => { + await usdc.connect(usdcHolder).transfer(wrapperNoBatcher.address, parseUnits(highDsuBalance, 6)) + await wrapperNoBatcher.wrap(user.address) + await dsu.connect(user).transfer(wrapperNoBatcher.address, parseEther(highDsuBalance)) + await expect(wrapperNoBatcher.connect(user).unwrap(user.address)).to.not.emit(batcher, 'Unwrap') + expect((await usdc.balanceOf(user.address)).sub(originalUsdcBalance)).to.equal(parseUnits(highDsuBalance, 6)) + }) + + it('does not unwrap if partial solvency (reserve)', async () => { + await usdc.connect(usdcHolder).transfer(insolventReserve.address, parseUnits('1000', 6)) + + await dsu.connect(user).transfer(wrapperInsolventReserve.address, parseEther('10')) + await expect(wrapperInsolventReserve.connect(user).unwrap(user.address)).to.reverted + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index e4f91d0..216d840 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1196,6 +1196,13 @@ deep-eql "^4.0.1" ordinal "^1.0.3" +"@nomicfoundation/hardhat-network-helpers@^1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.9.tgz#767449e8a2acda79306ac84626117583d95d25aa" + integrity sha512-OXWCv0cHpwLUO2u7bFxBna6dQtCC2Gg/aN/KtJLO7gmuuA28vgmVKYFRCDUqrbjujzgfwQ2aKyZ9Y3vSmDqS7Q== + dependencies: + ethereumjs-util "^7.1.4" + "@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.0.tgz#83a7367342bd053a76d04bbcf4f373fef07cf760" @@ -3127,6 +3134,17 @@ ethereumjs-util@^7.1.0: ethjs-util "0.1.6" rlp "^2.2.4" +ethereumjs-util@^7.1.4: + version "7.1.5" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz#9ecf04861e4fbbeed7465ece5f23317ad1129181" + integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg== + dependencies: + "@types/bn.js" "^5.1.0" + bn.js "^5.1.2" + create-hash "^1.1.2" + ethereum-cryptography "^0.1.3" + rlp "^2.2.4" + ethers@^4.0.40: version "4.0.49" resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.49.tgz#0eb0e9161a0c8b4761be547396bbe2fb121a8894" From 15795c6b7dca502ee066a98ad9f43cb215b8b74a Mon Sep 17 00:00:00 2001 From: Fennel <0xfennel@proton.me> Date: Mon, 16 Oct 2023 23:09:53 -0700 Subject: [PATCH 2/4] Don't assume batcher is 1:1 --- contracts/mock/MockTwoWayBatcher.sol | 26 ++++++++ contracts/wrapper/Wrapper.sol | 8 +-- .../wrapper/WrapperIntegration.test.ts | 59 ++++++++++++++----- 3 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 contracts/mock/MockTwoWayBatcher.sol diff --git a/contracts/mock/MockTwoWayBatcher.sol b/contracts/mock/MockTwoWayBatcher.sol new file mode 100644 index 0000000..fe01ff2 --- /dev/null +++ b/contracts/mock/MockTwoWayBatcher.sol @@ -0,0 +1,26 @@ +//SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.17; + +import "../batcher/TwoWayBatcher.sol"; + +contract MockTwoWayBatcher is TwoWayBatcher { + UFixed18 public immutable wrapRatio; + UFixed18 public immutable unwrapRatio; + + constructor(IEmptySetReserve reserve_, Token18 dsu_, Token6 usdc_, UFixed18 wrapRatio_, UFixed18 unwrapRatio_) + TwoWayBatcher(reserve_, dsu_, usdc_) + { + wrapRatio = wrapRatio_; + unwrapRatio = unwrapRatio_; + } + + function _wrap(UFixed18 amount, address to) override internal { + USDC.pull(msg.sender, amount, true); + DSU.push(to, amount.mul(wrapRatio)); + } + + function _unwrap(UFixed18 amount, address to) override internal { + DSU.pull(msg.sender, amount); + USDC.push(to, amount.mul(unwrapRatio)); + } +} diff --git a/contracts/wrapper/Wrapper.sol b/contracts/wrapper/Wrapper.sol index 9ce1873..7a4e017 100644 --- a/contracts/wrapper/Wrapper.sol +++ b/contracts/wrapper/Wrapper.sol @@ -62,11 +62,11 @@ contract Wrapper is IWrapper, UReentrancyGuard, UOwnable { function wrap(address to) external nonReentrant { UFixed18 usdcBalance = USDC.balanceOf(); if (address(batcher) != address(0) && DSU.balanceOf(address(batcher)).gte(usdcBalance)) { - batcher.wrap(usdcBalance, to); + batcher.wrap(usdcBalance, address(this)); } else { RESERVE.mint(usdcBalance); - DSU.push(to, usdcBalance); } + DSU.push(to, usdcBalance); } /// @notice Unwraps all DSU owned by this contract and sends the USDC to `to` @@ -76,10 +76,10 @@ contract Wrapper is IWrapper, UReentrancyGuard, UOwnable { function unwrap(address to) external nonReentrant { UFixed18 dsuBalance = DSU.balanceOf(); if (address(batcher) != address(0) && USDC.balanceOf(address(batcher)).gte(dsuBalance)) { - batcher.unwrap(dsuBalance, to); + batcher.unwrap(dsuBalance, address(this)); } else { RESERVE.redeem(dsuBalance); - USDC.push(to, dsuBalance); } + USDC.push(to, dsuBalance); } } diff --git a/test/integration/wrapper/WrapperIntegration.test.ts b/test/integration/wrapper/WrapperIntegration.test.ts index 28fc6a5..a841477 100644 --- a/test/integration/wrapper/WrapperIntegration.test.ts +++ b/test/integration/wrapper/WrapperIntegration.test.ts @@ -8,6 +8,8 @@ import { IERC20Metadata__factory, MockEmptySetReserve, MockEmptySetReserve__factory, + MockTwoWayBatcher, + MockTwoWayBatcher__factory, TwoWayBatcher, TwoWayBatcher__factory, Wrapper, @@ -31,8 +33,9 @@ describe('Wrapper', () => { let batcher: TwoWayBatcher let wrapper: Wrapper let wrapperNoBatcher: Wrapper - let wrapperInsolventReserve: Wrapper - let insolventReserve: MockEmptySetReserve + let wrapperInsolvent: Wrapper + let reserveInsolvent: MockEmptySetReserve + let batcherInsolvent: MockTwoWayBatcher beforeEach(async () => { await reset(process.env.MAINNET_NODE_URL || '', 18333333) @@ -50,18 +53,25 @@ describe('Wrapper', () => { usdc.address, ) - insolventReserve = await new MockEmptySetReserve__factory(owner).deploy( + reserveInsolvent = await new MockEmptySetReserve__factory(owner).deploy( dsu.address, usdc.address, parseEther('0.9'), parseEther('0.9'), ) - wrapperInsolventReserve = await new Wrapper__factory(owner).deploy( - insolventReserve.address, + wrapperInsolvent = await new Wrapper__factory(owner).deploy( + reserveInsolvent.address, constants.AddressZero, dsu.address, usdc.address, ) + batcherInsolvent = await new MockTwoWayBatcher__factory(owner).deploy( + RESERVE_ADDRESS, + dsu.address, + usdc.address, + parseEther('0.9'), + parseEther('0.9'), + ) }) describe('#constructor', async () => { @@ -125,7 +135,7 @@ describe('Wrapper', () => { await usdc.connect(user).transfer(wrapper.address, parseUnits(usdcBalance, 6)) await expect(wrapper.connect(user).wrap(user.address)) .to.emit(batcher, 'Wrap') - .withArgs(user.address, parseEther(usdcBalance)) + .withArgs(wrapper.address, parseEther(usdcBalance)) expect((await dsu.balanceOf(user.address)).sub(originalDsuBalance)).to.equal(parseEther(usdcBalance)) }) @@ -142,12 +152,22 @@ describe('Wrapper', () => { expect((await dsu.balanceOf(user.address)).sub(originalDsuBalance)).to.equal(parseEther(highUsdcBalance)) }) - it('does not wrap if partial solvency (reserve)', async () => { + it('does not wrap if batcher has partial solvency', async () => { + await wrapperInsolvent.setBatcher(batcherInsolvent.address) + await usdc.connect(usdcHolder).transfer(wrapperNoBatcher.address, parseUnits('1000', 6)) - await wrapperNoBatcher.wrap(insolventReserve.address) + await wrapperNoBatcher.wrap(batcherInsolvent.address) - await usdc.connect(user).transfer(wrapperInsolventReserve.address, parseUnits('10', 6)) - await expect(wrapperInsolventReserve.connect(user).wrap(user.address)).to.reverted + await usdc.connect(user).transfer(wrapperInsolvent.address, parseUnits('10', 6)) + await expect(wrapperInsolvent.connect(user).wrap(user.address)).to.reverted + }) + + it('does not wrap if reserve has partial solvency', async () => { + await usdc.connect(usdcHolder).transfer(wrapperNoBatcher.address, parseUnits('1000', 6)) + await wrapperNoBatcher.wrap(reserveInsolvent.address) + + await usdc.connect(user).transfer(wrapperInsolvent.address, parseUnits('10', 6)) + await expect(wrapperInsolvent.connect(user).wrap(user.address)).to.reverted }) }) @@ -168,7 +188,7 @@ describe('Wrapper', () => { await dsu.connect(user).transfer(wrapper.address, parseEther(dsuBalance)) await expect(wrapper.connect(user).unwrap(user.address)) .to.emit(batcher, 'Unwrap') - .withArgs(user.address, parseEther(dsuBalance)) + .withArgs(wrapper.address, parseEther(dsuBalance)) expect((await usdc.balanceOf(user.address)).sub(originalUsdcBalance)).to.equal(parseUnits(dsuBalance, 6)) }) @@ -186,11 +206,20 @@ describe('Wrapper', () => { expect((await usdc.balanceOf(user.address)).sub(originalUsdcBalance)).to.equal(parseUnits(highDsuBalance, 6)) }) - it('does not unwrap if partial solvency (reserve)', async () => { - await usdc.connect(usdcHolder).transfer(insolventReserve.address, parseUnits('1000', 6)) + it('does not unwrap if batcher has partial solvency', async () => { + await wrapperInsolvent.setBatcher(batcherInsolvent.address) + + await usdc.connect(usdcHolder).transfer(batcherInsolvent.address, parseUnits('1000', 6)) + + await dsu.connect(user).transfer(wrapperInsolvent.address, parseEther('10')) + await expect(wrapperInsolvent.connect(user).unwrap(user.address)).to.reverted + }) + + it('does not unwrap if reserve has partial solvency', async () => { + await usdc.connect(usdcHolder).transfer(reserveInsolvent.address, parseUnits('1000', 6)) - await dsu.connect(user).transfer(wrapperInsolventReserve.address, parseEther('10')) - await expect(wrapperInsolventReserve.connect(user).unwrap(user.address)).to.reverted + await dsu.connect(user).transfer(wrapperInsolvent.address, parseEther('10')) + await expect(wrapperInsolvent.connect(user).unwrap(user.address)).to.reverted }) }) }) From 35dbd9754d45ad07ab13f98f06410fbd2b4fb420 Mon Sep 17 00:00:00 2001 From: Fennel <0xfennel@proton.me> Date: Mon, 16 Oct 2023 23:25:12 -0700 Subject: [PATCH 3/4] typo --- contracts/wrapper/Wrapper.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/wrapper/Wrapper.sol b/contracts/wrapper/Wrapper.sol index 7a4e017..09337cc 100644 --- a/contracts/wrapper/Wrapper.sol +++ b/contracts/wrapper/Wrapper.sol @@ -57,7 +57,7 @@ contract Wrapper is IWrapper, UReentrancyGuard, UOwnable { /// @notice Wraps all USDC owned by this contract and sends the DSU to `to` /// @dev Falls back on the non-batcher wrapping flow if no batcher is set or the batcher has - /// little DSU. + /// too little DSU. /// @param to Receiving address of resulting DSU function wrap(address to) external nonReentrant { UFixed18 usdcBalance = USDC.balanceOf(); @@ -71,7 +71,7 @@ contract Wrapper is IWrapper, UReentrancyGuard, UOwnable { /// @notice Unwraps all DSU owned by this contract and sends the USDC to `to` /// @dev Falls back on the non-batcher wrapping flow if no batcher is set or the batcher has - /// little USDC. + /// too little USDC. /// @param to Receiving address of resulting USDC function unwrap(address to) external nonReentrant { UFixed18 dsuBalance = DSU.balanceOf(); From 1e311439f0949eafc09526f92aa3101ea382e09f Mon Sep 17 00:00:00 2001 From: Fennel <0xfennel@proton.me> Date: Thu, 19 Oct 2023 11:12:33 -0700 Subject: [PATCH 4/4] Add USDC/DSU to wrapper interface --- contracts/interfaces/IBatcher.sol | 2 +- contracts/interfaces/IWrapper.sol | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/interfaces/IBatcher.sol b/contracts/interfaces/IBatcher.sol index 696aed8..508433e 100644 --- a/contracts/interfaces/IBatcher.sol +++ b/contracts/interfaces/IBatcher.sol @@ -18,7 +18,7 @@ interface IBatcher { function RESERVE() external view returns (IEmptySetReserve); // solhint-disable-line func-name-mixedcase function USDC() external view returns (Token6); // solhint-disable-line func-name-mixedcase function DSU() external view returns (Token18); // solhint-disable-line func-name-mixedcase - function totalBalance() external view returns (UFixed18); +function totalBalance() external view returns (UFixed18); function wrap(UFixed18 amount, address to) external; function unwrap(UFixed18 amount, address to) external; function rebalance() external; diff --git a/contracts/interfaces/IWrapper.sol b/contracts/interfaces/IWrapper.sol index ef1975a..74891d9 100644 --- a/contracts/interfaces/IWrapper.sol +++ b/contracts/interfaces/IWrapper.sol @@ -1,7 +1,13 @@ //SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.17; +import "@equilibria/root/token/types/Token6.sol"; +import "@equilibria/root/token/types/Token18.sol"; + interface IWrapper { + function USDC() external view returns (Token6); // solhint-disable-line func-name-mixedcase + function DSU() external view returns (Token18); // solhint-disable-line func-name-mixedcase + function wrap(address to) external; function unwrap(address to) external; }