Draft and lifecycles directories cleanup (#2122)

* Move Pausable into utils

* Move Strings into utils

* Move Counters into utils

* Move SignedSafeMath into math

* Remove ERC1046

* Make ERC20Snapshot.snapshot internal

* Move ERC20Snapshot into ERC20

* Add drafts deprecation notice

* Remove drafts directory

* Add changelog entry

* Apply suggestions from code review

Co-Authored-By: Francisco Giordano <frangio.1@gmail.com>

Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
pull/2112/head^2
Nicolás Venturo 5 years ago committed by GitHub
parent 8176a901a9
commit c9630526e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      CHANGELOG.md
  2. 24
      contracts/drafts/ERC1046/ERC20Metadata.sol
  3. 23
      contracts/drafts/README.adoc
  4. 5
      contracts/lifecycle/README.adoc
  5. 2
      contracts/math/README.adoc
  6. 0
      contracts/math/SignedSafeMath.sol
  7. 2
      contracts/mocks/CountersImpl.sol
  8. 12
      contracts/mocks/ERC20MetadataMock.sol
  9. 6
      contracts/mocks/ERC20SnapshotMock.sol
  10. 2
      contracts/mocks/PausableMock.sol
  11. 2
      contracts/mocks/SignedSafeMathMock.sol
  12. 2
      contracts/mocks/StringsMock.sol
  13. 2
      contracts/token/ERC20/ERC20Pausable.sol
  14. 53
      contracts/token/ERC20/ERC20Snapshot.sol
  15. 2
      contracts/token/ERC20/README.adoc
  16. 2
      contracts/token/ERC721/ERC721.sol
  17. 2
      contracts/token/ERC721/ERC721Pausable.sol
  18. 0
      contracts/utils/Counters.sol
  19. 0
      contracts/utils/Pausable.sol
  20. 6
      contracts/utils/README.adoc
  21. 7
      contracts/utils/Strings.sol
  22. 19
      docs/modules/ROOT/pages/drafts.adoc
  23. 2
      docs/modules/ROOT/pages/erc721.adoc
  24. 26
      test/drafts/ERC1046/ERC20Metadata.test.js
  25. 172
      test/drafts/TokenVesting.test.js
  26. 0
      test/math/SignedSafeMath.test.js
  27. 0
      test/token/ERC20/ERC20Snapshot.test.js
  28. 0
      test/utils/Counters.test.js
  29. 0
      test/utils/Pausable.test.js
  30. 0
      test/utils/Strings.test.js

@ -4,6 +4,11 @@
### Breaking changes
* `ECDSA`: when receiving an invalid signature, `recover` now reverts instead of returning the zero address. ([#2114](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2114))
* `Pausable`: moved to the `utils` directory. ([#2122](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2122))
* `Strings`: moved to the `utils` directory. ([#2122](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2122))
* `Counters`: moved to the `utils` directory. ([#2122](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2122))
* `SignedSafeMath`: moved to the `math` directory. ([#2122](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2122))
* `ERC20Snapshot`: moved to the `token/ERC20` directory. `snapshot` was changed into an `internal` function. ([#2122](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2122))
* `Ownable`: moved to the `access` directory. ([#2120](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2120))
* `Ownable`: removed `isOwner`. ([#2120](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2120))
* `Secondary`: removed from the library, use `Ownable` instead. ([#2120](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2120))

@ -1,24 +0,0 @@
pragma solidity ^0.6.0;
import "../../token/ERC20/IERC20.sol";
/**
* @title ERC-1047 Token Metadata
* @dev See https://eips.ethereum.org/EIPS/eip-1046
* @dev {tokenURI} must respond with a URI that implements https://eips.ethereum.org/EIPS/eip-1047
*/
contract ERC20Metadata {
string private _tokenURI;
constructor (string memory tokenURI_) public {
_setTokenURI(tokenURI_);
}
function tokenURI() external view returns (string memory) {
return _tokenURI;
}
function _setTokenURI(string memory tokenURI_) internal virtual {
_tokenURI = tokenURI_;
}
}

@ -1,23 +0,0 @@
= Drafts
Contracts in this category should be considered unstable. They are as thoroughly reviewed as everything else in OpenZeppelin Contracts, but we have doubts about their API so we don't commit to backwards compatibility. This means these contracts can receive breaking changes in a minor version, so you should pay special attention to the changelog when upgrading. For anything that is outside of this category you can read more about xref:ROOT:api-stability.adoc[API Stability].
NOTE: This page is incomplete. We're working to improve it for the next release. Stay tuned!
== ERC 20
{{ERC20Migrator}}
{{ERC20Snapshot}}
{{TokenVesting}}
== Miscellaneous
{{Counters}}
{{SignedSafeMath}}
== ERC 1046
{{ERC1046}}

@ -1,5 +0,0 @@
= Lifecycle
== Pausable
{{Pausable}}

@ -6,4 +6,6 @@ These are math-related utilities.
{{SafeMath}}
{{SignedSafeMath}}
{{Math}}

@ -1,6 +1,6 @@
pragma solidity ^0.6.0;
import "../drafts/Counters.sol";
import "../utils/Counters.sol";
contract CountersImpl {
using Counters for Counters.Counter;

@ -1,12 +0,0 @@
pragma solidity ^0.6.0;
import "../token/ERC20/ERC20.sol";
import "../drafts/ERC1046/ERC20Metadata.sol";
contract ERC20MetadataMock is ERC20, ERC20Metadata {
constructor (string memory tokenURI) public ERC20Metadata(tokenURI) { }
function setTokenURI(string memory tokenURI) public {
_setTokenURI(tokenURI);
}
}

@ -1,6 +1,6 @@
pragma solidity ^0.6.0;
import "../drafts/ERC20Snapshot.sol";
import "../token/ERC20/ERC20Snapshot.sol";
contract ERC20SnapshotMock is ERC20Snapshot {
@ -8,6 +8,10 @@ contract ERC20SnapshotMock is ERC20Snapshot {
_mint(initialAccount, initialBalance);
}
function snapshot() public {
_snapshot();
}
function mint(address account, uint256 amount) public {
_mint(account, amount);
}

@ -1,6 +1,6 @@
pragma solidity ^0.6.0;
import "../lifecycle/Pausable.sol";
import "../utils/Pausable.sol";
contract PausableMock is Pausable {
bool public drasticMeasureTaken;

@ -1,6 +1,6 @@
pragma solidity ^0.6.0;
import "../drafts/SignedSafeMath.sol";
import "../math/SignedSafeMath.sol";
contract SignedSafeMathMock {
function mul(int256 a, int256 b) public pure returns (int256) {

@ -1,6 +1,6 @@
pragma solidity ^0.6.0;
import "../drafts/Strings.sol";
import "../utils/Strings.sol";
contract StringsMock {
function fromUint256(uint256 value) public pure returns (string memory) {

@ -1,7 +1,7 @@
pragma solidity ^0.6.0;
import "./ERC20.sol";
import "../../lifecycle/Pausable.sol";
import "../../utils/Pausable.sol";
/**
* @title Pausable token

@ -1,15 +1,12 @@
pragma solidity ^0.6.0;
import "../math/SafeMath.sol";
import "../utils/Arrays.sol";
import "../drafts/Counters.sol";
import "../token/ERC20/ERC20.sol";
import "../../math/SafeMath.sol";
import "../../utils/Arrays.sol";
import "../../utils/Counters.sol";
import "./ERC20.sol";
/**
* @title ERC20 token with snapshots.
* @dev Inspired by Jordi Baylina's
* https://github.com/Giveth/minimd/blob/ea04d950eea153a04c51fa510b068b9dded390cb/contracts/MiniMeToken.sol[MiniMeToken]
* to record historical balances.
* @dev ERC20 token with snapshots.
*
* When a snapshot is made, the balances and total supply at the time of the snapshot are recorded for later
* access.
@ -21,6 +18,9 @@ import "../token/ERC20/ERC20.sol";
* @author Validity Labs AG <info@validitylabs.org>
*/
contract ERC20Snapshot is ERC20 {
// Inspired by Jordi Baylina's MiniMeToken to record historical balances:
// https://github.com/Giveth/minimd/blob/ea04d950eea153a04c51fa510b068b9dded390cb/contracts/MiniMeToken.sol
using SafeMath for uint256;
using Arrays for uint256[];
using Counters for Counters.Counter;
@ -40,10 +40,12 @@ contract ERC20Snapshot is ERC20 {
event Snapshot(uint256 id);
// Creates a new snapshot id. Balances are only stored in snapshots on demand: unless a snapshot was taken, a
// balance change will not be recorded. This means the extra added cost of storing snapshotted balances is only paid
// when required, but is also flexible enough that it allows for e.g. daily snapshots.
function snapshot() public virtual returns (uint256) {
/**
* @dev Creates a new snapshot id. Balances are only stored in snapshots on demand: unless a snapshot was taken, a
* balance change will not be recorded. This means the extra added cost of storing snapshotted balances is only paid
* when required, but is also flexible enough that it allows for e.g. daily snapshots.
*/
function _snapshot() internal virtual returns (uint256) {
_currentSnapshotId.increment();
uint256 currentId = _currentSnapshotId.current();
@ -87,19 +89,6 @@ contract ERC20Snapshot is ERC20 {
super._burn(account, value);
}
// When a valid snapshot is queried, there are three possibilities:
// a) The queried value was not modified after the snapshot was taken. Therefore, a snapshot entry was never
// created for this id, and all stored snapshot ids are smaller than the requested one. The value that corresponds
// to this id is the current one.
// b) The queried value was modified after the snapshot was taken. Therefore, there will be an entry with the
// requested id, and its value is the one to return.
// c) More snapshots were created after the requested one, and the queried value was later modified. There will be
// no entry for the requested id: the value that corresponds to it is that of the smallest snapshot id that is
// larger than the requested one.
//
// In summary, we need to find an element in an array, returning the index of the smallest value that is larger if
// it is not found, unless said value doesn't exist (e.g. when all values are smaller). Arrays.findUpperBound does
// exactly this.
function _valueAt(uint256 snapshotId, Snapshots storage snapshots)
private view returns (bool, uint256)
{
@ -107,6 +96,20 @@ contract ERC20Snapshot is ERC20 {
// solhint-disable-next-line max-line-length
require(snapshotId <= _currentSnapshotId.current(), "ERC20Snapshot: nonexistent id");
// When a valid snapshot is queried, there are three possibilities:
// a) The queried value was not modified after the snapshot was taken. Therefore, a snapshot entry was never
// created for this id, and all stored snapshot ids are smaller than the requested one. The value that corresponds
// to this id is the current one.
// b) The queried value was modified after the snapshot was taken. Therefore, there will be an entry with the
// requested id, and its value is the one to return.
// c) More snapshots were created after the requested one, and the queried value was later modified. There will be
// no entry for the requested id: the value that corresponds to it is that of the smallest snapshot id that is
// larger than the requested one.
//
// In summary, we need to find an element in an array, returning the index of the smallest value that is larger if
// it is not found, unless said value doesn't exist (e.g. when all values are smaller). Arrays.findUpperBound does
// exactly this.
uint256 index = snapshots.ids.findUpperBound(snapshotId);
if (index == snapshots.ids.length) {

@ -43,6 +43,8 @@ NOTE: This page is incomplete. We're working to improve it for the next release.
{{ERC20Capped}}
{{ERC20Snapshot}}
== Utilities
{{SafeERC20}}

@ -5,7 +5,7 @@ import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "../../math/SafeMath.sol";
import "../../utils/Address.sol";
import "../../drafts/Counters.sol";
import "../../utils/Counters.sol";
import "../../introspection/ERC165.sol";
/**

@ -1,7 +1,7 @@
pragma solidity ^0.6.0;
import "./ERC721.sol";
import "../../lifecycle/Pausable.sol";
import "../../utils/Pausable.sol";
/**
* @title ERC721 Non-Fungible Pausable token

@ -10,8 +10,14 @@ Miscellaneous contracts containing utility functions, often related to working w
{{Arrays}}
{{Counters}}
{{Strings}}
{{EnumerableSet}}
{{Create2}}
{{ReentrancyGuard}}
{{Pausable}}

@ -6,11 +6,12 @@ pragma solidity ^0.6.0;
*/
library Strings {
/**
* @dev Converts a `uint256` to a `string`.
* via OraclizeAPI - MIT licence
* https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
* @dev Converts a `uint256` to its ASCII `string` representation.
*/
function fromUint256(uint256 value) internal pure returns (string memory) {
// Inspired by OraclizeAPI's implementation - MIT licence
// https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
if (value == 0) {
return "0";
}

@ -0,0 +1,19 @@
= Drafts
All draft contracts were either moved into a different directory or removed from the OpenZeppelin Contracts library on the https://forum.openzeppelin.com/t/openzeppelin-contracts-v3-0-beta-release/2256[v3.0.0 release].
* `ERC20Migrator`: removed.
* xref:api:token/ERC20.adoc#ERC20Snapshot[`ERC20Snapshot`]: moved to `token/ERC20`.
* `ERC20Detailed` and `ERC1046`: removed.
* `TokenVesting`: removed. Pending a replacement that is being discussed in https://github.com/OpenZeppelin/openzeppelin-contracts/issues/1214[`#1214`].
* xref:api:utils.adoc#Counters[`Counters`]: moved to xref:api:utils.adoc[`utils`].
* xref:api:utils.adoc#Strings[`Strings`]: moved to xref:api:utils.adoc[`utils`].
* xref:api:utils.adoc#SignedSafeMath[`SignedSafeMath`]: moved to xref:api:utils.adoc[`utils`].
Removed contracts are still available on the v2.5 release of OpenZeppelin Contracts, which you can install by running:
```console
$ npm install @openzeppelin/contracts@v2.5
```
Refer to the xref:2.x@contracts:api:utils.adoc[v2.x documentation] when working with them.

@ -15,7 +15,7 @@ Here's what a contract for tokenized items might look like:
pragma solidity ^0.5.0;
import "@openzeppelin/contracts/token/ERC721/ERC721Full.sol";
import "@openzeppelin/contracts/drafts/Counters.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract GameItem is ERC721Full {
using Counters for Counters.Counter;

@ -1,26 +0,0 @@
const { contract } = require('@openzeppelin/test-environment');
require('@openzeppelin/test-helpers');
const ERC20MetadataMock = contract.fromArtifact('ERC20MetadataMock');
const { expect } = require('chai');
const metadataURI = 'https://example.com';
describe('ERC20Metadata', function () {
beforeEach(async function () {
this.token = await ERC20MetadataMock.new(metadataURI);
});
it('responds with the metadata', async function () {
expect(await this.token.tokenURI()).to.equal(metadataURI);
});
describe('setTokenURI', function () {
it('changes the original URI', async function () {
const newMetadataURI = 'https://betterexample.com';
await this.token.setTokenURI(newMetadataURI);
expect(await this.token.tokenURI()).to.equal(newMetadataURI);
});
});
});

@ -1,172 +0,0 @@
const { accounts, contract } = require('@openzeppelin/test-environment');
const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
const { ZERO_ADDRESS } = constants;
const { expect } = require('chai');
const ERC20Mock = contract.fromArtifact('ERC20Mock');
const TokenVesting = contract.fromArtifact('TokenVesting');
describe('TokenVesting', function () {
const [ owner, beneficiary ] = accounts;
const amount = new BN('1000');
beforeEach(async function () {
// +1 minute so it starts after contract instantiation
this.start = (await time.latest()).add(time.duration.minutes(1));
this.cliffDuration = time.duration.years(1);
this.duration = time.duration.years(2);
});
it('reverts with a duration shorter than the cliff', async function () {
const cliffDuration = this.duration;
const duration = this.cliffDuration;
expect(cliffDuration).to.be.bignumber.that.is.at.least(duration);
await expectRevert(
TokenVesting.new(beneficiary, this.start, cliffDuration, duration, true, { from: owner }),
'TokenVesting: cliff is longer than duration'
);
});
it('reverts with a null beneficiary', async function () {
await expectRevert(
TokenVesting.new(ZERO_ADDRESS, this.start, this.cliffDuration, this.duration, true, { from: owner }),
'TokenVesting: beneficiary is the zero address'
);
});
it('reverts with a null duration', async function () {
// cliffDuration should also be 0, since the duration must be larger than the cliff
await expectRevert(
TokenVesting.new(beneficiary, this.start, 0, 0, true, { from: owner }), 'TokenVesting: duration is 0'
);
});
it('reverts if the end time is in the past', async function () {
const now = await time.latest();
this.start = now.sub(this.duration).sub(time.duration.minutes(1));
await expectRevert(
TokenVesting.new(beneficiary, this.start, this.cliffDuration, this.duration, true, { from: owner }),
'TokenVesting: final time is before current time'
);
});
context('once deployed', function () {
beforeEach(async function () {
this.vesting = await TokenVesting.new(
beneficiary, this.start, this.cliffDuration, this.duration, true, { from: owner });
this.token = await ERC20Mock.new(this.vesting.address, amount);
});
it('can get state', async function () {
expect(await this.vesting.beneficiary()).to.equal(beneficiary);
expect(await this.vesting.cliff()).to.be.bignumber.equal(this.start.add(this.cliffDuration));
expect(await this.vesting.start()).to.be.bignumber.equal(this.start);
expect(await this.vesting.duration()).to.be.bignumber.equal(this.duration);
expect(await this.vesting.revocable()).to.be.equal(true);
});
it('cannot be released before cliff', async function () {
await expectRevert(this.vesting.release(this.token.address),
'TokenVesting: no tokens are due'
);
});
it('can be released after cliff', async function () {
await time.increaseTo(this.start.add(this.cliffDuration).add(time.duration.weeks(1)));
const { logs } = await this.vesting.release(this.token.address);
expectEvent.inLogs(logs, 'TokensReleased', {
token: this.token.address,
amount: await this.token.balanceOf(beneficiary),
});
});
it('should release proper amount after cliff', async function () {
await time.increaseTo(this.start.add(this.cliffDuration));
await this.vesting.release(this.token.address);
const releaseTime = await time.latest();
const releasedAmount = amount.mul(releaseTime.sub(this.start)).div(this.duration);
expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(releasedAmount);
expect(await this.vesting.released(this.token.address)).to.be.bignumber.equal(releasedAmount);
});
it('should linearly release tokens during vesting period', async function () {
const vestingPeriod = this.duration.sub(this.cliffDuration);
const checkpoints = 4;
for (let i = 1; i <= checkpoints; i++) {
const now = this.start.add(this.cliffDuration).add((vestingPeriod.muln(i).divn(checkpoints)));
await time.increaseTo(now);
await this.vesting.release(this.token.address);
const expectedVesting = amount.mul(now.sub(this.start)).div(this.duration);
expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(expectedVesting);
expect(await this.vesting.released(this.token.address)).to.be.bignumber.equal(expectedVesting);
}
});
it('should have released all after end', async function () {
await time.increaseTo(this.start.add(this.duration));
await this.vesting.release(this.token.address);
expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount);
expect(await this.vesting.released(this.token.address)).to.be.bignumber.equal(amount);
});
it('should be revoked by owner if revocable is set', async function () {
const { logs } = await this.vesting.revoke(this.token.address, { from: owner });
expectEvent.inLogs(logs, 'TokenVestingRevoked', { token: this.token.address });
expect(await this.vesting.revoked(this.token.address)).to.equal(true);
});
it('should fail to be revoked by owner if revocable not set', async function () {
const vesting = await TokenVesting.new(
beneficiary, this.start, this.cliffDuration, this.duration, false, { from: owner }
);
await expectRevert(vesting.revoke(this.token.address, { from: owner }),
'TokenVesting: cannot revoke'
);
});
it('should return the non-vested tokens when revoked by owner', async function () {
await time.increaseTo(this.start.add(this.cliffDuration).add(time.duration.weeks(12)));
const vested = vestedAmount(amount, await time.latest(), this.start, this.cliffDuration, this.duration);
await this.vesting.revoke(this.token.address, { from: owner });
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(amount.sub(vested));
});
it('should keep the vested tokens when revoked by owner', async function () {
await time.increaseTo(this.start.add(this.cliffDuration).add(time.duration.weeks(12)));
const vestedPre = vestedAmount(amount, await time.latest(), this.start, this.cliffDuration, this.duration);
await this.vesting.revoke(this.token.address, { from: owner });
const vestedPost = vestedAmount(amount, await time.latest(), this.start, this.cliffDuration, this.duration);
expect(vestedPre).to.be.bignumber.equal(vestedPost);
});
it('should fail to be revoked a second time', async function () {
await this.vesting.revoke(this.token.address, { from: owner });
await expectRevert(this.vesting.revoke(this.token.address, { from: owner }),
'TokenVesting: token already revoked'
);
});
function vestedAmount (total, now, start, cliffDuration, duration) {
return (now.lt(start.add(cliffDuration))) ? new BN(0) : total.mul((now.sub(start))).div(duration);
}
});
});
Loading…
Cancel
Save