From d6d13e719d4aae56fcbe36387eeb903fdd2b9ef4 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 1 Nov 2021 15:39:57 -0500 Subject: [PATCH] feat(EIP2612): add permitAndDepositToAndDelegate --- contracts/permit/EIP2612PermitAndDeposit.sol | 84 ++++++++++--- test/Ticket.test.ts | 9 +- test/permit/EIP2612PermitAndDeposit.test.ts | 125 ++++++++++++++----- 3 files changed, 159 insertions(+), 59 deletions(-) diff --git a/contracts/permit/EIP2612PermitAndDeposit.sol b/contracts/permit/EIP2612PermitAndDeposit.sol index afaa8846..c05b793f 100644 --- a/contracts/permit/EIP2612PermitAndDeposit.sol +++ b/contracts/permit/EIP2612PermitAndDeposit.sol @@ -7,6 +7,31 @@ import "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../interfaces/IPrizePool.sol"; +import "../interfaces/ITicket.sol"; + +/** + * @notice Permit signature to allow spending of ERC20 token by this contract. + * @param v `v` portion of the signature + * @param r `r` portion of the signature + * @param s `s` portion of the signature + */ +struct PermitSignature { + uint8 v; + bytes32 r; + bytes32 s; +} + +/** + * @notice Delegate signature to allow delegation of tickets to delegate. + * @param v `v` portion of the signature + * @param r `r` portion of the signature + * @param s `s` portion of the signature + */ +struct DelegateSignature { + uint8 v; + bytes32 r; + bytes32 s; +} /// @title Allows users to approve and deposit EIP-2612 compatible tokens into a prize pool in a single transaction. contract EIP2612PermitAndDeposit { @@ -15,41 +40,60 @@ contract EIP2612PermitAndDeposit { /** * @notice Permits this contract to spend on a user's behalf, and deposits into the prize pool. * @dev The `spender` address required by the permit function is the address of this contract. - * @param _token Address of the EIP-2612 token to approve and deposit. - * @param _owner Token owner's address (Authorizer). - * @param _amount Amount of tokens to deposit. - * @param _deadline Timestamp at which the signature expires. - * @param _v `v` portion of the signature. - * @param _r `r` portion of the signature. - * @param _s `s` portion of the signature. - * @param _prizePool Address of the prize pool to deposit into. - * @param _to Address that will receive the tickets. + * @param _token Address of the EIP-2612 token to approve and deposit + * @param _owner Token owner's address (Authorizer) + * @param _amount Amount of tokens to deposit + * @param _deadline Timestamp at which the signature expires + * @param _permitSignature Permit signature + * @param _delegateSignature Delegate signature + * @param _prizePool Address of the prize pool to deposit into + * @param _to Address that will receive the tickets + * @param _ticket Address of the prize pool ticket + * @param _delegate The address to delegate the prize pool tickets to */ - function permitAndDepositTo( + function permitAndDepositToAndDelegate( address _token, address _owner, uint256 _amount, uint256 _deadline, - uint8 _v, - bytes32 _r, - bytes32 _s, + PermitSignature calldata _permitSignature, + DelegateSignature calldata _delegateSignature, address _prizePool, - address _to + address _to, + ITicket _ticket, + address _delegate ) external { require(msg.sender == _owner, "EIP2612PermitAndDeposit/only-signer"); - IERC20Permit(_token).permit(_owner, address(this), _amount, _deadline, _v, _r, _s); + IERC20Permit(_token).permit( + _owner, + address(this), + _amount, + _deadline, + _permitSignature.v, + _permitSignature.r, + _permitSignature.s + ); _depositTo(_token, _owner, _amount, _prizePool, _to); + + _ticket.delegateWithSignature( + _owner, + _delegate, + _deadline, + _delegateSignature.v, + _delegateSignature.r, + _delegateSignature.s + ); } /** * @notice Deposits user's token into the prize pool. - * @param _token Address of the EIP-2612 token to approve and deposit. - * @param _owner Token owner's address (Authorizer). - * @param _amount Amount of tokens to deposit. - * @param _prizePool Address of the prize pool to deposit into. - * @param _to Address that will receive the tickets. + * @param _token Address of the EIP-2612 token to approve and deposit + * @param _owner Token owner's address (Authorizer) + * @param _amount Amount of tokens to deposit + * @param _prizePool Address of the prize pool to deposit into + * @param _to Address that will receive the tickets */ function _depositTo( address _token, diff --git a/test/Ticket.test.ts b/test/Ticket.test.ts index 258f5de4..41250483 100644 --- a/test/Ticket.test.ts +++ b/test/Ticket.test.ts @@ -1,9 +1,7 @@ -import { Signer } from '@ethersproject/abstract-signer'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { deployMockContract, MockContract } from 'ethereum-waffle'; import { utils, Contract, ContractFactory, BigNumber } from 'ethers'; -import hre, { ethers } from 'hardhat'; +import { ethers } from 'hardhat'; import { delegateSignature } from './helpers/delegateSignature'; import { increaseTime as increaseTimeHelper } from './helpers/increaseTime'; @@ -12,7 +10,7 @@ const newDebug = require('debug'); const debug = newDebug('pt:Ticket.test.ts'); const { constants, getSigners, provider } = ethers; -const { AddressZero, MaxUint256 } = constants; +const { AddressZero } = constants; const { getBlock } = provider; const { parseEther: toWei } = utils; @@ -928,8 +926,7 @@ describe('Ticket', () => { describe('delegateWithSignature()', () => { it('should allow somone to delegate with a signature', async () => { - // @ts-ignore - const { user, delegate, nonce, deadline, v, r, s } = await delegateSignature({ + const { user, delegate, deadline, v, r, s } = await delegateSignature({ ticket, userWallet: wallet1, delegate: wallet2.address, diff --git a/test/permit/EIP2612PermitAndDeposit.test.ts b/test/permit/EIP2612PermitAndDeposit.test.ts index d8c47c01..1699f4f9 100644 --- a/test/permit/EIP2612PermitAndDeposit.test.ts +++ b/test/permit/EIP2612PermitAndDeposit.test.ts @@ -1,13 +1,16 @@ import { Signer } from '@ethersproject/abstract-signer'; +import { SignatureLike } from '@ethersproject/bytes'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { utils, Contract } from 'ethers'; +import { utils, Contract, ContractFactory } from 'ethers'; import { deployMockContract, MockContract } from 'ethereum-waffle'; import hre, { ethers } from 'hardhat'; +import { delegateSignature } from '../helpers/delegateSignature'; import { signPermit } from '../helpers/signPermit'; -const { getContractFactory, getSigners, provider } = ethers; +const { constants, getContractFactory, getSigners, provider } = ethers; +const { AddressZero } = constants; const { artifacts } = hre; const { getNetwork } = provider; const { parseEther: toWei, splitSignature } = utils; @@ -15,35 +18,48 @@ const { parseEther: toWei, splitSignature } = utils; describe('EIP2612PermitAndDeposit', () => { let wallet: SignerWithAddress; let wallet2: SignerWithAddress; - let wallet3: SignerWithAddress; + let prizeStrategyManager: SignerWithAddress; let permitAndDeposit: Contract; let usdc: Contract; - let prizePool: MockContract; + let PrizePoolHarness: ContractFactory; + let prizePool: Contract; + let ticket: Contract; + let yieldSourceStub: MockContract; let chainId: number; - type EIP2612PermitAndDepositTo = { + type EIP2612PermitAndDepositToAndDelegate = { prizePool: string; fromWallet?: SignerWithAddress; to: string; amount: string; + ticketAddress: string; + delegateAddress: string; }; - async function permitAndDepositTo({ + async function permitAndDepositToAndDelegate({ prizePool, fromWallet, to, amount, - }: EIP2612PermitAndDepositTo) { + ticketAddress, + delegateAddress + }: EIP2612PermitAndDepositToAndDelegate) { if (!fromWallet) { fromWallet = wallet; } - const deadline = new Date().getTime(); + const { user, delegate, deadline, v, r, s } = await delegateSignature({ + ticket, + userWallet: fromWallet, + delegate: delegateAddress, + }); + + const delegateSign: SignatureLike = { v, r, s }; - let permit = await signPermit( - wallet, + const permit = await signPermit( + fromWallet, { name: 'USD Coin', version: '1', @@ -51,7 +67,7 @@ describe('EIP2612PermitAndDeposit', () => { verifyingContract: usdc.address, }, { - owner: wallet.address, + owner: user, spender: permitAndDeposit.address, value: amount, nonce: 0, @@ -59,25 +75,25 @@ describe('EIP2612PermitAndDeposit', () => { }, ); - let { v, r, s } = splitSignature(permit.sig); + const permitSignature = splitSignature(permit.sig); return permitAndDeposit - .connect(fromWallet) - .permitAndDepositTo( + .permitAndDepositToAndDelegate( usdc.address, - wallet.address, + user, amount, deadline, - v, - r, - s, + permitSignature, + delegateSign, prizePool, to, + ticketAddress, + delegate ); } beforeEach(async () => { - [wallet, wallet2, wallet3] = await getSigners(); + [wallet, wallet2, prizeStrategyManager] = await getSigners(); const network = await getNetwork(); chainId = network.chainId; @@ -85,45 +101,88 @@ describe('EIP2612PermitAndDeposit', () => { const Usdc = await getContractFactory('EIP2612PermitMintable'); usdc = await Usdc.deploy('USD Coin', 'USDC'); - const IPrizePool = await artifacts.readArtifact('IPrizePool'); - prizePool = await deployMockContract(wallet as Signer, IPrizePool.abi); + const YieldSourceStub = await artifacts.readArtifact('YieldSourceStub'); + yieldSourceStub = await deployMockContract(wallet as Signer, YieldSourceStub.abi); + await yieldSourceStub.mock.depositToken.returns(usdc.address); - const EIP2612PermitAndDeposit = await getContractFactory('EIP2612PermitAndDeposit'); + PrizePoolHarness = await getContractFactory('PrizePoolHarness', wallet); + prizePool = await PrizePoolHarness.deploy(wallet.address, yieldSourceStub.address); + const EIP2612PermitAndDeposit = await getContractFactory('EIP2612PermitAndDeposit'); permitAndDeposit = await EIP2612PermitAndDeposit.deploy(); + + const Ticket = await getContractFactory('TicketHarness'); + ticket = await Ticket.deploy( + 'PoolTogether Usdc Ticket', + 'PcUSDC', + 18, + prizePool.address, + ); + + await prizePool.setTicket(ticket.address); + await prizePool.setPrizeStrategy(prizeStrategyManager.address); }); - describe('permitAndDepositTo()', () => { - it('should work', async () => { + describe('permitAndDepositToAndDelegate()', () => { + it('should deposit and delegate to itself', async () => { + const amount = toWei('100'); + await usdc.mint(wallet.address, toWei('1000')); - await prizePool.mock.depositTo.withArgs(wallet2.address, toWei('100')).returns(); + await yieldSourceStub.mock.supplyTokenTo.withArgs(amount, prizePool.address).returns(); - await permitAndDepositTo({ + await permitAndDepositToAndDelegate({ prizePool: prizePool.address, - to: wallet2.address, + to: wallet.address, amount: '100000000000000000000', + ticketAddress: ticket.address, + delegateAddress: wallet.address }); - expect(await usdc.allowance(permitAndDeposit.address, prizePool.address)).to.equal( - toWei('100'), - ); + expect(await usdc.balanceOf(prizePool.address)).to.equal(amount); + expect(await usdc.balanceOf(wallet.address)).to.equal(toWei('900')); + expect(await ticket.balanceOf(wallet.address)).to.equal(amount); + expect(await ticket.delegateOf(wallet.address)).to.equal(wallet.address); + }); + + it('should deposit and delegate to someone else', async () => { + const amount = toWei('100'); + + await usdc.mint(wallet.address, toWei('1000')); + + await yieldSourceStub.mock.supplyTokenTo.withArgs(amount, prizePool.address).returns(); - expect(await usdc.balanceOf(permitAndDeposit.address)).to.equal(toWei('100')); + await permitAndDepositToAndDelegate({ + prizePool: prizePool.address, + to: wallet.address, + amount: '100000000000000000000', + ticketAddress: ticket.address, + delegateAddress: wallet2.address + }); + + expect(await usdc.balanceOf(prizePool.address)).to.equal(amount); expect(await usdc.balanceOf(wallet.address)).to.equal(toWei('900')); + expect(await ticket.balanceOf(wallet.address)).to.equal(amount); + expect(await ticket.balanceOf(wallet2.address)).to.equal(toWei('0')); + expect(await ticket.delegateOf(wallet.address)).to.equal(wallet2.address); + expect(await ticket.delegateOf(wallet2.address)).to.equal(AddressZero); }); it('should not allow anyone else to use the signature', async () => { + const amount = toWei('100'); + await usdc.mint(wallet.address, toWei('1000')); - await prizePool.mock.depositTo.withArgs(wallet2.address, toWei('100')).returns(); + await yieldSourceStub.mock.supplyTokenTo.withArgs(amount, prizePool.address).returns(); await expect( - permitAndDepositTo({ + permitAndDepositToAndDelegate({ prizePool: prizePool.address, to: wallet2.address, fromWallet: wallet2, amount: '100000000000000000000', + ticketAddress: ticket.address, + delegateAddress: wallet2.address }), ).to.be.revertedWith('EIP2612PermitAndDeposit/only-signer'); });