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 extension for ERC20 token #2633

Merged
merged 12 commits into from
Jun 22, 2021
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@

* `ERC20Votes`: add a new extension of the `ERC20` token with support for voting snapshots and delegation. ([#2632](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2632))
* `ERC20VotesComp`: Variant of `ERC20Votes` that is compatible with Compound's `Comp` token interface but restricts supply to `uint96`. ([#2706](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2706))
* `ERC20Wrapper`: add a new extension of the `ERC20` token which wraps an underlying token. Deposit and withdraw guarantee that the total supply is backed by a corresponding amount of underlying token. ([#2633](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2633))
* Enumerables: Improve gas cost of removal in `EnumerableSet` and `EnumerableMap`.
* Enumerables: Improve gas cost of lookup in `EnumerableSet` and `EnumerableMap`.
* `Counter`: add a reset method. ([#2678](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2678))
Expand Down
17 changes: 17 additions & 0 deletions contracts/mocks/ERC20WrapperMock.sol
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../token/ERC20/extensions/ERC20Wrapper.sol";

contract ERC20WrapperMock is ERC20Wrapper {
constructor(
IERC20 _underlyingToken,
string memory name,
string memory symbol
) ERC20(name, symbol) ERC20Wrapper(_underlyingToken) {}

function recover(address account) public returns (uint256) {
return _recover(account);
}
}
3 changes: 3 additions & 0 deletions contracts/token/ERC20/README.adoc
Expand Up @@ -23,6 +23,7 @@ Additionally there are multiple custom extensions, including:
* {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC3156).
* {ERC20Votes}: support for voting and vote delegation.
* {ERC20VotesComp}: support for voting and vote delegation (compatible with Compound's tokenn, with uint96 restrictions).
* {ERC20Wrapper}: wrapper to create an ERC20 backed by another ERC20, with deposit and withdraw methods. Usefull in conjunction with {ERC20Votes}.

Finally, there are some utilities to interact with ERC20 contracts in various ways.

Expand Down Expand Up @@ -58,6 +59,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel

{{ERC20VotesComp}}

{{ERC20Wrapper}}

== Draft EIPs

The following EIPs are still in Draft status. Due to their nature as drafts, the details of these contracts may change and we cannot guarantee their xref:ROOT:releases-stability.adoc[stability]. Minor releases of OpenZeppelin Contracts may contain breaking changes for the contracts in this directory, which will be duly announced in the https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CHANGELOG.md[changelog]. The EIPs included here are used by projects in production and this may make them less likely to change significantly.
Expand Down
51 changes: 51 additions & 0 deletions contracts/token/ERC20/extensions/ERC20Wrapper.sol
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../ERC20.sol";
import "../utils/SafeERC20.sol";

/**
* @dev Extension of the ERC20 token contract to support token wrapping.
*
* User can deposit and withdraw "underlying tokens" and receive a mathing number of "wrapped tokens". This is usefull
* in conjunction with other modules. For example, combining this wrapping mechanism with ERC20Votes will allow the
* wrapping of an existing "basic" ERC20 into a governance token.
*
* _Available since v4.2._
*/
abstract contract ERC20Wrapper is ERC20 {
IERC20 public immutable underlying;

constructor(IERC20 underlyingToken) {
underlying = underlyingToken;
}

/**
* @dev Allow a user to deposit underlying tokens and mint the corresponding number of wrapped tokens.
*/
function depositFor(address account, uint256 amount) public virtual returns (bool) {
SafeERC20.safeTransferFrom(underlying, _msgSender(), address(this), amount);
_mint(account, amount);
return true;
}

/**
* @dev Allow a user to burn a number of wrapped tokens and withdraw the corresponding number of underlying tokens.
*/
function withdrawTo(address account, uint256 amount) public virtual returns (bool) {
_burn(_msgSender(), amount);
SafeERC20.safeTransfer(underlying, account, amount);
return true;
}

/**
* @dev Mint wrapped token to cover any underlyingTokens that would have been transfered by mistake. Might be
* exposed through access control.
*/
function _recover(address account) internal virtual returns (uint256) {
uint256 value = underlying.balanceOf(address(this)) - totalSupply();
_mint(account, value);
return value;
}
}
161 changes: 161 additions & 0 deletions test/token/ERC20/extensions/ERC20Wrapper.test.js
@@ -0,0 +1,161 @@
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { ZERO_ADDRESS, MAX_UINT256 } = constants;

const { shouldBehaveLikeERC20 } = require('../ERC20.behavior');

const ERC20Mock = artifacts.require('ERC20Mock');
const ERC20WrapperMock = artifacts.require('ERC20WrapperMock');

contract('ERC20', function (accounts) {
const [ initialHolder, recipient, anotherAccount ] = accounts;

const name = 'My Token';
const symbol = 'MTKN';

const initialSupply = new BN(100);

beforeEach(async function () {
this.underlying = await ERC20Mock.new(name, symbol, initialHolder, initialSupply);
this.token = await ERC20WrapperMock.new(this.underlying.address, `Wrapped ${name}`, `W${symbol}`);
});

it('has a name', async function () {
expect(await this.token.name()).to.equal(`Wrapped ${name}`);
});

it('has a symbol', async function () {
expect(await this.token.symbol()).to.equal(`W${symbol}`);
});

it('has 18 decimals', async function () {
expect(await this.token.decimals()).to.be.bignumber.equal('18');
});

it('has underlying', async function () {
expect(await this.token.underlying()).to.be.bignumber.equal(this.underlying.address);
});

describe('deposit', function () {
it('valid', async function () {
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
const { tx } = await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
frangio marked this conversation as resolved.
Show resolved Hide resolved
from: initialHolder,
to: this.token.address,
value: initialSupply,
});
expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: ZERO_ADDRESS,
to: initialHolder,
value: initialSupply,
});
});

it('missing approval', async function () {
await expectRevert(
this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }),
'ERC20: transfer amount exceeds allowance',
);
});

it('missing balance', async function () {
await this.underlying.approve(this.token.address, MAX_UINT256, { from: initialHolder });
await expectRevert(
this.token.depositFor(initialHolder, MAX_UINT256, { from: initialHolder }),
'ERC20: transfer amount exceeds balance',
);
});

it('to other account', async function () {
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
const { tx } = await this.token.depositFor(anotherAccount, initialSupply, { from: initialHolder });
expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
from: initialHolder,
to: this.token.address,
value: initialSupply,
});
expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: ZERO_ADDRESS,
to: anotherAccount,
value: initialSupply,
});
});
});

describe('withdraw', function () {
beforeEach(async function () {
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
});

it('missing balance', async function () {
await expectRevert(
this.token.withdrawTo(initialHolder, MAX_UINT256, { from: initialHolder }),
'ERC20: burn amount exceeds balance',
);
});

it('valid', async function () {
const { tx } = await this.token.withdrawTo(initialHolder, initialSupply, { from: initialHolder });
frangio marked this conversation as resolved.
Show resolved Hide resolved
expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
from: this.token.address,
to: initialHolder,
value: initialSupply,
});
expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: initialHolder,
to: ZERO_ADDRESS,
value: initialSupply,
});
});

it('to other account', async function () {
const { tx } = await this.token.withdrawTo(anotherAccount, initialSupply, { from: initialHolder });
expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
from: this.token.address,
to: anotherAccount,
value: initialSupply,
});
expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: initialHolder,
to: ZERO_ADDRESS,
value: initialSupply,
});
});
});

describe('recover', function () {
it('nothing to recover', async function () {
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });

const { tx } = await this.token.recover(anotherAccount);
expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: ZERO_ADDRESS,
to: anotherAccount,
value: '0',
});
});

it('something to recover', async function () {
await this.underlying.transfer(this.token.address, initialSupply, { from: initialHolder });

const { tx } = await this.token.recover(anotherAccount);
expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: ZERO_ADDRESS,
to: anotherAccount,
value: initialSupply,
});
});
});

describe('erc20 behaviour', function () {
beforeEach(async function () {
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
});

shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount);
});
});