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

Wrapper #12

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions contracts/interfaces/IWrapper.sol
Original file line number Diff line number Diff line change
@@ -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;
}
41 changes: 41 additions & 0 deletions contracts/mock/MockEmptySetReserve.sol
Original file line number Diff line number Diff line change
@@ -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 {}

Check warning on line 32 in contracts/mock/MockEmptySetReserve.sol

View workflow job for this annotation

GitHub Actions / Lint

Code contains empty blocks

function mint(UFixed18 amount) external {
DSU.push(msg.sender, amount.mul(mintRatio));
}

function redeem(UFixed18 amount) external {
USDC.push(msg.sender, amount.mul(redeemRatio));
}
}
26 changes: 26 additions & 0 deletions contracts/mock/MockTwoWayBatcher.sol
Original file line number Diff line number Diff line change
@@ -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));
}
}
85 changes: 85 additions & 0 deletions contracts/wrapper/Wrapper.sol
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking maybe we should just make this immutable as well since we'll have to change the batcher-related code for different batcher implementations, so it will require a redeploy either way.

Maybe we should add a better interface to the batchers so we don't need to do that in the future 😓 .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we deploy a wrapper before a 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
/// too 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, address(this));
} 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
/// too 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, address(this));
} else {
RESERVE.redeem(dsuBalance);
}
USDC.push(to, dsuBalance);
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
225 changes: 225 additions & 0 deletions test/integration/wrapper/WrapperIntegration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
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,
MockTwoWayBatcher,
MockTwoWayBatcher__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 wrapperInsolvent: Wrapper
let reserveInsolvent: MockEmptySetReserve
let batcherInsolvent: MockTwoWayBatcher

beforeEach(async () => {
await reset(process.env.MAINNET_NODE_URL || '', 18333333)
;[owner, user] = await ethers.getSigners()
const contracts = getContracts('mainnet')!

Check warning on line 43 in test/integration/wrapper/WrapperIntegration.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
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,
)

reserveInsolvent = await new MockEmptySetReserve__factory(owner).deploy(
dsu.address,
usdc.address,
parseEther('0.9'),
parseEther('0.9'),
)
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 () => {
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'))

Check warning on line 129 in test/integration/wrapper/WrapperIntegration.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
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(wrapper.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 batcher has partial solvency', async () => {
await wrapperInsolvent.setBatcher(batcherInsolvent.address)

await usdc.connect(usdcHolder).transfer(wrapperNoBatcher.address, parseUnits('1000', 6))
await wrapperNoBatcher.wrap(batcherInsolvent.address)

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

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'))

Check warning on line 181 in test/integration/wrapper/WrapperIntegration.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
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(wrapper.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 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(wrapperInsolvent.address, parseEther('10'))
await expect(wrapperInsolvent.connect(user).unwrap(user.address)).to.reverted
})
})
})
Loading
Loading