From 5611ced768d432e618bb5e1c3f344dabeb03249a Mon Sep 17 00:00:00 2001 From: CodeSandwich Date: Mon, 17 May 2021 10:21:04 +0200 Subject: [PATCH] Add dai pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Igor Żuk --- contracts/Pool.sol | 35 ++++++++++++- contracts/TestDai.sol | 55 ++++++++++++++++++++ src/deploy.ts | 22 +++++++- src/index.ts | 1 + src/utils.ts | 30 +++++++++++ test/pool.test.ts | 114 +++++++++++++++++++++++++++++++++++------- 6 files changed, 238 insertions(+), 19 deletions(-) create mode 100644 contracts/TestDai.sol create mode 100644 src/utils.ts diff --git a/contracts/Pool.sol b/contracts/Pool.sol index f6d7871..37f1ef0 100644 --- a/contracts/Pool.sol +++ b/contracts/Pool.sol @@ -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. /// @@ -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); @@ -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); + } +} diff --git a/contracts/TestDai.sol b/contracts/TestDai.sol new file mode 100644 index 0000000..1d91b00 --- /dev/null +++ b/contracts/TestDai.sol @@ -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); + } +} diff --git a/src/deploy.ts b/src/deploy.ts index c032ff7..28480b2 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -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"; @@ -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, @@ -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 { 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); @@ -85,9 +92,10 @@ export async function deployAll(signer: Signer): Promise { 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 { @@ -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 { + return deployOk(new DaiPool__factory(signer).deploy(cycleSecs, daiAddress)); +} + // The signer becomes an owner of the '', 'eth' and '