Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dai pool #145

Merged
merged 1 commit into from
May 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion contracts/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma experimental ABIEncoderV2;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./libraries/ProxyDeltas.sol";
import "./libraries/ReceiverWeights.sol";
import "./TestDai.sol";

/// @notice Funding pool contract. Automatically sends funds to a configurable set of receivers.
///
Expand Down Expand Up @@ -863,7 +864,7 @@ contract Erc20Pool is Pool {
uint128 amtPerSec,
ReceiverWeight[] calldata updatedReceivers,
ReceiverWeight[] calldata updatedProxies
) public payable {
) public {
transferToContract(topUpAmt);
uint128 withdrawn =
updateSenderInternal(topUpAmt, withdraw, amtPerSec, updatedReceivers, updatedProxies);
Expand All @@ -878,3 +879,35 @@ contract Erc20Pool is Pool {
if (amt != 0) erc20.transfer(msg.sender, amt);
}
}

/// @notice Funding pool contract for DAI token.
/// See the base `Pool` contract docs for more details.
contract DaiPool is Erc20Pool {
// solhint-disable no-empty-blocks
/// @notice See `Erc20Pool` constructor documentation for more details.
constructor(uint64 cycleSecs, Dai dai) Erc20Pool(cycleSecs, dai) {}

/// @notice Updates all the sender parameters of the sender of the message
/// and permits spending sender's Dai by the pool.
/// This function is an extension of `updateSender`, see its documentation for more details.
///
/// The sender must sign a Dai permission document allowing the pool to spend their funds.
/// The document's `nonce` and `expiry` must be passed here along the parts of its signature.
/// These parameters will be passed to the Dai contract by this function.
function updateSenderAndPermit(
uint128 topUpAmt,
uint128 withdraw,
uint128 amtPerSec,
ReceiverWeight[] calldata updatedReceivers,
ReceiverWeight[] calldata updatedProxies,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) public {
Dai dai = Dai(address(erc20));
dai.permit(msg.sender, address(this), nonce, expiry, true, v, r, s);
updateSender(topUpAmt, withdraw, amtPerSec, updatedReceivers, updatedProxies);
}
}
55 changes: 55 additions & 0 deletions contracts/TestDai.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-3.0-only

pragma solidity ^0.7.5;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Dai is ERC20 {
bytes32 private immutable domainSeparator;
bytes32 private immutable typehash;
mapping(address => uint256) public nonces;

constructor() ERC20("DAI Stablecoin", "DAI") {
// TODO replace with `block.chainid` after upgrade to Solidity 0.8
uint256 chainId;
// solhint-disable no-inline-assembly
assembly {
chainId := chainid()
}
domainSeparator = keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256(bytes(name())),
keccak256(bytes("1")),
chainId,
address(this)
)
);
typehash = keccak256(
"Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)"
);
_mint(msg.sender, 10**9 * 10**18); // 1 billion DAI, 18 decimals
}

function permit(
address holder,
address spender,
uint256 nonce,
uint256 expiry,
bool allowed,
uint8 v,
bytes32 r,
bytes32 s
) external {
bytes32 message = keccak256(abi.encode(typehash, holder, spender, nonce, expiry, allowed));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, message));
address signer = ecrecover(digest, v, r, s);
require(holder == signer, "Invalid signature");
require(nonce == nonces[holder]++, "Invalid nonce");
require(expiry == 0 || expiry > block.timestamp, "Signature expired");
uint256 amount = allowed ? type(uint256).max : 0;
_approve(holder, spender, amount);
}
}
22 changes: 21 additions & 1 deletion src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
Signer,
} from "ethers";
import { Claims } from "../contract-bindings/ethers/Claims";
import { Dai } from "../contract-bindings/ethers/Dai";
import { DaiPool } from "../contract-bindings/ethers/DaiPool";
import { ENS } from "../contract-bindings/ethers/ENS";
import { EthPool } from "../contract-bindings/ethers/EthPool";
import { Exchange } from "../contract-bindings/ethers/Exchange";
Expand All @@ -22,6 +24,8 @@ import { VestingToken } from "../contract-bindings/ethers/VestingToken";
import {
BaseRegistrarImplementation__factory,
Claims__factory,
Dai__factory,
DaiPool__factory,
ENSRegistry__factory,
Erc20Pool__factory,
Erc20Pool,
Expand Down Expand Up @@ -57,17 +61,20 @@ export async function nextDeployedContractAddr(
export interface DeployedContracts {
gov: Governor;
rad: RadicleToken;
dai: Dai;
registrar: Registrar;
exchange: Exchange;
ens: ENS;
ethPool: EthPool;
erc20Pool: Erc20Pool;
daiPool: DaiPool;
claims: Claims;
}

export async function deployAll(signer: Signer): Promise<DeployedContracts> {
const signerAddr = await signer.getAddress();
const rad = await deployRadicleToken(signer, signerAddr);
const dai = await deployTestDai(signer);
const timelock = await deployTimelock(signer, signerAddr, 2 * 60 * 60 * 24);
const gov = await deployGovernance(signer, timelock.address, rad.address, signerAddr);
const exchange = await deployExchange(rad, signer);
Expand All @@ -85,9 +92,10 @@ export async function deployAll(signer: Signer): Promise<DeployedContracts> {
await transferEthDomain(ens, label, registrar.address);
const ethPool = await deployEthPool(signer, 10);
const erc20Pool = await deployErc20Pool(signer, 10, rad.address);
const daiPool = await deployDaiPool(signer, 10, dai.address);
const claims = await deployClaims(signer);

return { gov, rad, exchange, registrar, ens, ethPool, erc20Pool, claims };
return { gov, rad, dai, exchange, registrar, ens, ethPool, erc20Pool, daiPool, claims };
}

export async function deployRadicleToken(signer: Signer, account: string): Promise<RadicleToken> {
Expand Down Expand Up @@ -252,6 +260,14 @@ export async function deployErc20Pool(
return deployOk(new Erc20Pool__factory(signer).deploy(cycleSecs, erc20TokenAddress));
}

export async function deployDaiPool(
signer: Signer,
cycleSecs: number,
daiAddress: string
): Promise<DaiPool> {
return deployOk(new DaiPool__factory(signer).deploy(cycleSecs, daiAddress));
}

// The signer becomes an owner of the '', 'eth' and '<label>.eth' domains,
// the owner of the root ENS and the owner and controller of the 'eth' registrar
export async function deployTestEns(signer: Signer, label: string): Promise<ENS> {
Expand Down Expand Up @@ -293,6 +309,10 @@ export async function deployClaims(signer: Signer): Promise<Claims> {
return deployOk(new Claims__factory(signer).deploy());
}

export async function deployTestDai(signer: Signer): Promise<Dai> {
return deployOk(new Dai__factory(signer).deploy());
}

async function deployOk<T extends BaseContract>(contractPromise: Promise<T>): Promise<T> {
const contract = await contractPromise;
await contract.deployed();
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { deployAll, DeployedContracts } from "./deploy";
export { daiPermitDigest } from "./utils";
30 changes: 30 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { utils, BigNumberish } from "ethers";

export function daiPermitDigest(
daiContract: string,
chainId: BigNumberish,
holder: string,
spender: string,
nonce: BigNumberish,
expiry: BigNumberish,
allowed: boolean
): Uint8Array {
const domain = {
name: "DAI Stablecoin",
version: "1",
chainId,
verifyingContract: daiContract,
};
const types = {
Permit: [
{ name: "holder", type: "address" },
{ name: "spender", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "expiry", type: "uint256" },
{ name: "allowed", type: "bool" },
],
};
const value = { holder, spender, nonce, expiry, allowed };
const hash = utils._TypedDataEncoder.hash(domain, types, value);
return utils.arrayify(hash);
}
114 changes: 97 additions & 17 deletions test/pool.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import {
Erc20Pool__factory,
EthPool__factory,
RadicleToken__factory,
} from "../contract-bindings/ethers";
import { Dai } from "../contract-bindings/ethers/Dai";
import { IERC20 } from "../contract-bindings/ethers/IERC20";
import { DaiPool } from "../contract-bindings/ethers/DaiPool";
import { Erc20Pool } from "../contract-bindings/ethers/Erc20Pool";
import { EthPool } from "../contract-bindings/ethers/EthPool";
import { ethers } from "hardhat";
import { Signer, BigNumber, BigNumberish, ContractReceipt, ContractTransaction } from "ethers";
import {
utils,
Signer,
BigNumber,
BigNumberish,
ContractReceipt,
ContractTransaction,
} from "ethers";
import { expect } from "chai";
import {
callOnNextBlock,
elapseTime,
elapseTimeUntil,
expectBigNumberEq,
getSigningKey,
randomAddress,
submit,
submitFailing,
} from "./support";
import { deployDaiPool, deployErc20Pool, deployEthPool, deployTestDai } from "../src/deploy";
import { daiPermitDigest } from "../src/utils";

const CYCLE_SECS = 10;

Expand All @@ -29,7 +36,7 @@ async function elapseTimeUntilCycleEnd(): Promise<void> {
await elapseTimeUntil(Math.ceil((latestBlock.timestamp + 2) / CYCLE_SECS) * CYCLE_SECS - 1);
}

type AnyPool = EthPool | Erc20Pool;
type AnyPool = EthPool | Erc20Pool | DaiPool;

type ReceiverWeights = [PoolUser<AnyPool>, number][];
type ReceiverWeightsAddr = Array<{
Expand Down Expand Up @@ -532,8 +539,7 @@ abstract class PoolUser<Pool extends AnyPool> {

async function getEthPoolUsers(): Promise<EthPoolUser[]> {
const signers = await ethers.getSigners();
const pool = await new EthPool__factory(signers[0]).deploy(CYCLE_SECS);
await pool.deployed();
const pool = await deployEthPool(signers[0], CYCLE_SECS);
const constants = await poolConstants(pool);
const poolSigners = signers.map(
async (signer: Signer) => await EthPoolUser.new(pool, signer, constants)
Expand Down Expand Up @@ -572,14 +578,8 @@ class EthPoolUser extends PoolUser<EthPool> {

async function getErc20PoolUsers(): Promise<Erc20PoolUser[]> {
const signers = await ethers.getSigners();
const signer0 = signers[0];
const signer0Addr = await signer0.getAddress();

const erc20 = await new RadicleToken__factory(signer0).deploy(signer0Addr);
await erc20.deployed();

const pool = await new Erc20Pool__factory(signer0).deploy(CYCLE_SECS, erc20.address);
await pool.deployed();
const erc20 = await deployTestDai(signers[0]);
const pool = await deployErc20Pool(signers[0], CYCLE_SECS, erc20.address);
const constants = await poolConstants(pool);

const supplyPerUser = (await erc20.totalSupply()).div(signers.length);
Expand Down Expand Up @@ -629,6 +629,79 @@ class Erc20PoolUser extends PoolUser<Erc20Pool> {
}
}

async function getDaiPoolUsers(): Promise<DaiPoolUser[]> {
const signers = await ethers.getSigners();
const dai = await deployTestDai(signers[0]);
const pool = await deployDaiPool(signers[0], CYCLE_SECS, dai.address);
const constants = await poolConstants(pool);

const supplyPerUser = (await dai.totalSupply()).div(signers.length);
const users = [];
for (const signer of signers) {
const user = await DaiPoolUser.new(pool, dai, signer, constants);
await dai.transfer(user.addr, supplyPerUser);
users.push(user);
}
return users;
}

class DaiPoolUser extends Erc20PoolUser {
dai: Dai;
daiPool: DaiPool;

constructor(daiPool: DaiPool, userAddr: string, constants: PoolConstants, dai: Dai) {
super(daiPool, userAddr, constants, dai);
this.dai = dai;
this.daiPool = daiPool;
}

static async new(
pool: DaiPool,
dai: Dai,
signer: Signer,
constants: PoolConstants
): Promise<DaiPoolUser> {
const userPool = pool.connect(signer);
const userAddr = await signer.getAddress();
const userDai = dai.connect(signer);
return new DaiPoolUser(userPool, userAddr, constants, userDai);
}

async submitUpdateSender(
topUp: BigNumberish,
withdraw: BigNumberish,
amtPerSec: BigNumberish,
setReceivers: ReceiverWeightsAddr,
setProxies: ReceiverWeightsAddr
): Promise<ContractTransaction> {
const nonce = await this.dai.nonces(this.addr);
const expiry = 0; // never expires
const digest = daiPermitDigest(
this.dai.address,
await this.pool.signer.getChainId(),
this.addr, // holder
this.pool.address, // spender
nonce,
expiry,
true // allowed
);
const signature = getSigningKey(this.addr).signDigest(digest);
const { r, s, v } = utils.splitSignature(signature);
return this.daiPool.updateSenderAndPermit(
topUp,
withdraw,
amtPerSec,
setReceivers,
setProxies,
nonce,
expiry,
v,
r,
s
);
}
}

describe("EthPool", function () {
runCommonPoolTests(getEthPoolUsers);

Expand Down Expand Up @@ -1305,6 +1378,13 @@ describe("Erc20Pool", function () {
});
});

describe("DaiPool", function () {
it("Allows sender update", async function () {
const [sender, receiver] = await getDaiPoolUsers();
await sender.updateSender(0, 10, 10, [[receiver, 1]], []);
});
});

function runCommonPoolTests(getPoolUsers: () => Promise<PoolUser<AnyPool>[]>): void {
it("Allows full sender update with top up", async function () {
const [sender, proxy, receiver1, receiver2] = await getPoolUsers();
Expand Down