Skip to content

Permit [eip-2612] support for AMPL #181

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions contracts/UFragments.sol
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ contract UFragments is ERC20Detailed, Ownable {
// it's fully paid.
mapping(address => mapping(address => uint256)) private _allowedFragments;

// EIP-2612: permit – 712-signed approvals
// https://eips.ethereum.org/EIPS/eip-2612
string public constant EIP712_REVISION = "1";
bytes32 public constant EIP712_DOMAIN =
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 public constant PERMIT_TYPEHASH =
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);

// EIP-2612: keeps track of number of permits per address
mapping(address => uint256) private _nonces;

/**
* @param monetaryPolicy_ The address of the monetary policy contract to use for authentication.
*/
Expand Down Expand Up @@ -174,6 +189,35 @@ contract UFragments is ERC20Detailed, Ownable {
return TOTAL_GONS;
}

/**
* @return The number of successful permits by the specified address.
*/
function nonces(address who) public view returns (uint256) {
return _nonces[who];
}

/**
* @return The computed DOMAIN_SEPARATOR to be used off-chain services
* which implement EIP-712.
* https://eips.ethereum.org/EIPS/eip-2612
*/
function DOMAIN_SEPARATOR() public view returns (bytes32) {
uint256 chainId;
assembly {
chainId := chainid()
}
return
keccak256(
abi.encode(
EIP712_DOMAIN,
keccak256(bytes(name())),
keccak256(bytes(EIP712_REVISION)),
chainId,
address(this)
)
);
}

/**
* @dev Transfer tokens to a specified address.
* @param to The address to transfer to.
Expand Down Expand Up @@ -326,4 +370,39 @@ contract UFragments is ERC20Detailed, Ownable {

return true;
}

/**
* @dev Allows for approvals to be made via secp256k1 signatures.
* @param owner The owner of the funds
* @param spender The spender
* @param value The amount
* @param deadline The deadline timestamp, type(uint256).max for max deadline
* @param v Signature param
* @param s Signature param
* @param r Signature param
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public {
require(block.timestamp <= deadline);

uint256 ownerNonce = _nonces[owner];
bytes32 permitDataDigest =
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, ownerNonce, deadline));
bytes32 digest =
keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), permitDataDigest));

require(owner == ecrecover(digest, v, r, s));

_nonces[owner] = ownerNonce.add(1);

_allowedFragments[owner][spender] = value;
emit Approval(owner, spender, value);
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"bignumber.js": "^9.0.0",
"chai": "^4.2.0",
"ethereum-waffle": "^3.2.1",
"ethers": "^5.0.24",
"ethereumjs-util": "^7.0.7",
"ethers": "5.0.18",
"hardhat": "^2.0.6",
"pre-commit": "^1.2.2",
"prettier": "^2.1.1",
Expand Down
177 changes: 177 additions & 0 deletions test/unit/UFragments_erc20_permit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { network, ethers, upgrades } from 'hardhat'
import { Contract, Signer, Wallet, BigNumber } from 'ethers'
import { expect } from 'chai'

import {
EIP712_DOMAIN_TYPEHASH,
EIP2612_PERMIT_TYPEHASH,
getDomainSeparator,
signEIP712Permission,
} from '../utils/signatures'

let accounts: Signer[],
deployer: Signer,
deployerAddress: string,
owner: Wallet,
ownerAddress: string,
spender: Wallet,
spenderAddress: string,
uFragments: Contract,
initialSupply: BigNumber

async function setupContracts() {
// prepare signers
accounts = await ethers.getSigners()
deployer = accounts[0]
deployerAddress = await deployer.getAddress()

owner = Wallet.createRandom()
ownerAddress = await owner.getAddress()

spender = Wallet.createRandom()
spenderAddress = await spender.getAddress()

// deploy upgradable token
const factory = await ethers.getContractFactory('UFragments')
uFragments = await upgrades.deployProxy(factory, [deployerAddress], {
initializer: 'initialize(address)',
})
// fetch initial supply
initialSupply = await uFragments.totalSupply()
}

// https://eips.ethereum.org/EIPS/eip-2612
// Test cases as in:
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/drafts/ERC20Permit.test.js
describe('UFragments:Initialization', () => {
before('setup UFragments contract', setupContracts)

it('should set the EIP2612 parameters', async function () {
expect(await uFragments.EIP712_REVISION()).to.eq('1')
expect(await uFragments.EIP712_DOMAIN()).to.eq(EIP712_DOMAIN_TYPEHASH)
expect(await uFragments.PERMIT_TYPEHASH()).to.eq(EIP2612_PERMIT_TYPEHASH)
// with hard-coded parameters
expect(await uFragments.DOMAIN_SEPARATOR()).to.eq(
getDomainSeparator(
await uFragments.EIP712_REVISION(),
await uFragments.name(),
uFragments.address,
network.config.chainId || 1,
),
)
})

it('initial nonce is 0', async function () {
expect(await uFragments.nonces(deployerAddress)).to.eq('0')
expect(await uFragments.nonces(ownerAddress)).to.eq('0')
expect(await uFragments.nonces(spenderAddress)).to.eq('0')
})
})

// Using the cases specified by:
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/drafts/ERC20Permit.test.js
describe('UFragments:EIP-2612 Permit', () => {
const MAX_DEADLINE = BigNumber.from(2).pow(256).sub(1)

beforeEach('setup UFragments contract', setupContracts)

describe('permit', function () {
const signPermission = async (
signer: Wallet,
owner: string,
spender: string,
value: number,
nonce: number,
deadline: BigNumber,
) => {
return signEIP712Permission(
await uFragments.EIP712_REVISION(),
await uFragments.name(),
uFragments.address,
network.config.chainId || 1,
signer,
owner,
spender,
value,
nonce,
deadline,
)
}

it('accepts owner signature', async function () {
const { v, r, s } = await signPermission(
owner,
ownerAddress,
spenderAddress,
123,
0,
MAX_DEADLINE,
)
await expect(
uFragments
.connect(deployer)
.permit(ownerAddress, spenderAddress, 123, MAX_DEADLINE, v, r, s),
)
.to.emit(uFragments, 'Approval')
.withArgs(ownerAddress, spenderAddress, '123')
expect(await uFragments.nonces(ownerAddress)).to.eq('1')
expect(await uFragments.allowance(ownerAddress, spenderAddress)).to.eq(
'123',
)
})

it('rejects reused signature', async function () {
const { v, r, s } = await signPermission(
owner,
ownerAddress,
spenderAddress,
123,
0,
MAX_DEADLINE,
)
await uFragments
.connect(deployer)
.permit(ownerAddress, spenderAddress, 123, MAX_DEADLINE, v, r, s)
await expect(
uFragments
.connect(deployer)
.permit(ownerAddress, spenderAddress, 123, MAX_DEADLINE, v, r, s),
).to.be.reverted
})

it('rejects other signature', async function () {
const { v, r, s } = await signPermission(
spender,
ownerAddress,
spenderAddress,
123,
0,
MAX_DEADLINE,
)
await expect(
uFragments
.connect(deployer)
.permit(ownerAddress, spenderAddress, 123, MAX_DEADLINE, v, r, s),
).to.be.reverted
})

it('rejects expired permit', async function () {
const currentTs = (await ethers.provider.getBlock('latest')).timestamp
const olderTs = currentTs - 3600 * 24 * 7
const deadline = BigNumber.from(olderTs)
const { v, r, s } = await signPermission(
owner,
ownerAddress,
spenderAddress,
123,
0,
deadline,
)
await expect(
uFragments
.connect(deployer)
.permit(ownerAddress, spenderAddress, 123, deadline, v, r, s),
).to.be.reverted
})
})
})
Loading