From d6d13e719d4aae56fcbe36387eeb903fdd2b9ef4 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 1 Nov 2021 15:39:57 -0500 Subject: [PATCH 1/5] 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'); }); From 1bbaf51ad6809203ef725c0440f0398ef822ccdf Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 1 Nov 2021 18:38:56 -0500 Subject: [PATCH 2/5] fix(EIP2612): use only one struct for signature --- contracts/permit/EIP2612PermitAndDeposit.sol | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/contracts/permit/EIP2612PermitAndDeposit.sol b/contracts/permit/EIP2612PermitAndDeposit.sol index c05b793f..3d32f1f0 100644 --- a/contracts/permit/EIP2612PermitAndDeposit.sol +++ b/contracts/permit/EIP2612PermitAndDeposit.sol @@ -10,24 +10,12 @@ import "../interfaces/IPrizePool.sol"; import "../interfaces/ITicket.sol"; /** - * @notice Permit signature to allow spending of ERC20 token by this contract. + * @notice Secp256k1 signature values. * @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 { +struct Signature { uint8 v; bytes32 r; bytes32 s; @@ -56,8 +44,8 @@ contract EIP2612PermitAndDeposit { address _owner, uint256 _amount, uint256 _deadline, - PermitSignature calldata _permitSignature, - DelegateSignature calldata _delegateSignature, + Signature calldata _permitSignature, + Signature calldata _delegateSignature, address _prizePool, address _to, ITicket _ticket, From 769cfda70b08e8cc0304de02483378212c785f56 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 1 Nov 2021 18:47:16 -0500 Subject: [PATCH 3/5] fix(EIP2612): retrieve ticket and token from prizePool --- contracts/permit/EIP2612PermitAndDeposit.sol | 11 +++-- test/permit/EIP2612PermitAndDeposit.test.ts | 43 +++++++------------- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/contracts/permit/EIP2612PermitAndDeposit.sol b/contracts/permit/EIP2612PermitAndDeposit.sol index 3d32f1f0..26496f78 100644 --- a/contracts/permit/EIP2612PermitAndDeposit.sol +++ b/contracts/permit/EIP2612PermitAndDeposit.sol @@ -28,7 +28,6 @@ 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 @@ -36,23 +35,23 @@ contract EIP2612PermitAndDeposit { * @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 permitAndDepositToAndDelegate( - address _token, address _owner, uint256 _amount, uint256 _deadline, Signature calldata _permitSignature, Signature calldata _delegateSignature, - address _prizePool, + IPrizePool _prizePool, address _to, - ITicket _ticket, address _delegate ) external { require(msg.sender == _owner, "EIP2612PermitAndDeposit/only-signer"); + ITicket _ticket = _prizePool.getTicket(); + address _token = _prizePool.getToken(); + IERC20Permit(_token).permit( _owner, address(this), @@ -63,7 +62,7 @@ contract EIP2612PermitAndDeposit { _permitSignature.s ); - _depositTo(_token, _owner, _amount, _prizePool, _to); + _depositTo(_token, _owner, _amount, address(_prizePool), _to); _ticket.delegateWithSignature( _owner, diff --git a/test/permit/EIP2612PermitAndDeposit.test.ts b/test/permit/EIP2612PermitAndDeposit.test.ts index 1699f4f9..196a4f0c 100644 --- a/test/permit/EIP2612PermitAndDeposit.test.ts +++ b/test/permit/EIP2612PermitAndDeposit.test.ts @@ -34,7 +34,6 @@ describe('EIP2612PermitAndDeposit', () => { fromWallet?: SignerWithAddress; to: string; amount: string; - ticketAddress: string; delegateAddress: string; }; @@ -43,8 +42,7 @@ describe('EIP2612PermitAndDeposit', () => { fromWallet, to, amount, - ticketAddress, - delegateAddress + delegateAddress, }: EIP2612PermitAndDepositToAndDelegate) { if (!fromWallet) { fromWallet = wallet; @@ -77,19 +75,16 @@ describe('EIP2612PermitAndDeposit', () => { const permitSignature = splitSignature(permit.sig); - return permitAndDeposit - .permitAndDepositToAndDelegate( - usdc.address, - user, - amount, - deadline, - permitSignature, - delegateSign, - prizePool, - to, - ticketAddress, - delegate - ); + return permitAndDeposit.permitAndDepositToAndDelegate( + user, + amount, + deadline, + permitSignature, + delegateSign, + prizePool, + to, + delegate, + ); } beforeEach(async () => { @@ -112,12 +107,7 @@ describe('EIP2612PermitAndDeposit', () => { permitAndDeposit = await EIP2612PermitAndDeposit.deploy(); const Ticket = await getContractFactory('TicketHarness'); - ticket = await Ticket.deploy( - 'PoolTogether Usdc Ticket', - 'PcUSDC', - 18, - prizePool.address, - ); + ticket = await Ticket.deploy('PoolTogether Usdc Ticket', 'PcUSDC', 18, prizePool.address); await prizePool.setTicket(ticket.address); await prizePool.setPrizeStrategy(prizeStrategyManager.address); @@ -135,8 +125,7 @@ describe('EIP2612PermitAndDeposit', () => { prizePool: prizePool.address, to: wallet.address, amount: '100000000000000000000', - ticketAddress: ticket.address, - delegateAddress: wallet.address + delegateAddress: wallet.address, }); expect(await usdc.balanceOf(prizePool.address)).to.equal(amount); @@ -156,8 +145,7 @@ describe('EIP2612PermitAndDeposit', () => { prizePool: prizePool.address, to: wallet.address, amount: '100000000000000000000', - ticketAddress: ticket.address, - delegateAddress: wallet2.address + delegateAddress: wallet2.address, }); expect(await usdc.balanceOf(prizePool.address)).to.equal(amount); @@ -181,8 +169,7 @@ describe('EIP2612PermitAndDeposit', () => { to: wallet2.address, fromWallet: wallet2, amount: '100000000000000000000', - ticketAddress: ticket.address, - delegateAddress: wallet2.address + delegateAddress: wallet2.address, }), ).to.be.revertedWith('EIP2612PermitAndDeposit/only-signer'); }); From 2ca3f11514284a44cc978e81b4f46c781c455551 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 2 Nov 2021 11:35:43 -0500 Subject: [PATCH 4/5] chore(EIP2612): add unaudited code comment --- contracts/permit/EIP2612PermitAndDeposit.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/permit/EIP2612PermitAndDeposit.sol b/contracts/permit/EIP2612PermitAndDeposit.sol index 26496f78..b96f4070 100644 --- a/contracts/permit/EIP2612PermitAndDeposit.sol +++ b/contracts/permit/EIP2612PermitAndDeposit.sol @@ -27,6 +27,7 @@ contract EIP2612PermitAndDeposit { /** * @notice Permits this contract to spend on a user's behalf, and deposits into the prize pool. + * @custom:experimental This function has not been audited yet. * @dev The `spender` address required by the permit function is the address of this contract. * @param _owner Token owner's address (Authorizer) * @param _amount Amount of tokens to deposit From 6fdac950b75a64fafb3af7472bc735f96c352d50 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 2 Nov 2021 11:41:15 -0500 Subject: [PATCH 5/5] 1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 598ecfcf..a20e3a00 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pooltogether/v4-core", - "version": "1.0.0", + "version": "1.1.0", "description": "PoolTogether V4 Core Smart Contracts", "main": "index.js", "license": "GPL-3.0",