Skip to content

Commit

Permalink
Merge pull request #249 from pooltogether/pool-1824-add-deposittoandd…
Browse files Browse the repository at this point in the history
…elegate-to

feat(EIP2612): add permitAndDepositToAndDelegate
  • Loading branch information
PierrickGT authored Nov 2, 2021
2 parents 3092ea0 + 6fdac95 commit f233c6f
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 71 deletions.
78 changes: 55 additions & 23 deletions contracts/permit/EIP2612PermitAndDeposit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,81 @@ 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 {
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.
* @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,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
9 changes: 3 additions & 6 deletions test/Ticket.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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;

Expand Down Expand Up @@ -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,
Expand Down
128 changes: 87 additions & 41 deletions test/permit/EIP2612PermitAndDeposit.test.ts
Original file line number Diff line number Diff line change
@@ -1,129 +1,175 @@
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;

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',
chainId,
verifyingContract: usdc.address,
},
{
owner: wallet.address,
owner: user,
spender: permitAndDeposit.address,
value: amount,
nonce: 0,
deadline,
},
);

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;

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');
});
Expand Down

0 comments on commit f233c6f

Please sign in to comment.