Skip to content

Commit 517988e

Browse files
committed
Separate the minting from signature into its own AirdropableToken.sol
1 parent b9e3d6f commit 517988e

File tree

5 files changed

+177
-84
lines changed

5 files changed

+177
-84
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
pragma solidity ^0.4.24;
2+
3+
import "./MintableToken.sol";
4+
import "../../ECRecovery.sol";
5+
6+
/**
7+
* @title AirdropableToken
8+
* @author SylTi
9+
* @dev Simple ERC20 Token that allow for an airdrop to be realised based on signatures from the owner.
10+
* This allow the owners of the token to forward the gas cost to the receiver of the airdrop instead of having to pay for it.
11+
*/
12+
13+
14+
contract AirdropableToken is MintableToken {
15+
using ECRecovery for bytes32;
16+
17+
mapping (address => uint256) public usedNonces;
18+
19+
/**
20+
* @dev Function to mint tokens given a valid signature.
21+
* @param _to The address that will receive the minted tokens.
22+
* @param _amount The amount of tokens to mint.
23+
* @param _nonce Unique value used to prevent multiple usage of the same signature
24+
* @param _signature Signature of the hash
25+
* @return A boolean that indicates if the operation was successful.
26+
*/
27+
function claim(
28+
address _to,
29+
uint256 _amount,
30+
uint256 _nonce,
31+
bytes _signature
32+
)
33+
canMint
34+
public
35+
returns (bool)
36+
{
37+
require(_nonce == usedNonces[_to], "Invalid nonce");
38+
bytes32 proof = getProof(_to, _amount, _nonce);
39+
address signer = proof.recover(_signature);
40+
require(signer == owner, "Not signed by owner");
41+
usedNonces[_to] = _nonce.add(1);
42+
return super.doMint(_to, _amount);
43+
}
44+
45+
/**
46+
* @dev Function that create the hash proof used to check against the signature.
47+
* @param _to The address that will receive the minted tokens.
48+
* @param _amount The amount of tokens to mint.
49+
* @param _nonce Unique value used to prevent multiple usage of the same signature
50+
* @return A bytes32 hash of the parameters.
51+
*/
52+
function getProof(
53+
address _to,
54+
uint256 _amount,
55+
uint256 _nonce
56+
) public view returns (bytes32)
57+
{
58+
return keccak256(
59+
abi.encodePacked(
60+
address(this), // check if the signature was for this specific contract
61+
_to,
62+
_amount,
63+
_nonce
64+
)
65+
).toEthSignedMessageHash();
66+
}
67+
}

contracts/token/ERC20/MintableToken.sol

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -46,37 +46,6 @@ contract MintableToken is StandardToken, Ownable {
4646
{
4747
return doMint(_to, _amount);
4848
}
49-
50-
/**
51-
* @dev Function to mint tokens from an off chain signed message. This can be very usefull during airdrop to make the redeemer pay the gas instead of the owner of the contract.
52-
* @param _to The address that will receive the minted tokens.
53-
* @param _amount The amount of tokens to mint.
54-
* @param _nonce Unique value used to allow multiple minting of the same amount to the same address
55-
* @param _signature Signature of the hash
56-
* @return A boolean that indicates if the operation was successful.
57-
*/
58-
function mintWithSignature(
59-
address _to,
60-
uint256 _amount,
61-
uint256 _nonce,
62-
bytes _signature
63-
)
64-
canMint
65-
public
66-
returns (bool)
67-
{
68-
require(usedSignatures[_signature] == false, "Signature already used");
69-
bytes32 proof = keccak256(
70-
address(this),
71-
_to,
72-
_amount,
73-
_nonce
74-
).toEthSignedMessageHash();
75-
address signer = proof.recover(_signature);
76-
require(signer == owner, "Not signed by owner");
77-
usedSignatures[_signature] = true;
78-
return doMint(_to, _amount);
79-
}
8049

8150
/**
8251
* @dev Function to stop minting new tokens.
@@ -98,7 +67,7 @@ contract MintableToken is StandardToken, Ownable {
9867
address _to,
9968
uint256 _amount
10069
)
101-
private
70+
internal
10271
returns (bool)
10372
{
10473
totalSupply_ = totalSupply_.add(_amount);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import assertRevert from '../../helpers/assertRevert';
2+
import { soliditySha3 } from 'web3-utils';
3+
4+
const BigNumber = web3.BigNumber;
5+
6+
require('chai')
7+
.use(require('chai-as-promised'))
8+
.use(require('chai-bignumber')(BigNumber))
9+
.should();
10+
11+
export default function ([owner, anotherAccount, minter]) {
12+
describe('as an Airdopable token', function () {
13+
describe('mint', function () {
14+
const amount = 100;
15+
16+
describe('when the sender has a valid signature', function () {
17+
const from = minter;
18+
19+
describe('when the token minting is not finished', function () {
20+
it('mints the requested amount to the agreed upon address with a signed proof from any address',
21+
async function () {
22+
const signature = web3.eth.sign(owner, soliditySha3(this.token.address, anotherAccount, amount, 0));
23+
await this.token.claim(anotherAccount, amount, 0, signature, { from });
24+
25+
const balance = await this.token.balanceOf(anotherAccount);
26+
assert.equal(amount, balance);
27+
});
28+
29+
it('mints the requested amount from the same signed proof should fail', async function () {
30+
const signature = web3.eth.sign(owner, soliditySha3(this.token.address, anotherAccount, amount, 0));
31+
await this.token.claim(anotherAccount, amount, 0, signature, { anotherAccount });
32+
await assertRevert(this.token.claim(anotherAccount, amount, 0, signature, { anotherAccount }));
33+
await assertRevert(this.token.claim(anotherAccount, amount, 0, signature, { owner }));
34+
await assertRevert(this.token.claim(anotherAccount, amount, 0, signature, { minter }));
35+
});
36+
37+
it('mints the requested amount twice from the different signed proof', async function () {
38+
const signature = web3.eth.sign(owner, soliditySha3(this.token.address, anotherAccount, amount, 0));
39+
const signature2 = web3.eth.sign(owner, soliditySha3(this.token.address, anotherAccount, amount, 1));
40+
41+
await this.token.claim(anotherAccount, amount, 0, signature, { anotherAccount });
42+
await this.token.claim(anotherAccount, amount, 1, signature2, { anotherAccount });
43+
44+
const balance = await this.token.balanceOf(anotherAccount);
45+
assert.equal(amount + amount, balance);
46+
});
47+
48+
it('mints with different parameters than the signed proof should fail', async function () {
49+
const signature = web3.eth.sign(owner, soliditySha3(this.token.address, anotherAccount, amount, 0));
50+
await assertRevert(this.token.claim(anotherAccount, amount + 1, 0, signature,
51+
{ anotherAccount }));
52+
await assertRevert(this.token.claim(anotherAccount, amount, 1, signature, { anotherAccount }));
53+
await assertRevert(this.token.claim(owner, amount, 0, signature, { anotherAccount }));
54+
});
55+
});
56+
57+
describe('when the token minting is finished', function () {
58+
beforeEach(async function () {
59+
await this.token.finishMinting({ from: owner });
60+
});
61+
62+
it('reverts', async function () {
63+
const signature = web3.eth.sign(from, soliditySha3(this.token.address, owner, amount, 0));
64+
await assertRevert(this.token.claim(owner, amount, 0, signature, { from }));
65+
});
66+
});
67+
});
68+
69+
describe('when the sender has not a valid signature', function () {
70+
const from = anotherAccount;
71+
72+
describe('when the token minting is not finished', function () {
73+
it('reverts', async function () {
74+
const signature = web3.eth.sign(from, soliditySha3(this.token.address, owner, amount, 0));
75+
await assertRevert(this.token.claim(owner, amount, 0, signature, { from }));
76+
await assertRevert(this.token.claim(owner, amount, 0, signature, { owner }));
77+
});
78+
});
79+
80+
describe('when the token minting is already finished', function () {
81+
beforeEach(async function () {
82+
await this.token.finishMinting({ from: owner });
83+
});
84+
85+
it('reverts', async function () {
86+
const signature = web3.eth.sign(from, soliditySha3(this.token.address, owner, amount, 0));
87+
await assertRevert(this.token.claim(owner, amount, 0, signature, { from }));
88+
});
89+
});
90+
});
91+
});
92+
});
93+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import shouldBehaveLikeAirdropableToken from './AirdropableToken.behaviour';
2+
import shouldBehaveLikeMintableToken from './MintableToken.behaviour';
3+
4+
const AirdropableToken = artifacts.require('AirdropableToken');
5+
6+
contract('AirdropableToken', function ([owner, anotherAccount]) {
7+
const minter = owner;
8+
9+
beforeEach(async function () {
10+
this.token = await AirdropableToken.new({ from: owner });
11+
// await this.token.addMinter(minter, { from: owner });
12+
});
13+
14+
shouldBehaveLikeAirdropableToken([owner, anotherAccount, minter]);
15+
shouldBehaveLikeMintableToken([owner, anotherAccount, minter]);
16+
});

test/token/ERC20/MintableToken.behaviour.js

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import assertRevert from '../../helpers/assertRevert';
2-
import { soliditySha3 } from 'web3-utils';
32

43
const BigNumber = web3.BigNumber;
54

@@ -103,42 +102,6 @@ export default function ([owner, anotherAccount, minter]) {
103102
assert.equal(balance, amount);
104103
});
105104

106-
it('mints the requested amount to the agreed upon address with a signed proof from any address',
107-
async function () {
108-
const signature = web3.eth.sign(owner, soliditySha3(this.token.address, anotherAccount, amount, 0));
109-
await this.token.mintWithSignature(anotherAccount, amount, 0, signature, { from });
110-
111-
const balance = await this.token.balanceOf(anotherAccount);
112-
assert.equal(amount, balance);
113-
});
114-
115-
it('mints the requested amount from the same signed proof should fail', async function () {
116-
const signature = web3.eth.sign(owner, soliditySha3(this.token.address, anotherAccount, amount, 0));
117-
await this.token.mintWithSignature(anotherAccount, amount, 0, signature, { anotherAccount });
118-
await assertRevert(this.token.mintWithSignature(anotherAccount, amount, 0, signature, { anotherAccount }));
119-
await assertRevert(this.token.mintWithSignature(anotherAccount, amount, 0, signature, { owner }));
120-
await assertRevert(this.token.mintWithSignature(anotherAccount, amount, 0, signature, { minter }));
121-
});
122-
123-
it('mints the requested amount twice from the different signed proof', async function () {
124-
const signature = web3.eth.sign(owner, soliditySha3(this.token.address, anotherAccount, amount, 0));
125-
const signature2 = web3.eth.sign(owner, soliditySha3(this.token.address, anotherAccount, amount, 1));
126-
127-
await this.token.mintWithSignature(anotherAccount, amount, 0, signature, { anotherAccount });
128-
await this.token.mintWithSignature(anotherAccount, amount, 1, signature2, { anotherAccount });
129-
130-
const balance = await this.token.balanceOf(anotherAccount);
131-
assert.equal(amount + amount, balance);
132-
});
133-
134-
it('mints with different parameters than the signed proof should fail', async function () {
135-
const signature = web3.eth.sign(owner, soliditySha3(this.token.address, anotherAccount, amount, 0));
136-
await assertRevert(this.token.mintWithSignature(anotherAccount, amount + 1, 0, signature,
137-
{ anotherAccount }));
138-
await assertRevert(this.token.mintWithSignature(anotherAccount, amount, 1, signature, { anotherAccount }));
139-
await assertRevert(this.token.mintWithSignature(owner, amount, 0, signature, { anotherAccount }));
140-
});
141-
142105
it('emits a mint and a transfer event', async function () {
143106
const { logs } = await this.token.mint(owner, amount, { from });
144107

@@ -158,11 +121,6 @@ export default function ([owner, anotherAccount, minter]) {
158121
it('reverts', async function () {
159122
await assertRevert(this.token.mint(owner, amount, { from }));
160123
});
161-
162-
it('reverts', async function () {
163-
const signature = web3.eth.sign(from, soliditySha3(this.token.address, owner, amount, 0));
164-
await assertRevert(this.token.mintWithSignature(owner, amount, 0, signature, { from }));
165-
});
166124
});
167125
});
168126

@@ -173,11 +131,6 @@ export default function ([owner, anotherAccount, minter]) {
173131
it('reverts', async function () {
174132
await assertRevert(this.token.mint(owner, amount, { from }));
175133
});
176-
it('reverts', async function () {
177-
const signature = web3.eth.sign(from, soliditySha3(this.token.address, owner, amount, 0));
178-
await assertRevert(this.token.mintWithSignature(owner, amount, 0, signature, { from }));
179-
await assertRevert(this.token.mintWithSignature(owner, amount, 0, signature, { owner }));
180-
});
181134
});
182135

183136
describe('when the token minting is already finished', function () {
@@ -188,11 +141,6 @@ export default function ([owner, anotherAccount, minter]) {
188141
it('reverts', async function () {
189142
await assertRevert(this.token.mint(owner, amount, { from }));
190143
});
191-
192-
it('reverts', async function () {
193-
const signature = web3.eth.sign(from, soliditySha3(this.token.address, owner, amount, 0));
194-
await assertRevert(this.token.mintWithSignature(owner, amount, 0, signature, { from }));
195-
});
196144
});
197145
});
198146
});

0 commit comments

Comments
 (0)