Skip to content

Commit 04f9a96

Browse files
committed
fixed #178
1 parent 3442040 commit 04f9a96

File tree

4 files changed

+247
-4
lines changed

4 files changed

+247
-4
lines changed

contracts/UFragments.sol

+85
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,34 @@ contract UFragments is ERC20Detailed, Ownable {
7676
// it's fully paid.
7777
mapping(address => mapping(address => uint256)) private _allowedFragments;
7878

79+
// EIP-2612: permit – 712-signed approvals
80+
// https://eips.ethereum.org/EIPS/eip-2612
81+
bytes public constant EIP712_REVISION = "1";
82+
bytes32 internal constant EIP712_DOMAIN =
83+
keccak256(
84+
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
85+
);
86+
bytes32 public constant PERMIT_TYPEHASH =
87+
keccak256(
88+
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
89+
);
90+
bytes32 public constant DOMAIN_SEPARATOR =
91+
keccak256(
92+
abi.encode(
93+
EIP712_DOMAIN,
94+
// hardcoding token name
95+
keccak256("Ampleforth"),
96+
keccak256(EIP712_REVISION),
97+
// hardcoding Ethereum chainId
98+
uint256(1),
99+
// hardcoding Ampleforth ethereum address
100+
address(0xD46bA6D942050d489DBd938a2C909A5d5039A161)
101+
)
102+
);
103+
104+
// EIP-2612: keeps track of number of permits per address
105+
mapping(address => uint256) private _nonces;
106+
79107
/**
80108
* @param monetaryPolicy_ The address of the monetary policy contract to use for authentication.
81109
*/
@@ -155,6 +183,14 @@ contract UFragments is ERC20Detailed, Ownable {
155183
return _gonBalances[who].div(_gonsPerFragment);
156184
}
157185

186+
/**
187+
* @param who The address to query.
188+
* @return The number of successful permits by the specified address.
189+
*/
190+
function nonces(address who) public view returns (uint256) {
191+
return _nonces[who];
192+
}
193+
158194
/**
159195
* @dev Transfer tokens to a specified address.
160196
* @param to The address to transfer to.
@@ -255,4 +291,53 @@ contract UFragments is ERC20Detailed, Ownable {
255291
emit Approval(msg.sender, spender, _allowedFragments[msg.sender][spender]);
256292
return true;
257293
}
294+
295+
/**
296+
* @dev Allows for approvals to be made via secp256k1 signatures.
297+
* @param owner The owner of the funds
298+
* @param spender The spender
299+
* @param value The amount
300+
* @param deadline The deadline timestamp, type(uint256).max for max deadline
301+
* @param v Signature param
302+
* @param s Signature param
303+
* @param r Signature param
304+
*/
305+
function permit(
306+
address owner,
307+
address spender,
308+
uint256 value,
309+
uint256 deadline,
310+
uint8 v,
311+
bytes32 r,
312+
bytes32 s
313+
) public {
314+
//solium-disable-next-line
315+
require(block.timestamp <= deadline);
316+
317+
uint256 currentValidNonce = _nonces[owner];
318+
bytes32 digest =
319+
keccak256(
320+
abi.encodePacked(
321+
"\x19\x01",
322+
DOMAIN_SEPARATOR,
323+
keccak256(
324+
abi.encode(
325+
PERMIT_TYPEHASH,
326+
owner,
327+
spender,
328+
value,
329+
currentValidNonce,
330+
deadline
331+
)
332+
)
333+
)
334+
);
335+
336+
require(owner == ecrecover(digest, v, r, s));
337+
338+
_nonces[owner] = currentValidNonce.add(1);
339+
340+
_allowedFragments[owner][spender] = value;
341+
emit Approval(owner, spender, value);
342+
}
258343
}

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
"bignumber.js": "^9.0.0",
3939
"chai": "^4.2.0",
4040
"ethereum-waffle": "^3.2.1",
41+
"ethereumjs-util": "^7.0.7",
42+
"ethereumjs-wallet": "^1.0.1",
4143
"ethers": "^5.0.24",
4244
"hardhat": "^2.0.6",
4345
"pre-commit": "^1.2.2",

test/unit/UFragments.ts

+145-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,53 @@
11
import { ethers, upgrades } from 'hardhat'
2-
import { Contract, Signer, BigNumber } from 'ethers'
2+
import { Contract, Signer, BigNumber, BigNumberish } from 'ethers'
33
import { expect } from 'chai'
4+
import { fromRpcSig } from 'ethereumjs-util'
5+
import { TypedDataUtils, signTypedMessage, TypedMessage } from 'eth-sig-util'
6+
import { default as Wallet } from 'ethereumjs-wallet'
47

58
const toUFrgDenomination = (ample: string): BigNumber =>
69
ethers.utils.parseUnits(ample, DECIMALS)
710

11+
const ETHEREUM_CHAIN_ID = 1
12+
const ETHEREUM_AMPL_ADDRESS = '0xD46bA6D942050d489DBd938a2C909A5d5039A161'
13+
const EIP712_REVISION = '1'
14+
15+
const TOKEN_NAME = 'Ampleforth'
16+
const TOKEN_SYMBOL = 'AMPL'
817
const DECIMALS = 9
918
const INITIAL_SUPPLY = ethers.utils.parseUnits('50', 6 + DECIMALS)
1019
const transferAmount = toUFrgDenomination('10')
1120
const unitTokenAmount = toUFrgDenomination('1')
1221

22+
const EIP712Domain = [
23+
{ name: 'name', type: 'string' },
24+
{ name: 'version', type: 'string' },
25+
{ name: 'chainId', type: 'uint256' },
26+
{ name: 'verifyingContract', type: 'address' },
27+
]
28+
const Permit = [
29+
{ name: 'owner', type: 'address' },
30+
{ name: 'spender', type: 'address' },
31+
{ name: 'value', type: 'uint256' },
32+
{ name: 'nonce', type: 'uint256' },
33+
{ name: 'deadline', type: 'uint256' },
34+
]
35+
function domainSeparator(
36+
name: string,
37+
version: string,
38+
chainId: number,
39+
verifyingContract: string,
40+
): string {
41+
return (
42+
'0x' +
43+
TypedDataUtils.hashStruct(
44+
'EIP712Domain',
45+
{ name, version, chainId, verifyingContract },
46+
{ EIP712Domain },
47+
).toString('hex')
48+
)
49+
}
50+
1351
let accounts: Signer[],
1452
deployer: Signer,
1553
uFragments: Contract,
@@ -19,6 +57,7 @@ async function setupContracts() {
1957
// prepare signers
2058
accounts = await ethers.getSigners()
2159
deployer = accounts[0]
60+
2261
// deploy upgradable token
2362
const factory = await ethers.getContractFactory('UFragments')
2463
uFragments = await upgrades.deployProxy(
@@ -60,10 +99,113 @@ describe('UFragments:Initialization', () => {
6099
})
61100

62101
it('should set detailed ERC20 parameters', async function () {
63-
expect(await uFragments.name()).to.eq('Ampleforth')
64-
expect(await uFragments.symbol()).to.eq('AMPL')
102+
expect(await uFragments.name()).to.eq(TOKEN_NAME)
103+
expect(await uFragments.symbol()).to.eq(TOKEN_SYMBOL)
65104
expect(await uFragments.decimals()).to.eq(DECIMALS)
66105
})
106+
107+
it('should set the EIP2612 parameters', async function () {
108+
expect(await uFragments.EIP712_REVISION()).to.eq('0x31')
109+
expect(await uFragments.PERMIT_TYPEHASH()).to.eq(
110+
'0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9',
111+
)
112+
// with hard-coded parameters
113+
expect(await uFragments.DOMAIN_SEPARATOR()).to.eq(
114+
domainSeparator(
115+
TOKEN_NAME,
116+
'1',
117+
ETHEREUM_CHAIN_ID,
118+
ETHEREUM_AMPL_ADDRESS,
119+
),
120+
)
121+
})
122+
123+
it('initial nonce is 0', async function () {
124+
expect(await uFragments.nonces(await deployer.getAddress())).to.eq('0')
125+
})
126+
})
127+
128+
// Using the cases specified by:
129+
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/drafts/ERC20Permit.test.js
130+
describe('UFragments:EIP-2612 Permit', () => {
131+
let wallet: Wallet, owner: string, spender: string, nonce: number
132+
133+
const value = 42
134+
const maxDeadline = 99999999999
135+
136+
beforeEach('setup UFragments contract', async function () {
137+
await setupContracts()
138+
139+
wallet = Wallet.generate()
140+
owner = wallet.getAddressString()
141+
spender = Wallet.generate().getAddressString()
142+
nonce = 0
143+
})
144+
145+
describe('permit', function () {
146+
const buildPermitData = (
147+
deadline: number = maxDeadline,
148+
): TypedMessage<any> => {
149+
return {
150+
primaryType: 'Permit',
151+
types: { EIP712Domain, Permit },
152+
domain: {
153+
name: TOKEN_NAME,
154+
version: EIP712_REVISION,
155+
chainId: ETHEREUM_CHAIN_ID,
156+
verifyingContract: ETHEREUM_AMPL_ADDRESS,
157+
},
158+
message: { owner, spender, value, nonce, deadline },
159+
}
160+
}
161+
162+
it('accepts owner signature', async function () {
163+
const data = buildPermitData()
164+
const signature = signTypedMessage(wallet.getPrivateKey(), { data })
165+
const { v, r, s } = fromRpcSig(signature)
166+
const receipt = await uFragments
167+
.connect(deployer)
168+
.permit(owner, spender, value, maxDeadline, v, r, s)
169+
expect(await uFragments.nonces(owner)).to.eq('1')
170+
expect(await uFragments.allowance(owner, spender)).to.eq(value)
171+
})
172+
173+
it('rejects reused signature', async function () {
174+
const data = buildPermitData()
175+
const signature = signTypedMessage(wallet.getPrivateKey(), { data })
176+
const { v, r, s } = fromRpcSig(signature)
177+
await uFragments
178+
.connect(deployer)
179+
.permit(owner, spender, value, maxDeadline, v, r, s)
180+
await expect(
181+
uFragments
182+
.connect(deployer)
183+
.permit(owner, spender, value, maxDeadline, v, r, s),
184+
).to.be.reverted
185+
})
186+
187+
it('rejects other signature', async function () {
188+
const otherWallet = Wallet.generate()
189+
const data = buildPermitData()
190+
const signature = signTypedMessage(otherWallet.getPrivateKey(), { data })
191+
const { v, r, s } = fromRpcSig(signature)
192+
await expect(
193+
uFragments
194+
.connect(deployer)
195+
.permit(owner, spender, value, maxDeadline, v, r, s),
196+
).to.be.reverted
197+
})
198+
199+
it('rejects expired permit', async function () {
200+
const deadline =
201+
(await ethers.provider.getBlock('latest')).timestamp - 3600 * 24 * 7
202+
const data = buildPermitData(deadline)
203+
const signature = signTypedMessage(wallet.getPrivateKey(), { data })
204+
const { v, r, s } = fromRpcSig(signature)
205+
await expect(uFragments.permit(owner, spender, value, deadline, v, r, s))
206+
.to.be.reverted
207+
})
208+
})
67209
})
68210

69211
describe('UFragments:setMonetaryPolicy', async () => {

yarn.lock

+15-1
Original file line numberDiff line numberDiff line change
@@ -3316,7 +3316,7 @@ ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereum
33163316
rlp "^2.0.0"
33173317
safe-buffer "^5.1.1"
33183318

3319-
ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.3:
3319+
ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.3, ethereumjs-util@^7.0.7:
33203320
version "7.0.7"
33213321
resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.0.7.tgz#484fb9c03b766b2ee64821281070616562fb5a59"
33223322
integrity sha512-vU5rtZBlZsgkTw3o6PDKyB8li2EgLavnAbsKcfsH2YhHH1Le+PP8vEiMnAnvgc1B6uMoaM5GDCrVztBw0Q5K9g==
@@ -3381,6 +3381,20 @@ [email protected]:
33813381
utf8 "^3.0.0"
33823382
uuid "^3.3.2"
33833383

3384+
ethereumjs-wallet@^1.0.1:
3385+
version "1.0.1"
3386+
resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-1.0.1.tgz#664a4bcacfc1291ca2703de066df1178938dba1c"
3387+
integrity sha512-3Z5g1hG1das0JWU6cQ9HWWTY2nt9nXCcwj7eXVNAHKbo00XAZO8+NHlwdgXDWrL0SXVQMvTWN8Q/82DRH/JhPw==
3388+
dependencies:
3389+
aes-js "^3.1.1"
3390+
bs58check "^2.1.2"
3391+
ethereum-cryptography "^0.1.3"
3392+
ethereumjs-util "^7.0.2"
3393+
randombytes "^2.0.6"
3394+
scrypt-js "^3.0.1"
3395+
utf8 "^3.0.0"
3396+
uuid "^3.3.2"
3397+
33843398
ethers@^4.0.32:
33853399
version "4.0.48"
33863400
resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.48.tgz#330c65b8133e112b0613156e57e92d9009d8fbbe"

0 commit comments

Comments
 (0)