diff --git a/contracts/permit/EIP2612PermitAndDeposit.sol b/contracts/permit/EIP2612PermitAndDeposit.sol index afaa8846..b96f4070 100644 --- a/contracts/permit/EIP2612PermitAndDeposit.sol +++ b/contracts/permit/EIP2612PermitAndDeposit.sol @@ -7,6 +7,19 @@ 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 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 Signature { + 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 { @@ -14,42 +27,61 @@ 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 _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 _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 _delegate The address to delegate the prize pool tickets to */ - function permitAndDepositTo( - address _token, + function permitAndDepositToAndDelegate( address _owner, uint256 _amount, uint256 _deadline, - uint8 _v, - bytes32 _r, - bytes32 _s, - address _prizePool, - address _to + Signature calldata _permitSignature, + Signature calldata _delegateSignature, + IPrizePool _prizePool, + address _to, + address _delegate ) external { require(msg.sender == _owner, "EIP2612PermitAndDeposit/only-signer"); - IERC20Permit(_token).permit(_owner, address(this), _amount, _deadline, _v, _r, _s); + ITicket _ticket = _prizePool.getTicket(); + address _token = _prizePool.getToken(); + + IERC20Permit(_token).permit( + _owner, + address(this), + _amount, + _deadline, + _permitSignature.v, + _permitSignature.r, + _permitSignature.s + ); + + _depositTo(_token, _owner, _amount, address(_prizePool), _to); - _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/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", 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..196a4f0c 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,46 @@ 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; + delegateAddress: string; }; - async function permitAndDepositTo({ + async function permitAndDepositToAndDelegate({ prizePool, fromWallet, to, amount, - }: EIP2612PermitAndDepositTo) { + 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 +65,7 @@ describe('EIP2612PermitAndDeposit', () => { verifyingContract: usdc.address, }, { - owner: wallet.address, + owner: user, spender: permitAndDeposit.address, value: amount, nonce: 0, @@ -59,25 +73,22 @@ describe('EIP2612PermitAndDeposit', () => { }, ); - let { v, r, s } = splitSignature(permit.sig); - - return permitAndDeposit - .connect(fromWallet) - .permitAndDepositTo( - usdc.address, - wallet.address, - amount, - deadline, - v, - r, - s, - prizePool, - to, - ); + const permitSignature = splitSignature(permit.sig); + + return permitAndDeposit.permitAndDepositToAndDelegate( + user, + amount, + deadline, + permitSignature, + delegateSign, + prizePool, + to, + delegate, + ); } beforeEach(async () => { - [wallet, wallet2, wallet3] = await getSigners(); + [wallet, wallet2, prizeStrategyManager] = await getSigners(); const network = await getNetwork(); chainId = network.chainId; @@ -85,45 +96,80 @@ 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', + 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', + 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', + delegateAddress: wallet2.address, }), ).to.be.revertedWith('EIP2612PermitAndDeposit/only-signer'); });