Skip to content

Commit ccfd317

Browse files
committed
fixed #178
1 parent 3442040 commit ccfd317

File tree

5 files changed

+714
-3
lines changed

5 files changed

+714
-3
lines changed

contracts/UFragments.sol

+71
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 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+
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,39 @@ 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+
require(block.timestamp <= deadline);
315+
316+
uint256 ownerNonce = _nonces[owner];
317+
bytes32 permitDataDigest =
318+
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, ownerNonce, deadline));
319+
bytes32 digest =
320+
keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, permitDataDigest));
321+
322+
require(owner == ecrecover(digest, v, r, s));
323+
324+
_nonces[owner] = ownerNonce.add(1);
325+
326+
_allowedFragments[owner][spender] = value;
327+
emit Approval(owner, spender, value);
328+
}
258329
}

package.json

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

test/unit/UFragments_erc20_permit.ts

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

test/utils/signatures.ts

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// https://github.com/albertocuestacanada/ERC20Permit/blob/master/utils/signatures.ts
2+
import {
3+
keccak256,
4+
defaultAbiCoder,
5+
toUtf8Bytes,
6+
solidityPack,
7+
} from 'ethers/lib/utils'
8+
import { ecsign } from 'ethereumjs-util'
9+
import { BigNumberish } from 'ethers'
10+
11+
export const sign = (digest: any, privateKey: any) => {
12+
return ecsign(
13+
Buffer.from(digest.slice(2), 'hex'),
14+
Buffer.from(privateKey.slice(2), 'hex'),
15+
)
16+
}
17+
18+
export const PERMIT_TYPEHASH = keccak256(
19+
toUtf8Bytes(
20+
'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)',
21+
),
22+
)
23+
24+
export const EIP712_DOMAIN_TYPEHASH = keccak256(
25+
toUtf8Bytes(
26+
'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)',
27+
),
28+
)
29+
30+
// Returns the EIP712 hash which should be signed by the user
31+
// in order to make a call to `permit`
32+
export function getPermitDigest(
33+
versionNumber: string,
34+
name: string,
35+
address: string,
36+
chainId: number,
37+
owner: string,
38+
spender: string,
39+
value: number,
40+
nonce: number,
41+
deadline: BigNumberish,
42+
) {
43+
const DOMAIN_SEPARATOR = getDomainSeparator(
44+
versionNumber,
45+
name,
46+
address,
47+
chainId,
48+
)
49+
const permitHash = keccak256(
50+
defaultAbiCoder.encode(
51+
['bytes32', 'address', 'address', 'uint256', 'uint256', 'uint256'],
52+
[PERMIT_TYPEHASH, owner, spender, value, nonce, deadline],
53+
),
54+
)
55+
const hash = keccak256(
56+
solidityPack(
57+
['bytes1', 'bytes1', 'bytes32', 'bytes32'],
58+
['0x19', '0x01', DOMAIN_SEPARATOR, permitHash],
59+
),
60+
)
61+
return hash
62+
}
63+
64+
// Gets the EIP712 domain separator
65+
export function getDomainSeparator(
66+
versionNumber: string,
67+
name: string,
68+
contractAddress: string,
69+
chainId: number,
70+
) {
71+
return keccak256(
72+
defaultAbiCoder.encode(
73+
['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'],
74+
[
75+
EIP712_DOMAIN_TYPEHASH,
76+
keccak256(toUtf8Bytes(name)),
77+
keccak256(toUtf8Bytes(versionNumber)),
78+
chainId,
79+
contractAddress,
80+
],
81+
),
82+
)
83+
}

0 commit comments

Comments
 (0)