- Added permit() to aToken.

This commit is contained in:
eboado 2020-09-14 15:57:11 +02:00
parent f3856bac12
commit 748312cf20
7 changed files with 463 additions and 5 deletions

View File

@ -2,13 +2,13 @@ import {usePlugin, BuidlerConfig} from '@nomiclabs/buidler/config';
// @ts-ignore
import {accounts} from './test-wallets.js';
import {eEthereumNetwork} from './helpers/types';
import { BUIDLEREVM_CHAINID, COVERAGE_CHAINID } from './helpers/constants';
usePlugin('@nomiclabs/buidler-ethers');
usePlugin('buidler-typechain');
usePlugin('solidity-coverage');
usePlugin('@nomiclabs/buidler-waffle');
usePlugin('@nomiclabs/buidler-etherscan');
usePlugin('buidler-gas-reporter');
const DEFAULT_BLOCK_GAS_LIMIT = 10000000;
const DEFAULT_GAS_PRICE = 10;
@ -59,6 +59,7 @@ const config: any = {
networks: {
coverage: {
url: 'http://localhost:8555',
chainId: COVERAGE_CHAINID,
},
kovan: getCommonNetworkConfig(eEthereumNetwork.kovan, 42),
ropsten: getCommonNetworkConfig(eEthereumNetwork.ropsten, 3),
@ -68,7 +69,7 @@ const config: any = {
blockGasLimit: DEFAULT_BLOCK_GAS_LIMIT,
gas: DEFAULT_BLOCK_GAS_LIMIT,
gasPrice: 8000000000,
chainId: 31337,
chainId: BUIDLEREVM_CHAINID,
throwOnTransactionFailures: true,
throwOnCallFailures: true,
accounts: accounts.map(({secretKey, balance}: {secretKey: string; balance: string}) => ({

View File

@ -29,8 +29,15 @@ contract AToken is VersionedInitializable, ERC20, IAToken {
mapping(address => uint256) private _scaledRedirectedBalances;
/// @dev owner => next valid nonce to submit with permit()
mapping (address => uint256) public _nonces;
uint256 public constant ATOKEN_REVISION = 0x1;
bytes32 public DOMAIN_SEPARATOR;
bytes public constant EIP712_REVISION = bytes("1");
bytes32 internal 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)");
modifier onlyLendingPool {
require(msg.sender == address(POOL), Errors.CALLER_MUST_BE_LENDING_POOL);
@ -56,6 +63,21 @@ contract AToken is VersionedInitializable, ERC20, IAToken {
string calldata tokenName,
string calldata tokenSymbol
) external virtual initializer {
uint256 chainId;
//solium-disable-next-line
assembly {
chainId := chainid()
}
DOMAIN_SEPARATOR = keccak256(abi.encode(
EIP712_DOMAIN,
keccak256(bytes(tokenName)),
keccak256(EIP712_REVISION),
chainId,
address(this)
));
_setName(tokenName);
_setSymbol(tokenSymbol);
_setDecimals(underlyingAssetDecimals);
@ -187,6 +209,42 @@ contract AToken is VersionedInitializable, ERC20, IAToken {
return amount;
}
/**
* @dev implements the permit function as for https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md
* @param owner the owner of the funds
* @param spender the spender
* @param value the amount
* @param deadline the deadline timestamp, 0 for no 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
) external {
require(owner != address(0), "INVALID_OWNER");
//solium-disable-next-line
require(block.timestamp <= deadline, "INVALID_EXPIRATION");
uint256 currentValidNonce = _nonces[owner];
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(
abi.encode(PERMIT_TYPEHASH, owner, spender, value, currentValidNonce, deadline))
)
);
require(owner == ecrecover(digest, v, r, s), "INVALID_SIGNATURE");
_nonces[owner] = currentValidNonce.add(1);
_approve(owner, spender, value);
}
function _transfer(
address from,
address to,

View File

@ -8,12 +8,16 @@ import {
IReserveParams,
tEthereumAddress,
iBasicDistributionParams,
eEthereumNetwork,
} from './types';
import BigNumber from 'bignumber.js';
import {getParamPerPool} from './contracts-helpers';
import {getParamPerPool, getParamPerNetwork} from './contracts-helpers';
export const TEST_SNAPSHOT_ID = '0x1';
export const BUIDLEREVM_CHAINID = 31337;
export const COVERAGE_CHAINID = 1337;
// ----------------
// MATH
// ----------------
@ -531,3 +535,18 @@ export const getFeeDistributionParamsCommon = (
percentages,
};
};
export const getATokenDomainSeparatorPerNetwork = (
network: eEthereumNetwork
): tEthereumAddress =>
getParamPerNetwork<tEthereumAddress>(
{
[eEthereumNetwork.coverage]: "0x95b73a72c6ecf4ccbbba5178800023260bad8e75cdccdb8e4827a2977a37c820",
[eEthereumNetwork.buidlerevm]:
"0x76cbbf8aa4b11a7c207dd79ccf8c394f59475301598c9a083f8258b4fafcfa86",
[eEthereumNetwork.kovan]: "",
[eEthereumNetwork.ropsten]: "",
[eEthereumNetwork.main]: "",
},
network
);

View File

@ -32,6 +32,8 @@ import {Ierc20Detailed} from '../types/Ierc20Detailed';
import {StableDebtToken} from '../types/StableDebtToken';
import {VariableDebtToken} from '../types/VariableDebtToken';
import {MockSwapAdapter} from '../types/MockSwapAdapter';
import { signTypedData_v4, TypedData } from "eth-sig-util";
import { fromRpcSig, ECDSASignature } from "ethereumjs-util";
export const registerContractInJsonDb = async (contractId: string, contractInstance: Contract) => {
const currentNetwork = BRE.network.name;
@ -431,10 +433,14 @@ const linkBytecode = (artifact: Artifact, libraries: any) => {
};
export const getParamPerNetwork = <T>(
{kovan, ropsten, main}: iParamsPerNetwork<T>,
{kovan, ropsten, main, buidlerevm, coverage}: iParamsPerNetwork<T>,
network: eEthereumNetwork
) => {
switch (network) {
case eEthereumNetwork.coverage:
return coverage;
case eEthereumNetwork.buidlerevm:
return buidlerevm;
case eEthereumNetwork.kovan:
return kovan;
case eEthereumNetwork.ropsten:
@ -471,3 +477,59 @@ export const convertToCurrencyUnits = async (tokenAddress: string, amount: strin
const amountInCurrencyUnits = new BigNumber(amount).div(currencyUnit);
return amountInCurrencyUnits.toFixed();
};
export const buildPermitParams = (
chainId: number,
token: tEthereumAddress,
revision: string,
tokenName: string,
owner: tEthereumAddress,
spender: tEthereumAddress,
nonce: number,
deadline: string,
value: tStringTokenSmallUnits
) => ({
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
],
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
},
primaryType: "Permit" as const,
domain: {
name: tokenName,
version: revision,
chainId: chainId,
verifyingContract: token,
},
message: {
owner,
spender,
value,
nonce,
deadline,
},
});
export const getSignatureFromTypedData = (
privateKey: string,
typedData: any // TODO: should be TypedData, from eth-sig-utils, but TS doesn't accept it
): ECDSASignature => {
const signature = signTypedData_v4(
Buffer.from(privateKey.substring(2, 66), "hex"),
{
data: typedData,
}
);
return fromRpcSig(signature);
};

View File

@ -5,6 +5,7 @@ export enum eEthereumNetwork {
kovan = 'kovan',
ropsten = 'ropsten',
main = 'main',
coverage = 'coverage'
}
export enum AavePools {
@ -251,6 +252,8 @@ export interface IMarketRates {
}
export interface iParamsPerNetwork<T> {
[eEthereumNetwork.coverage]: T;
[eEthereumNetwork.buidlerevm]: T;
[eEthereumNetwork.kovan]: T;
[eEthereumNetwork.ropsten]: T;
[eEthereumNetwork.main]: T;

View File

@ -16,6 +16,7 @@
"test-repay-with-collateral": "buidler test test/__setup.spec.ts test/repay-with-collateral.spec.ts",
"test-liquidate-with-collateral": "buidler test test/__setup.spec.ts test/flash-liquidation-with-collateral.spec.ts",
"test-flash": "buidler test test/__setup.spec.ts test/flashloan.spec.ts",
"test-permit": "buidler test test/__setup.spec.ts test/atoken-permit.spec.ts",
"dev:coverage": "buidler coverage --network coverage",
"dev:deployment": "buidler dev-deployment",
"dev:deployExample": "buidler deploy-Example",
@ -58,7 +59,9 @@
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.3.0",
"typechain": "2.0.0",
"typescript": "3.9.3"
"typescript": "3.9.3",
"eth-sig-util": "2.5.3",
"ethereumjs-util": "7.0.2"
},
"husky": {
"hooks": {

312
test/atoken-permit.spec.ts Normal file
View File

@ -0,0 +1,312 @@
import {
MAX_UINT_AMOUNT,
ZERO_ADDRESS,
getATokenDomainSeparatorPerNetwork,
BUIDLEREVM_CHAINID,
} from '../helpers/constants';
import {buildPermitParams, getSignatureFromTypedData} from '../helpers/contracts-helpers';
import {expect} from 'chai';
import {ethers} from 'ethers';
import {eEthereumNetwork} from '../helpers/types';
import {makeSuite, TestEnv} from './helpers/make-suite';
import {BRE} from '../helpers/misc-utils';
import {waitForTx} from './__setup.spec';
const {parseEther} = ethers.utils;
makeSuite('AToken: Permit', (testEnv: TestEnv) => {
it('Checks the domain separator', async () => {
const DOMAIN_SEPARATOR_ENCODED = getATokenDomainSeparatorPerNetwork(
eEthereumNetwork.buidlerevm
);
const {aDai} = testEnv;
const separator = await aDai.DOMAIN_SEPARATOR();
expect(separator).to.be.equal(DOMAIN_SEPARATOR_ENCODED, 'Invalid domain separator');
});
it('Get aDAI for tests', async () => {
const {dai, deployer, pool} = testEnv;
await dai.mint(parseEther('20000'));
await dai.approve(pool.address, parseEther('20000'));
await pool.deposit(dai.address, parseEther('20000'), deployer.address, 0);
});
it('Reverts submitting a permit with 0 expiration', async () => {
const {aDai, deployer, users} = testEnv;
const owner = deployer;
const spender = users[1];
const tokenName = await aDai.name();
const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID;
const expiration = 0;
const nonce = (await aDai._nonces(owner.address)).toNumber();
const permitAmount = ethers.utils.parseEther('2').toString();
const msgParams = buildPermitParams(
chainId,
aDai.address,
'1',
tokenName,
owner.address,
spender.address,
nonce,
permitAmount,
expiration.toFixed()
);
const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey;
if (!ownerPrivateKey) {
throw new Error('INVALID_OWNER_PK');
}
expect((await aDai.allowance(owner.address, spender.address)).toString()).to.be.equal(
'0',
'INVALID_ALLOWANCE_BEFORE_PERMIT'
);
const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams);
await expect(
aDai
.connect(spender.signer)
.permit(owner.address, spender.address, permitAmount, expiration, v, r, s)
).to.be.revertedWith('INVALID_EXPIRATION');
expect((await aDai.allowance(owner.address, spender.address)).toString()).to.be.equal(
'0',
'INVALID_ALLOWANCE_AFTER_PERMIT'
);
});
it('Submits a permit with maximum expiration length', async () => {
const {aDai, deployer, users} = testEnv;
const owner = deployer;
const spender = users[1];
const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID;
const deadline = MAX_UINT_AMOUNT;
const nonce = (await aDai._nonces(owner.address)).toNumber();
const permitAmount = parseEther('2').toString();
const msgParams = buildPermitParams(
chainId,
aDai.address,
'1',
await aDai.name(),
owner.address,
spender.address,
nonce,
deadline,
permitAmount
);
const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey;
if (!ownerPrivateKey) {
throw new Error('INVALID_OWNER_PK');
}
expect((await aDai.allowance(owner.address, spender.address)).toString()).to.be.equal(
'0',
'INVALID_ALLOWANCE_BEFORE_PERMIT'
);
const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams);
await waitForTx(
await aDai
.connect(spender.signer)
.permit(owner.address, spender.address, permitAmount, deadline, v, r, s)
);
expect((await aDai._nonces(owner.address)).toNumber()).to.be.equal(1);
});
it('Cancels the previous permit', async () => {
const {aDai, deployer, users} = testEnv;
const owner = deployer;
const spender = users[1];
const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID;
const deadline = MAX_UINT_AMOUNT;
const nonce = (await aDai._nonces(owner.address)).toNumber();
const permitAmount = '0';
const msgParams = buildPermitParams(
chainId,
aDai.address,
'1',
await aDai.name(),
owner.address,
spender.address,
nonce,
deadline,
permitAmount
);
const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey;
if (!ownerPrivateKey) {
throw new Error('INVALID_OWNER_PK');
}
const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams);
expect((await aDai.allowance(owner.address, spender.address)).toString()).to.be.equal(
ethers.utils.parseEther('2'),
'INVALID_ALLOWANCE_BEFORE_PERMIT'
);
await waitForTx(
await aDai
.connect(spender.signer)
.permit(owner.address, spender.address, permitAmount, deadline, v, r, s)
);
expect((await aDai.allowance(owner.address, spender.address)).toString()).to.be.equal(
permitAmount,
'INVALID_ALLOWANCE_AFTER_PERMIT'
);
expect((await aDai._nonces(owner.address)).toNumber()).to.be.equal(2);
});
it('Tries to submit a permit with invalid nonce', async () => {
const {aDai, deployer, users} = testEnv;
const owner = deployer;
const spender = users[1];
const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID;
const deadline = MAX_UINT_AMOUNT;
const nonce = 1000;
const permitAmount = '0';
const msgParams = buildPermitParams(
chainId,
aDai.address,
'1',
await aDai.name(),
owner.address,
spender.address,
nonce,
deadline,
permitAmount
);
const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey;
if (!ownerPrivateKey) {
throw new Error('INVALID_OWNER_PK');
}
const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams);
await expect(
aDai
.connect(spender.signer)
.permit(owner.address, spender.address, permitAmount, deadline, v, r, s)
).to.be.revertedWith('INVALID_SIGNATURE');
});
it('Tries to submit a permit with invalid expiration (previous to the current block)', async () => {
const {aDai, deployer, users} = testEnv;
const owner = deployer;
const spender = users[1];
const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID;
const expiration = '1';
const nonce = (await aDai._nonces(owner.address)).toNumber();
const permitAmount = '0';
const msgParams = buildPermitParams(
chainId,
aDai.address,
'1',
await aDai.name(),
owner.address,
spender.address,
nonce,
expiration,
permitAmount
);
const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey;
if (!ownerPrivateKey) {
throw new Error('INVALID_OWNER_PK');
}
const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams);
await expect(
aDai
.connect(spender.signer)
.permit(owner.address, spender.address, expiration, permitAmount, v, r, s)
).to.be.revertedWith('INVALID_EXPIRATION');
});
it('Tries to submit a permit with invalid signature', async () => {
const {aDai, deployer, users} = testEnv;
const owner = deployer;
const spender = users[1];
const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID;
const deadline = MAX_UINT_AMOUNT;
const nonce = (await aDai._nonces(owner.address)).toNumber();
const permitAmount = '0';
const msgParams = buildPermitParams(
chainId,
aDai.address,
'1',
await aDai.name(),
owner.address,
spender.address,
nonce,
deadline,
permitAmount
);
const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey;
if (!ownerPrivateKey) {
throw new Error('INVALID_OWNER_PK');
}
const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams);
await expect(
aDai
.connect(spender.signer)
.permit(owner.address, ZERO_ADDRESS, permitAmount, deadline, v, r, s)
).to.be.revertedWith('INVALID_SIGNATURE');
});
it('Tries to submit a permit with invalid owner', async () => {
const {aDai, deployer, users} = testEnv;
const owner = deployer;
const spender = users[1];
const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID;
const expiration = MAX_UINT_AMOUNT;
const nonce = (await aDai._nonces(owner.address)).toNumber();
const permitAmount = '0';
const msgParams = buildPermitParams(
chainId,
aDai.address,
'1',
await aDai.name(),
owner.address,
spender.address,
nonce,
expiration,
permitAmount
);
const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey;
if (!ownerPrivateKey) {
throw new Error('INVALID_OWNER_PK');
}
const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams);
await expect(
aDai
.connect(spender.signer)
.permit(ZERO_ADDRESS, spender.address, expiration, permitAmount, v, r, s)
).to.be.revertedWith('INVALID_OWNER');
});
});