Skip to content

Commit 14634d0

Browse files
committed
fixed #178
1 parent 1f43051 commit 14634d0

File tree

5 files changed

+754
-3
lines changed

5 files changed

+754
-3
lines changed

contracts/UFragments.sol

+79
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,21 @@ 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+
string public constant EIP712_REVISION = "1";
82+
bytes32 public 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+
91+
// EIP-2612: keeps track of number of permits per address
92+
mapping(address => uint256) private _nonces;
93+
7994
/**
8095
* @param monetaryPolicy_ The address of the monetary policy contract to use for authentication.
8196
*/
@@ -174,6 +189,35 @@ contract UFragments is ERC20Detailed, Ownable {
174189
return TOTAL_GONS;
175190
}
176191

192+
/**
193+
* @return The number of successful permits by the specified address.
194+
*/
195+
function nonces(address who) public view returns (uint256) {
196+
return _nonces[who];
197+
}
198+
199+
/**
200+
* @return The computed DOMAIN_SEPARATOR to be used off-chain services
201+
* which implement EIP-712.
202+
* https://eips.ethereum.org/EIPS/eip-2612
203+
*/
204+
function DOMAIN_SEPARATOR() public view returns (bytes32) {
205+
uint256 chainId;
206+
assembly {
207+
chainId := chainid()
208+
}
209+
return
210+
keccak256(
211+
abi.encode(
212+
EIP712_DOMAIN,
213+
keccak256(bytes(name())),
214+
keccak256(bytes(EIP712_REVISION)),
215+
chainId,
216+
address(this)
217+
)
218+
);
219+
}
220+
177221
/**
178222
* @dev Transfer tokens to a specified address.
179223
* @param to The address to transfer to.
@@ -326,4 +370,39 @@ contract UFragments is ERC20Detailed, Ownable {
326370

327371
return true;
328372
}
373+
374+
/**
375+
* @dev Allows for approvals to be made via secp256k1 signatures.
376+
* @param owner The owner of the funds
377+
* @param spender The spender
378+
* @param value The amount
379+
* @param deadline The deadline timestamp, type(uint256).max for max deadline
380+
* @param v Signature param
381+
* @param s Signature param
382+
* @param r Signature param
383+
*/
384+
function permit(
385+
address owner,
386+
address spender,
387+
uint256 value,
388+
uint256 deadline,
389+
uint8 v,
390+
bytes32 r,
391+
bytes32 s
392+
) public {
393+
require(block.timestamp <= deadline);
394+
395+
uint256 ownerNonce = _nonces[owner];
396+
bytes32 permitDataDigest =
397+
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, ownerNonce, deadline));
398+
bytes32 digest =
399+
keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), permitDataDigest));
400+
401+
require(owner == ecrecover(digest, v, r, s));
402+
403+
_nonces[owner] = ownerNonce.add(1);
404+
405+
_allowedFragments[owner][spender] = value;
406+
emit Approval(owner, spender, value);
407+
}
329408
}

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"bignumber.js": "^9.0.0",
4040
"chai": "^4.2.0",
4141
"ethereum-waffle": "^3.2.1",
42-
"ethers": "^5.0.24",
42+
"ethereumjs-util": "^7.0.7",
43+
"ethers": "5.0.18",
4344
"hardhat": "^2.0.6",
4445
"pre-commit": "^1.2.2",
4546
"prettier": "^2.1.1",

test/unit/UFragments_erc20_permit.ts

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { network, ethers, upgrades } from 'hardhat'
2+
import { Contract, Signer, Wallet, BigNumber } from 'ethers'
3+
import { expect } from 'chai'
4+
5+
import {
6+
EIP712_DOMAIN_TYPEHASH,
7+
EIP2612_PERMIT_TYPEHASH,
8+
getDomainSeparator,
9+
signEIP712Permission,
10+
} from '../utils/signatures'
11+
12+
let accounts: Signer[],
13+
deployer: Signer,
14+
deployerAddress: string,
15+
owner: Wallet,
16+
ownerAddress: string,
17+
spender: Wallet,
18+
spenderAddress: string,
19+
uFragments: Contract,
20+
initialSupply: BigNumber
21+
22+
async function setupContracts() {
23+
// prepare signers
24+
accounts = await ethers.getSigners()
25+
deployer = accounts[0]
26+
deployerAddress = await deployer.getAddress()
27+
28+
owner = Wallet.createRandom()
29+
ownerAddress = await owner.getAddress()
30+
31+
spender = Wallet.createRandom()
32+
spenderAddress = await spender.getAddress()
33+
34+
// deploy upgradable token
35+
const factory = await ethers.getContractFactory('UFragments')
36+
uFragments = await upgrades.deployProxy(factory, [deployerAddress], {
37+
initializer: 'initialize(address)',
38+
})
39+
// fetch initial supply
40+
initialSupply = await uFragments.totalSupply()
41+
}
42+
43+
// https://eips.ethereum.org/EIPS/eip-2612
44+
// Test cases as in:
45+
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/drafts/ERC20Permit.test.js
46+
describe('UFragments:Initialization', () => {
47+
before('setup UFragments contract', setupContracts)
48+
49+
it('should set the EIP2612 parameters', async function () {
50+
expect(await uFragments.EIP712_REVISION()).to.eq('1')
51+
expect(await uFragments.EIP712_DOMAIN()).to.eq(EIP712_DOMAIN_TYPEHASH)
52+
expect(await uFragments.PERMIT_TYPEHASH()).to.eq(EIP2612_PERMIT_TYPEHASH)
53+
// with hard-coded parameters
54+
expect(await uFragments.DOMAIN_SEPARATOR()).to.eq(
55+
getDomainSeparator(
56+
await uFragments.EIP712_REVISION(),
57+
await uFragments.name(),
58+
uFragments.address,
59+
network.config.chainId || 1,
60+
),
61+
)
62+
})
63+
64+
it('initial nonce is 0', async function () {
65+
expect(await uFragments.nonces(deployerAddress)).to.eq('0')
66+
expect(await uFragments.nonces(ownerAddress)).to.eq('0')
67+
expect(await uFragments.nonces(spenderAddress)).to.eq('0')
68+
})
69+
})
70+
71+
// Using the cases specified by:
72+
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/drafts/ERC20Permit.test.js
73+
describe('UFragments:EIP-2612 Permit', () => {
74+
const MAX_DEADLINE = BigNumber.from(2).pow(256).sub(1)
75+
76+
beforeEach('setup UFragments contract', setupContracts)
77+
78+
describe('permit', function () {
79+
const signPermission = async (
80+
signer: Wallet,
81+
owner: string,
82+
spender: string,
83+
value: number,
84+
nonce: number,
85+
deadline: BigNumber,
86+
) => {
87+
return signEIP712Permission(
88+
await uFragments.EIP712_REVISION(),
89+
await uFragments.name(),
90+
uFragments.address,
91+
network.config.chainId || 1,
92+
signer,
93+
owner,
94+
spender,
95+
value,
96+
nonce,
97+
deadline,
98+
)
99+
}
100+
101+
it('accepts owner signature', async function () {
102+
const { v, r, s } = await signPermission(
103+
owner,
104+
ownerAddress,
105+
spenderAddress,
106+
123,
107+
0,
108+
MAX_DEADLINE,
109+
)
110+
await expect(
111+
uFragments
112+
.connect(deployer)
113+
.permit(ownerAddress, spenderAddress, 123, MAX_DEADLINE, v, r, s),
114+
)
115+
.to.emit(uFragments, 'Approval')
116+
.withArgs(ownerAddress, spenderAddress, '123')
117+
expect(await uFragments.nonces(ownerAddress)).to.eq('1')
118+
expect(await uFragments.allowance(ownerAddress, spenderAddress)).to.eq(
119+
'123',
120+
)
121+
})
122+
123+
it('rejects reused signature', async function () {
124+
const { v, r, s } = await signPermission(
125+
owner,
126+
ownerAddress,
127+
spenderAddress,
128+
123,
129+
0,
130+
MAX_DEADLINE,
131+
)
132+
await uFragments
133+
.connect(deployer)
134+
.permit(ownerAddress, spenderAddress, 123, MAX_DEADLINE, v, r, s)
135+
await expect(
136+
uFragments
137+
.connect(deployer)
138+
.permit(ownerAddress, spenderAddress, 123, MAX_DEADLINE, v, r, s),
139+
).to.be.reverted
140+
})
141+
142+
it('rejects other signature', async function () {
143+
const { v, r, s } = await signPermission(
144+
spender,
145+
ownerAddress,
146+
spenderAddress,
147+
123,
148+
0,
149+
MAX_DEADLINE,
150+
)
151+
await expect(
152+
uFragments
153+
.connect(deployer)
154+
.permit(ownerAddress, spenderAddress, 123, MAX_DEADLINE, v, r, s),
155+
).to.be.reverted
156+
})
157+
158+
it('rejects expired permit', async function () {
159+
const currentTs = (await ethers.provider.getBlock('latest')).timestamp
160+
const olderTs = currentTs - 3600 * 24 * 7
161+
const deadline = BigNumber.from(olderTs)
162+
const { v, r, s } = await signPermission(
163+
owner,
164+
ownerAddress,
165+
spenderAddress,
166+
123,
167+
0,
168+
deadline,
169+
)
170+
await expect(
171+
uFragments
172+
.connect(deployer)
173+
.permit(ownerAddress, spenderAddress, 123, deadline, v, r, s),
174+
).to.be.reverted
175+
})
176+
})
177+
})

0 commit comments

Comments
 (0)