Skip to content

Commit

Permalink
feat(EIP2612): add permitAndDepositToAndDelegate
Browse files Browse the repository at this point in the history
  • Loading branch information
PierrickGT committed Nov 1, 2021
1 parent 3092ea0 commit d6d13e7
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 59 deletions.
84 changes: 64 additions & 20 deletions contracts/permit/EIP2612PermitAndDeposit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
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
125 changes: 92 additions & 33 deletions test/permit/EIP2612PermitAndDeposit.test.ts
Original file line number Diff line number Diff line change
@@ -1,129 +1,188 @@
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;
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',
chainId,
verifyingContract: usdc.address,
},
{
owner: wallet.address,
owner: user,
spender: permitAndDeposit.address,
value: amount,
nonce: 0,
deadline,
},
);

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;

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

0 comments on commit d6d13e7

Please sign in to comment.