diff --git a/contracts/permit/EIP2612PermitAndDeposit.sol b/contracts/permit/EIP2612PermitAndDeposit.sol index b96f4070..35d239a3 100644 --- a/contracts/permit/EIP2612PermitAndDeposit.sol +++ b/contracts/permit/EIP2612PermitAndDeposit.sol @@ -11,67 +11,126 @@ import "../interfaces/ITicket.sol"; /** * @notice Secp256k1 signature values. + * @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 */ struct Signature { + uint256 deadline; uint8 v; bytes32 r; bytes32 s; } +/** + * @notice Delegate signature to allow delegation of tickets to delegate. + * @param delegate Address to delegate the prize pool tickets to + * @param signature Delegate signature + */ +struct DelegateSignature { + address delegate; + Signature signature; +} + /// @title Allows users to approve and deposit EIP-2612 compatible tokens into a prize pool in a single transaction. +/// @custom:experimental This contract has not been fully audited yet. contract EIP2612PermitAndDeposit { using SafeERC20 for IERC20; /** - * @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. + * @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 _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 _amount Amount of tokens to deposit into the prize pool * @param _to Address that will receive the tickets - * @param _delegate The address to delegate the prize pool tickets to + * @param _permitSignature Permit signature + * @param _delegateSignature Delegate signature */ function permitAndDepositToAndDelegate( - address _owner, - uint256 _amount, - uint256 _deadline, - Signature calldata _permitSignature, - Signature calldata _delegateSignature, IPrizePool _prizePool, + uint256 _amount, address _to, - address _delegate + Signature calldata _permitSignature, + DelegateSignature calldata _delegateSignature ) external { - require(msg.sender == _owner, "EIP2612PermitAndDeposit/only-signer"); - ITicket _ticket = _prizePool.getTicket(); address _token = _prizePool.getToken(); IERC20Permit(_token).permit( - _owner, + msg.sender, address(this), _amount, - _deadline, + _permitSignature.deadline, _permitSignature.v, _permitSignature.r, _permitSignature.s ); - _depositTo(_token, _owner, _amount, address(_prizePool), _to); + _depositToAndDelegate( + address(_prizePool), + _ticket, + _token, + _amount, + _to, + _delegateSignature + ); + } + + /** + * @notice Deposits user's token into the prize pool and delegate tickets. + * @param _prizePool Address of the prize pool to deposit into + * @param _amount Amount of tokens to deposit into the prize pool + * @param _to Address that will receive the tickets + * @param _delegateSignature Delegate signature + */ + function depositToAndDelegate( + IPrizePool _prizePool, + uint256 _amount, + address _to, + DelegateSignature calldata _delegateSignature + ) external { + ITicket _ticket = _prizePool.getTicket(); + address _token = _prizePool.getToken(); + + _depositToAndDelegate( + address(_prizePool), + _ticket, + _token, + _amount, + _to, + _delegateSignature + ); + } + + /** + * @notice Deposits user's token into the prize pool and delegate tickets. + * @param _prizePool Address of the prize pool to deposit into + * @param _ticket Address of the ticket minted by the prize pool + * @param _token Address of the token used to deposit into the prize pool + * @param _amount Amount of tokens to deposit into the prize pool + * @param _to Address that will receive the tickets + * @param _delegateSignature Delegate signature + */ + function _depositToAndDelegate( + address _prizePool, + ITicket _ticket, + address _token, + uint256 _amount, + address _to, + DelegateSignature calldata _delegateSignature + ) internal { + _depositTo(_token, msg.sender, _amount, _prizePool, _to); + + Signature memory signature = _delegateSignature.signature; _ticket.delegateWithSignature( - _owner, - _delegate, - _deadline, - _delegateSignature.v, - _delegateSignature.r, - _delegateSignature.s + _to, + _delegateSignature.delegate, + signature.deadline, + signature.v, + signature.r, + signature.s ); } diff --git a/package.json b/package.json index a20e3a00..6a96da02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pooltogether/v4-core", - "version": "1.1.0", + "version": "1.1.1", "description": "PoolTogether V4 Core Smart Contracts", "main": "index.js", "license": "GPL-3.0", diff --git a/test/permit/EIP2612PermitAndDeposit.test.ts b/test/permit/EIP2612PermitAndDeposit.test.ts index 196a4f0c..3f0fbf4d 100644 --- a/test/permit/EIP2612PermitAndDeposit.test.ts +++ b/test/permit/EIP2612PermitAndDeposit.test.ts @@ -32,14 +32,55 @@ describe('EIP2612PermitAndDeposit', () => { type EIP2612PermitAndDepositToAndDelegate = { prizePool: string; fromWallet?: SignerWithAddress; + toWallet?: SignerWithAddress; to: string; amount: string; delegateAddress: string; }; + async function generateDelegateSignature( + fromWallet: SignerWithAddress, + delegateAddress: string, + ) { + const { + user, + delegate, + deadline: delegateDeadline, + v, + r, + s, + } = await delegateSignature({ + ticket, + userWallet: fromWallet, + delegate: delegateAddress, + }); + + return { user, delegate, signature: { deadline: delegateDeadline, v, r, s } }; + } + + async function depositToAndDelegate({ + prizePool, + fromWallet, + to, + amount, + delegateAddress, + }: EIP2612PermitAndDepositToAndDelegate) { + if (!fromWallet) { + fromWallet = wallet; + } + + const { user, ...delegateSign } = await generateDelegateSignature( + fromWallet, + delegateAddress, + ); + + return permitAndDeposit.depositToAndDelegate(prizePool, amount, to, delegateSign); + } + async function permitAndDepositToAndDelegate({ prizePool, fromWallet, + toWallet, to, amount, delegateAddress, @@ -48,13 +89,12 @@ describe('EIP2612PermitAndDeposit', () => { fromWallet = wallet; } - const { user, delegate, deadline, v, r, s } = await delegateSignature({ - ticket, - userWallet: fromWallet, - delegate: delegateAddress, - }); + const { user, ...delegateSign } = await generateDelegateSignature( + toWallet ? toWallet : fromWallet, + delegateAddress, + ); - const delegateSign: SignatureLike = { v, r, s }; + const permitDeadline = (await provider.getBlock('latest')).timestamp + 50; const permit = await signPermit( fromWallet, @@ -65,25 +105,22 @@ describe('EIP2612PermitAndDeposit', () => { verifyingContract: usdc.address, }, { - owner: user, + owner: toWallet ? fromWallet.address : user, spender: permitAndDeposit.address, value: amount, nonce: 0, - deadline, + deadline: permitDeadline, }, ); - const permitSignature = splitSignature(permit.sig); + const permitSignature = { deadline: permitDeadline, ...splitSignature(permit.sig) }; return permitAndDeposit.permitAndDepositToAndDelegate( - user, + prizePool, amount, - deadline, + to, permitSignature, delegateSign, - prizePool, - to, - delegate, ); } @@ -156,6 +193,29 @@ describe('EIP2612PermitAndDeposit', () => { expect(await ticket.delegateOf(wallet2.address)).to.equal(AddressZero); }); + it('should deposit tickets to someone else and delegate on their behalf', async () => { + const amount = toWei('100'); + + await usdc.mint(wallet.address, toWei('1000')); + + await yieldSourceStub.mock.supplyTokenTo.withArgs(amount, prizePool.address).returns(); + + await permitAndDepositToAndDelegate({ + prizePool: prizePool.address, + toWallet: wallet2, + to: wallet2.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(toWei('0')); + expect(await ticket.balanceOf(wallet2.address)).to.equal(amount); + expect(await ticket.delegateOf(wallet.address)).to.equal(AddressZero); + expect(await ticket.delegateOf(wallet2.address)).to.equal(wallet2.address); + }); + it('should not allow anyone else to use the signature', async () => { const amount = toWei('100'); @@ -171,7 +231,57 @@ describe('EIP2612PermitAndDeposit', () => { amount: '100000000000000000000', delegateAddress: wallet2.address, }), - ).to.be.revertedWith('EIP2612PermitAndDeposit/only-signer'); + ).to.be.revertedWith('ERC20Permit: invalid signature'); + }); + }); + + describe('permitAndDepositToAndDelegate()', () => { + it('should deposit and delegate to itself', async () => { + const amount = toWei('100'); + + await usdc.mint(wallet.address, toWei('1000')); + await usdc.approve(permitAndDeposit.address, amount); + + expect(await usdc.allowance(wallet.address, permitAndDeposit.address)).to.equal(amount); + + await yieldSourceStub.mock.supplyTokenTo.withArgs(amount, prizePool.address).returns(); + + await depositToAndDelegate({ + prizePool: prizePool.address, + to: wallet.address, + amount: '100000000000000000000', + delegateAddress: wallet.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.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 usdc.approve(permitAndDeposit.address, amount); + + expect(await usdc.allowance(wallet.address, permitAndDeposit.address)).to.equal(amount); + + await yieldSourceStub.mock.supplyTokenTo.withArgs(amount, prizePool.address).returns(); + + await depositToAndDelegate({ + 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); }); }); });