-
Notifications
You must be signed in to change notification settings - Fork 11.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Wrapper extension for ERC20 token (#2633)
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
- Loading branch information
Showing
5 changed files
with
253 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |