From 1763f1ec518f1320af29082cae62e3fb891bbbf0 Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:09:36 +0530 Subject: [PATCH] test: buyVoucherWithPermit --- .gitignore | 4 +- contracts/common/misc/EIP712.sol | 5 + test/helpers/artifacts.js | 2 +- test/units/staking/ValidatorShare.test.js | 170 ++++++++++++++++++++- test/units/staking/ValidatorShareHelper.js | 25 +++ test/units/staking/permitHelper.js | 66 ++++++++ 6 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 test/units/staking/permitHelper.js diff --git a/.gitignore b/.gitignore index 11887e4e..d3bc16ce 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,8 @@ contractAddresses.json contracts/root/predicates/TransferWithSigUtils.sol contracts/common/mixin/ChainIdMixin.sol test/helpers/marketplaceUtils.js -cache_hardhat -forge-cache +cache_hardhat/ +forge-cache/ test-bor-docker/history diff --git a/contracts/common/misc/EIP712.sol b/contracts/common/misc/EIP712.sol index 2055cf9a..4b462141 100644 --- a/contracts/common/misc/EIP712.sol +++ b/contracts/common/misc/EIP712.sol @@ -20,6 +20,7 @@ contract EIP712 { bytes32 private _HASHED_VERSION; bytes32 private _TYPE_HASH; + string private _VERSION; /* solhint-enable var-name-mixedcase */ /** @@ -47,6 +48,10 @@ contract EIP712 { _TYPE_HASH = typeHash; } + function version() external view returns (string memory) { + return _VERSION; + } + function _chainId() internal pure returns (uint chainId) { assembly { chainId := chainid() diff --git a/test/helpers/artifacts.js b/test/helpers/artifacts.js index 9cb8d1cb..0305a13c 100644 --- a/test/helpers/artifacts.js +++ b/test/helpers/artifacts.js @@ -1,5 +1,5 @@ import hardhat from 'hardhat' -const ethers = hardhat.ethers +export const ethers = hardhat.ethers export const RootChain = await ethers.getContractFactory('RootChain') export const RootChainProxy = await ethers.getContractFactory('RootChainProxy') diff --git a/test/units/staking/ValidatorShare.test.js b/test/units/staking/ValidatorShare.test.js index abf99bbd..d08ee0b3 100644 --- a/test/units/staking/ValidatorShare.test.js +++ b/test/units/staking/ValidatorShare.test.js @@ -1,8 +1,9 @@ -import { TestToken, ValidatorShare, StakingInfo, EventsHub } from '../../helpers/artifacts.js' +import ethUtils from 'ethereumjs-util' +import { TestToken, ERC20Permit, ValidatorShare, StakingInfo, EventsHub, ethers } from '../../helpers/artifacts.js' import testHelpers from '@openzeppelin/test-helpers' import { checkPoint, assertBigNumberEquality, updateSlashedAmounts, assertInTransaction } from '../../helpers/utils.js' import { wallets, freshDeploy, approveAndStake } from './deployment.js' -import { buyVoucher, sellVoucher, sellVoucherNew } from './ValidatorShareHelper.js' +import { buyVoucher, buyVoucherWithPermit, sellVoucher, sellVoucherNew } from './ValidatorShareHelper.js' const BN = testHelpers.BN const expectRevert = testHelpers.expectRevert const toWei = web3.utils.toWei @@ -26,7 +27,7 @@ describe('ValidatorShare', async function () { async function doDeploy() { await freshDeploy.call(this) - this.stakeToken = await TestToken.deploy('MATIC', 'MATIC') + this.stakeToken = await ERC20Permit.deploy('POL', 'POL', '1.1.0') await this.governance.update( this.stakeManager.address, @@ -48,6 +49,14 @@ describe('ValidatorShare', async function () { this.stakeManager.interface.encodeFunctionData('updateValidatorThreshold', [8]) ) + await this.governance.update( + this.registry.address, + this.registry.interface.encodeFunctionData('updateContractMap', [ + ethUtils.keccak256('pol'), + this.stakeToken.address + ]) + ) + // we need to increase validator id beyond foundation id, repeat 7 times for (let i = 0; i < 7; ++i) { await approveAndStake.call(this, { @@ -143,6 +152,161 @@ describe('ValidatorShare', async function () { }) } + describe('buyVoucherWithPermit', function () { + function testBuyVoucherWithPermit({ + voucherValue, + voucherValueExpected, + userTotalStaked, + totalStaked, + shares, + reward, + initialBalance + }) { + it('must buy voucher with permit', async function () { + assertBigNumberEquality(await this.stakeToken.allowance(this.user, this.stakeManager.address), 0) + this.receipt = await ( + await buyVoucherWithPermit( + this.validatorContract, + voucherValue, + this.user, + shares, + this.stakeManager.address, + this.stakeToken + ) + ).wait() + }) + + shouldBuyShares({ + voucherValueExpected, + shares, + totalStaked + }) + + shouldHaveCorrectStakes({ + userTotalStaked, + totalStaked + }) + + shouldWithdrawReward({ + initialBalance, + reward, + validatorId: '8' + }) + } + + describe('when Alice purchases voucher with permit once', function () { + deployAliceAndBob() + + before(async function () { + this.user = this.alice + await this.stakeToken + .connect(this.stakeToken.provider.getSigner(this.user)) + .approve(this.stakeManager.address, 0) + }) + + testBuyVoucherWithPermit({ + voucherValue: toWei('100'), + voucherValueExpected: toWei('100'), + userTotalStaked: toWei('100'), + totalStaked: toWei('200'), + shares: toWei('100'), + reward: '0', + initialBalance: toWei('69900') + }) + }) + + describe('when alice provides invalid permit signature', function () { + deployAliceAndBob() + + before(async function () { + this.user = this.alice + await this.stakeToken + .connect(this.stakeToken.provider.getSigner(this.user)) + .approve(this.stakeManager.address, 0) + }) + + it('reverts with incorrect spender', async function () { + assertBigNumberEquality(await this.stakeToken.allowance(this.user, this.stakeManager.address), 0) + + await expectRevert( + buyVoucherWithPermit( + this.validatorContract, + toWei('1000'), + this.user, + toWei('1000'), + this.validatorContract.address /* spender, tokens are pulled from stakeManager */, + this.stakeToken + ), + 'ERC2612InvalidSigner' + ) + }) + + it('reverts with incorrect deadline', async function () { + assertBigNumberEquality(await this.stakeToken.allowance(this.user, this.stakeManager.address), 0) + + await expectRevert( + buyVoucherWithPermit( + this.validatorContract, + toWei('1000'), + this.user, + toWei('1000'), + this.stakeManager.address, + this.stakeToken, + (await this.validatorContract.provider.getBlock('latest')).timestamp - 60 + ), + 'ERC2612ExpiredSignature' + ) + }) + }) + + describe('when locked', function () { + deployAliceAndBob() + + before(async function () { + await this.stakeManager.testLockShareContract(this.validatorId, true) + }) + + it('reverts', async function () { + await expectRevert( + buyVoucherWithPermit( + this.validatorContract, + toWei('100'), + this.alice, + toWei('100'), + this.stakeManager.address, + this.stakeToken + ), + 'locked' + ) + }) + }) + + describe('when validator unstaked', function () { + deployAliceAndBob() + before(async function () { + const stakeManagerValidator = this.stakeManager.connect( + this.stakeManager.provider.getSigner(this.validatorUser.getChecksumAddressString()) + ) + await stakeManagerValidator.unstake(this.validatorId) + await this.stakeManager.advanceEpoch(Dynasty) + }) + + it('reverts', async function () { + await expectRevert( + buyVoucherWithPermit( + this.validatorContract, + toWei('100'), + this.alice, + toWei('100'), + this.stakeManager.address, + this.stakeToken + ), + 'locked' + ) + }) + }) + }) + describe('buyVoucher', function () { function testBuyVoucher({ voucherValue, diff --git a/test/units/staking/ValidatorShareHelper.js b/test/units/staking/ValidatorShareHelper.js index 563f1030..d6e7cb66 100644 --- a/test/units/staking/ValidatorShareHelper.js +++ b/test/units/staking/ValidatorShareHelper.js @@ -1,8 +1,33 @@ +import { getPermitDigest } from './permitHelper.js' + export async function buyVoucher(validatorContract, amount, delegator, minSharesToMint) { const validatorContract_Delegator = validatorContract.connect(validatorContract.provider.getSigner(delegator)) return validatorContract_Delegator.buyVoucher(amount.toString(), minSharesToMint || 0) } +export async function buyVoucherWithPermit( + validatorContract, + amount, + delegator, + minSharesToMint, + spender, + token, + deadline +) { + const signer = validatorContract.provider.getSigner(delegator) + const validatorContract_Delegator = validatorContract.connect(signer) + + if (!deadline) deadline = (await validatorContract.provider.getBlock('latest')).timestamp + 10 + + const signature = await signer._signTypedData(...(await getPermitDigest(delegator, spender, amount, token, deadline))) + + const r = signature.slice(0, 66) + const s = '0x' + signature.slice(66, 130) + const v = '0x' + signature.slice(130, 132) + + return validatorContract_Delegator.buyVoucherWithPermit(amount.toString(), minSharesToMint || 0, deadline, v, r, s) +} + export async function sellVoucher(validatorContract, delegator, minClaimAmount, maxShares) { if (maxShares === undefined) { maxShares = await validatorContract.balanceOf(delegator) diff --git a/test/units/staking/permitHelper.js b/test/units/staking/permitHelper.js new file mode 100644 index 00000000..83e11f51 --- /dev/null +++ b/test/units/staking/permitHelper.js @@ -0,0 +1,66 @@ +import { keccak256 } from 'ethereumjs-util' +import encode from 'ethereumjs-abi' + +const PERMIT_TYPEHASH = keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)') // 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9 + +export function getStructHash({ owner, spender, value, nonce, deadline }) { + return keccak256( + encode( + ['bytes32', 'address', 'address', 'uint256', 'uint256', 'uint256'], + [PERMIT_TYPEHASH, owner, spender, value, nonce, deadline] + ) + ) +} + +export function getTypedDataHash(DOMAIN_SEPARATOR, permitData) { + return keccak256(encode(['bytes', 'bytes32', 'bytes32'], ['\x19\x01', DOMAIN_SEPARATOR, getStructHash(permitData)])) +} + +const cache = new Map() + +export async function getPermitDigest(owner, spender, value, token, deadline) { + let { name, version } = cache.get(token.address) || {} + if (!name || !version) { + ;[name, version] = await Promise.all([token.name(), token.version()]) + cache.set(token.address, { name, version }) + } + return [ + { + name: 'POL', + version: '1.1.0', + chainId: (await token.provider._network).chainId, + verifyingContract: token.address + }, + { + Permit: [ + { + name: 'owner', + type: 'address' + }, + { + name: 'spender', + type: 'address' + }, + { + name: 'value', + type: 'uint256' + }, + { + name: 'nonce', + type: 'uint256' + }, + { + name: 'deadline', + type: 'uint256' + } + ] + }, + { + owner, + spender, + value, + nonce: await token.nonces(owner), + deadline + } + ] +}