Skip to content

Commit

Permalink
Wrapper extension for ERC20 token (#2633)
Browse files Browse the repository at this point in the history
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
  • Loading branch information
Amxx and frangio authored Jun 22, 2021
1 parent 8a775cd commit 6842518
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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. Useful 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
Original file line number Diff line number Diff line change
@@ -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.
*
* Users can deposit and withdraw "underlying tokens" and receive a matching number of "wrapped tokens". This is useful
* 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. Internal
* function that can be exposed with access control if desired.
*/
function _recover(address account) internal virtual returns (uint256) {
uint256 value = underlying.balanceOf(address(this)) - totalSupply();
_mint(account, value);
return value;
}
}
181 changes: 181 additions & 0 deletions test/token/ERC20/extensions/ERC20Wrapper.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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}`);
});

afterEach(async function () {
expect(await this.underlying.balanceOf(this.token.address)).to.be.bignumber.equal(await this.token.totalSupply());
});

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', {
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 value = new BN(42);

const { tx } = await this.token.withdrawTo(initialHolder, value, { from: initialHolder });
expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
from: this.token.address,
to: initialHolder,
value: value,
});
expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: initialHolder,
to: ZERO_ADDRESS,
value: value,
});
});

it('entire balance', async function () {
const { tx } = await this.token.withdrawTo(initialHolder, initialSupply, { from: initialHolder });
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);
});
});

0 comments on commit 6842518

Please sign in to comment.