diff --git a/contracts/mainnet/connectors/pooltogether_v4/events.sol b/contracts/mainnet/connectors/pooltogether_v4/events.sol new file mode 100644 index 00000000..7f155075 --- /dev/null +++ b/contracts/mainnet/connectors/pooltogether_v4/events.sol @@ -0,0 +1,11 @@ +pragma solidity ^0.7.0; + +// import { TokenFaucetInterface } from "./interface.sol"; + +contract Events { + event LogDepositTo(address prizePool, address to, uint256 amount, uint256 getId, uint256 setId); + event LogDepositToDelegate(address prizePool, address to, uint256 amount, address delegate, uint256 getId, uint256 setId); + event LogWithdrawFrom(address prizePool, address from, uint256 amount, uint256 getId, uint256 setId); + event LogDelegated(address prizePool, address user, address to); + event LogClaim(address prizeDistributor, address user, uint32[] drawIds, bytes data, uint256 payout, uint256 setId); +} \ No newline at end of file diff --git a/contracts/mainnet/connectors/pooltogether_v4/interface.sol b/contracts/mainnet/connectors/pooltogether_v4/interface.sol new file mode 100644 index 00000000..a99ff27f --- /dev/null +++ b/contracts/mainnet/connectors/pooltogether_v4/interface.sol @@ -0,0 +1,16 @@ +pragma solidity ^0.7.0; + +interface PrizePoolInterface { + function getToken() external view returns (address); + function depositTo(address to, uint256 amount) external; + function depositToAndDelegate(address to, uint256 amount, address delegate) external; + function withdrawFrom(address from, uint256 amount) external returns (uint256); +} + +interface TicketInterface { + function delegate(address _to) external; +} + +interface PrizeDistributorInterface { + function claim(address user, uint32[] calldata drawIds, bytes calldata data) external returns (uint256); +} \ No newline at end of file diff --git a/contracts/mainnet/connectors/pooltogether_v4/main.sol b/contracts/mainnet/connectors/pooltogether_v4/main.sol new file mode 100644 index 00000000..0304f40d --- /dev/null +++ b/contracts/mainnet/connectors/pooltogether_v4/main.sol @@ -0,0 +1,193 @@ +pragma solidity ^0.7.0; + +/** + * @title PoolTogether V4 + * @dev Deposit & Withdraw from PoolTogether V4 + */ + + import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; + import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + import { PrizePoolInterface, TicketInterface, PrizeDistributorInterface } from "./interface.sol"; + +import { TokenInterface } from "../../common/interfaces.sol"; +import { Stores } from "../../common/stores.sol"; +import { Events } from "./events.sol"; +import { DSMath } from "../../common/math.sol"; +import { Basic } from "../../common/basic.sol"; + +abstract contract PoolTogetherV4Resolver is Events, DSMath, Basic { + using SafeERC20 for IERC20; + + /** + * @dev Deposit into Prize Pool without a delegate + * @notice Deposit assets into the Prize Pool in exchange for tokens + * @param prizePool PrizePool address to deposit to + * @param amount The amount of the underlying asset the user wishes to deposit. The Prize Pool contract should have been pre-approved by the caller to transfer the underlying ERC20 tokens. + * @param getId Get token amount at this ID from `InstaMemory` Contract. + * @param setId Set token amount at this ID in `InstaMemory` Contract. + */ + + function depositTo( + address prizePool, + uint256 amount, + uint256 getId, + uint256 setId + ) external payable returns ( string memory _eventName, bytes memory _eventParam) { + uint _amount = getUint(getId, amount); + + PrizePoolInterface prizePoolContract = PrizePoolInterface(prizePool); + address prizePoolToken = prizePoolContract.getToken(); + + bool isEth = prizePoolToken == wethAddr; + TokenInterface tokenContract = TokenInterface(prizePoolToken); + + if (isEth) { + _amount = _amount == uint256(-1) ? address(this).balance : _amount; + convertEthToWeth(isEth, tokenContract, _amount); + } else { + _amount = _amount == uint256(-1) ? tokenContract.balanceOf(address(this)) : _amount; + } + + // Approve prizePool + approve(tokenContract, prizePool, _amount); + + prizePoolContract.depositTo(address(this), _amount); + + setUint(setId, _amount); + + _eventName = "LogDepositTo(address, address,uint256,uint256,uint256)"; + _eventParam = abi.encode(address(prizePool), address(this), _amount, getId, setId); + } + + /** + * @dev Deposit into Prize Pool and delegate + * @notice Deposit assets into the Prize Pool in exchange for tokens, then sets the delegate on behalf of the caller. + * @param prizePool PrizePool address to deposit to + * @param amount The amount of the underlying asset the user wishes to deposit. The Prize Pool contract should have been pre-approved by the caller to transfer the underlying ERC20 tokens. + * @param delegateTo The address to delegate to for the caller + * @param getId Get token amount at this ID from `InstaMemory` Contract. + * @param setId Set token amount at this ID in `InstaMemory` Contract. + */ + + function depositToAndDelegate( + address prizePool, + uint256 amount, + address delegateTo, + uint256 getId, + uint256 setId + ) external payable returns ( string memory _eventName, bytes memory _eventParam) { + uint _amount = getUint(getId, amount); + + PrizePoolInterface prizePoolContract = PrizePoolInterface(prizePool); + address prizePoolToken = prizePoolContract.getToken(); + + bool isEth = prizePoolToken == wethAddr; + TokenInterface tokenContract = TokenInterface(prizePoolToken); + + if (isEth) { + _amount = _amount == uint256(-1) ? address(this).balance : _amount; + convertEthToWeth(isEth, tokenContract, _amount); + } else { + _amount = _amount == uint256(-1) ? tokenContract.balanceOf(address(this)) : _amount; + } + + // Approve prizePool + approve(tokenContract, prizePool, _amount); + + prizePoolContract.depositToAndDelegate(address(this), _amount,delegateTo); + + setUint(setId, _amount); + + _eventName = "LogDepositToDelegate(address, address,uint256,address,uint256,uint256)"; + _eventParam = abi.encode(address(prizePool), address(this), _amount, address(delegateTo), getId, setId); + } + + /** + * @dev Withdraw from Prize Pool + * @notice Withdraw assets from the Prize Pool instantly. + * @param prizePool PrizePool address to withdraw from + * @param amount The amount of tokens to redeem for assets. + * @param getId Get token amount at this ID from `InstaMemory` Contract. + * @param setId Set token amount at this ID in `InstaMemory` Contract. + */ + + function withdrawFrom ( + address prizePool, + uint256 amount, + uint256 getId, + uint256 setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _amount = getUint(getId, amount); + + PrizePoolInterface prizePoolContract = PrizePoolInterface(prizePool); + address prizePoolToken = prizePoolContract.getToken(); + TokenInterface tokenContract = TokenInterface(prizePoolToken); + + // TokenInterface ticketToken = TokenInterface(controlledToken); + _amount = _amount == uint256(-1) ? tokenContract.balanceOf(address(this)) : _amount; + + _amount = prizePoolContract.withdrawFrom(address(this), _amount); + + convertWethToEth(prizePoolToken == wethAddr, tokenContract, _amount); + + setUint(setId, _amount); + + _eventName = "LogWithdrawFrom(address,address,uint256,uint256,uint256)"; + _eventParam = abi.encode(address(prizePool), address(this), _amount, getId, setId); + } + + /** + * @dev Delegate time-weighted average balances to an alternative address. + * @dev Transfers (including mints) trigger the storage of a TWAB in delegate(s) account, instead of the + * targetted sender and/or recipient address(s). + * @dev To reset the delegate, pass the zero address (0x000.000) as `to` parameter. + * @notice Delegate time-weighted average balances to an alternative address. + * @param ticket Prizepool ticket address + * @param to Recipient of delegated TWAB. + */ + + function delegate ( + address ticket, + address to + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + TicketInterface ticketContract = TicketInterface(ticket); + ticketContract.delegate(to); + + _eventName = "LogDelegated(address,address,address)"; + _eventParam = abi.encode(address(ticket), address(this), address(to)); + } + + /** + * @notice Claim prize payout(s) by submitting valud drawId(s) and winning pick indice(s). The user address + is used as the "seed" phrase to generate random numbers. + * @dev The claim function is public and any wallet may execute claim on behalf of another user. + Prizes are always paid out to the designated user account and not the caller (msg.sender). + Claiming prizes is not limited to a single transaction. Reclaiming can be executed + subsequentially if an "optimal" prize was not included in previous claim pick indices. The + payout difference for the new claim is calculated during the award process and transfered to user. + * @param prizeDistributor PrizeDistributor address + * @param drawIds Draw IDs from global DrawBuffer reference + * @param data The data to pass to the draw calculator + * @param setId Set token amount at this ID in `InstaMemory` Contract. + */ + + function claim( + address prizeDistributor, + uint32[] calldata drawIds, + bytes calldata data, + uint256 setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + + PrizeDistributorInterface prizeDistributorContract = PrizeDistributorInterface(prizeDistributor); + uint256 payout = prizeDistributorContract.claim(address(this), drawIds, data); + + setUint(setId, payout); + + _eventName = "LogClaim(address,address,uint32[],bytes,uint256,uint256)"; + _eventParam = abi.encode(address(prizeDistributor), address(this), drawIds, data, payout, setId); + } +} + +contract ConnectV2PoolTogetherV4 is PoolTogetherV4Resolver { + string public constant name = "PoolTogetherV4-v1"; +} \ No newline at end of file diff --git a/test/pooltogether_v4/pooltogether.test.js b/test/pooltogether_v4/pooltogether.test.js new file mode 100644 index 00000000..7bdd5dd7 --- /dev/null +++ b/test/pooltogether_v4/pooltogether.test.js @@ -0,0 +1,471 @@ +const { expect } = require("chai"); +const hre = require("hardhat"); +const { web3, deployments, waffle, ethers, artifacts} = hre; +const { provider, deployMockContract } = waffle + +const ALCHEMY_ID = process.env.ALCHEMY_ID; + +const impersonate = require("../../scripts/impersonate.js") +const deployAndEnableConnector = require("../../scripts/deployAndEnableConnector.js") +const buildDSAv2 = require("../../scripts/buildDSAv2") +const encodeSpells = require("../../scripts/encodeSpells.js") +const getMasterSigner = require("../../scripts/getMasterSigner") + +const addresses = require("../../scripts/constant/addresses"); +const abis = require("../../scripts/constant/abis"); + +const connectV2CompoundArtifacts = require("../../artifacts/contracts/mainnet/connectors/compound/main.sol/ConnectV2Compound.json") +const connectV2PoolTogetherV4Artifacts = require("../../artifacts/contracts/mainnet/connectors/pooltogether_v4/main.sol/ConnectV2PoolTogetherV4.json") +const PrizeDistributionBuffer = require('./artifacts/PrizeDistributionBuffer.json') +const DrawBeacon = require('./artifacts/DrawBeacon.json') +const DrawBuffer = require('./artifacts/DrawBuffer.json') + +// https://www.npmjs.com/package/@pooltogether/draw-calculator-js +const {drawCalculator, Draw, PrizeDistribution, generatePicks, prepareClaims, computePicks, batchCalculateDrawResults, calculateNumberOfPicksForUser } = require("@pooltogether/draw-calculator-js") + +// Mainnet Test Addresses https://v4.docs.pooltogether.com/protocol/reference/deployments/mainnet +const PRIZE_POOL_ADDR = "0xd89a09084555a7D0ABe7B111b1f78DFEdDd638Be" // Prize Pool +const PRIZE_POOL_TOKEN_ADDR = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" // ERC20 USDC +const PRIZE_POOL_TICKET_ADDR = "0xdd4d117723C257CEe402285D3aCF218E9A8236E1" // ERC20 TICKET +const PRIZE_DISTRIBUTOR_ADDR = "0xb9a179DcA5a7bf5f8B9E088437B3A85ebB495eFe" +const PRIZE_DISTRIBUTION_BUFFER_ADDR = "0xf025a8d9E6080F885e841C8cC0E324368D7C6577" +const DRAW_BEACON_ADDR = "0x0D33612870cd9A475bBBbB7CC38fC66680dEcAC5" +const DRAW_BUFFER_ADDR = "0x78Ea5a9595279DC2F9608283875571b1151F19D4" +const DRAW_CALCULATOR_ADDR = "0x14d0675580C7255043a3AeD3726F5D7f33292730" + +const TICKET_ABI = [ + "function delegateOf(address _user) external view returns (address)", + "function getBalanceAt(address user, uint64 timestamp) external view returns (uint256)", + "function balanceOf(address user) external view returns (uint256)" +] + +const DRAW_CALCULATOR_ABI = [ + "function getPrizeDistributionBuffer() external view returns (IPrizeDistributionBuffer)", + "function getNormalizedBalancesForDrawIds(address _user, uint32[] calldata _drawIds) external view returns (uint256[] memory)" +] + +const RNGBLOCKHASH_ABI = [ + "function getRequestFee() external view returns (address feeToken, uint256 requestFee)", + "function isRequestComplete(uint32 requestId) external view returns (bool isCompleted)", + "function randomNumber(uint32 requestId) external returns (uint256 randomNum)", + "function getLastRequestId() external view returns (uint32 requestId)", + "function requestRandomNumber() external returns (uint32 requestId, uint32 lockBlock)" +] + +describe("PoolTogether", function () { + const connectorName = "COMPOUND-TEST-A" + const ptConnectorName = "POOLTOGETHERV4-TEST-A" + + let dsaWallet0 + let masterSigner; + let instaConnectorsV2; + let connector; + let ptConnector; + let rng; + + const wallets = provider.getWallets() + const [wallet0, wallet1, wallet2, wallet3] = wallets + before(async () => { + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_ID}`, + blockNumber: 13475671, + }, + }, + ], + }); + + masterSigner = await getMasterSigner(wallet3) + instaConnectorsV2 = await ethers.getContractAt(abis.core.connectorsV2, addresses.core.connectorsV2); + + // Deploy and enable Compound Connector + connector = await deployAndEnableConnector({ + connectorName, + contractArtifact: connectV2CompoundArtifacts, + signer: masterSigner, + connectors: instaConnectorsV2 + }) + + // Deploy and enable Pool Together Connector + ptConnector = await deployAndEnableConnector({ + connectorName: ptConnectorName, + contractArtifact: connectV2PoolTogetherV4Artifacts, + signer: masterSigner, + connectors: instaConnectorsV2 + }) + }) + + it("Should have contracts deployed.", async function () { + expect(!!instaConnectorsV2.address).to.be.true; + expect(!!connector.address).to.be.true; + expect(!!ptConnector.address).to.be.true; + expect(!!masterSigner.address).to.be.true; + }); + + describe("DSA wallet setup", function () { + it("Should build DSA v2", async function () { + dsaWallet0 = await buildDSAv2(wallet0.address) + expect(!!dsaWallet0.address).to.be.true; + }); + + it("Should compute winning pick from hashed dsaWallet0.address, setup Mock RNG Contract", async function() { + // First pick generated from hashed dsaWallet0.address + const hashedAddress = ethers.utils.solidityKeccak256(['address'], [dsaWallet0.address]); + const pick0 = computePicks(hashedAddress,[ethers.BigNumber.from(0)])[0]; + + // Deploy Mock RNG Contract with hard coded winning random number of dsaWallet + rng = await deployMockContract(wallet0, RNGBLOCKHASH_ABI); + await rng.mock.getRequestFee.returns(ethers.constants.AddressZero, 0); + await rng.mock.randomNumber.returns(pick0.hash); + await rng.mock.isRequestComplete.returns(true); + await rng.mock.getLastRequestId.returns(0); + await rng.mock.requestRandomNumber.returns(1, await ethers.provider.getBlockNumber()); + expect(!!rng.address).to.be.true; + }) + + it("Deposit 10 ETH into DSA wallet", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.gte(ethers.utils.parseEther("10")); + }); + }); + + describe("Main - Prize Pool Test", function () { + + it("Should deposit 5 ETH in Compound", async function () { + const amount = ethers.utils.parseEther("5") // 1 ETH + const spells = [ + { + connector: connectorName, + method: "deposit", + args: ["ETH-A", amount, 0, 0] + } + ] + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet1.address) + const receipt = await tx.wait() + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.lte(ethers.utils.parseEther("5")); + }); + + it("Should borrow 10000 USDC from Compound and deposit 1000 USDC into USDC Prize Pool without delegating (depositTo)", async function () { + const borrowAmount = ethers.utils.parseUnits("10000", 6); + const amount = ethers.utils.parseUnits("1000", 6) // 1000 USDC + const spells = [ + { + connector: connectorName, + method: "borrow", + args: ["USDC-A", borrowAmount, 0, 0] + }, + { + connector: ptConnectorName, + method: "depositTo", + args: [PRIZE_POOL_ADDR, amount, 0, 0] + } + ] + const usdcToken = await ethers.getContractAt(abis.basic.erc20, PRIZE_POOL_TOKEN_ADDR); + const ticketToken = await ethers.getContractAt(TICKET_ABI, PRIZE_POOL_TICKET_ADDR); + + // Before Spell + const balance = await usdcToken.balanceOf(dsaWallet0.address) + console.log("TokenBalanceBefore: ", balance.toString()); + expect(balance, `USDC balance is 0`).to.be.eq(ethers.utils.parseUnits("0", 6)); + + // ticket.balanceOf is users total tickets deposited + // ticket.getBalanceAt is users balance delegated to them. In this case using "depositTo", the deposit is not delegated to anyone so user is not eligibile to win. + const ticketBalanceOf = await ticketToken.balanceOf(dsaWallet0.address); + const ticketBalanceAt = await ticketToken.getBalanceAt(dsaWallet0.address, (await ethers.provider.getBlock('latest')).timestamp); + console.log("TicketBalanceOfBefore: ", ticketBalanceOf.toString()); + console.log("TicketBalanceAtBefore: ", ticketBalanceAt.toString()); + expect(ticketBalanceOf, `PoolTogether Ticket balance equals 0`).to.be.eq(ethers.utils.parseUnits("0", 6)); + expect(ticketBalanceAt, `PoolTogether Ticket eligible to win balance equals 0`).to.be.eq(ethers.utils.parseUnits("0", 6)); + + // Run spell transaction + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet1.address) + const receipt = await tx.wait() + + // After spell + // usdcBalance = await usdcToken.balanceOf(dsaWallet0.address); + const balanceAfter = await usdcToken.balanceOf(dsaWallet0.address); + console.log("TokenBalanceAfter: ", balanceAfter.toString()); + expect(balanceAfter, `Token balance equals 9000`).to.be.eq(ethers.utils.parseUnits("9000", 6)); + + const ticketBalanceOfAfter = await ticketToken.balanceOf(dsaWallet0.address); + const ticketBalanceAtAfter = await ticketToken.getBalanceAt(dsaWallet0.address, (await ethers.provider.getBlock('latest')).timestamp); + console.log("TicketBalanceOfAfter: ", ticketBalanceOfAfter.toString()); + console.log("TicketBalanceAtAfter: ", ticketBalanceAtAfter.toString()); + expect(ticketBalanceOfAfter, `PoolTogether Ticket balance equals 1000`).to.be.eq(ethers.utils.parseUnits("1000", 6)); + expect(ticketBalanceAtAfter, `PoolTogether Ticket elgible to win balance equals 0`).to.be.eq(ethers.utils.parseUnits("0", 6)); + }); + + it("Should deposit USDC into USDC Prize Pool and delegate to dsaWallet0", async function () { + const amount = ethers.utils.parseUnits("1000", 6) // 1000 USDC + const spells = [ + { + connector: ptConnectorName, + method: "depositToAndDelegate", + args: [PRIZE_POOL_ADDR, amount, dsaWallet0.address, 0, 0] + } + ] + + // Before Spell + const usdcToken = await ethers.getContractAt(abis.basic.erc20, PRIZE_POOL_TOKEN_ADDR); + const balance = await usdcToken.balanceOf(dsaWallet0.address); + console.log("TokenBalanceBefore: ", balance.toString()); + expect(balance, `USDC balance is 9000`).to.be.eq(ethers.utils.parseUnits("9000", 6)); + + const ticketToken = await ethers.getContractAt(TICKET_ABI, PRIZE_POOL_TICKET_ADDR); + const ticketBalanceOf = await ticketToken.balanceOf(dsaWallet0.address); + const ticketBalanceAt = await ticketToken.getBalanceAt(dsaWallet0.address, (await ethers.provider.getBlock('latest')).timestamp); + console.log("TicketBalanceOfBefore: ", ticketBalanceOf.toString()); + console.log("TicketBalanceAtBefore: ", ticketBalanceAt.toString()); + expect(ticketBalanceOf, `PoolTogether Ticket balance equals 1000`).to.be.eq(ethers.utils.parseUnits("1000", 6)); + expect(ticketBalanceAt, `PoolTogether Ticket elgible to win balance equals 0`).to.be.eq(ethers.utils.parseUnits("0", 6)); + + // Run spell transaction + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet1.address) + const receipt = await tx.wait() + + // After spell + const balanceAfter = await usdcToken.balanceOf(dsaWallet0.address); + console.log("TokenBalanceAfter: ", balanceAfter.toString()); + expect(balanceAfter, `Token balance equals 8000`).to.be.eq(ethers.utils.parseUnits("8000", 6)); + + const ticketBalanceOfAfter = await ticketToken.balanceOf(dsaWallet0.address); + const ticketBalanceAtAfter = await ticketToken.getBalanceAt(dsaWallet0.address, (await ethers.provider.getBlock('latest')).timestamp); + console.log("TicketBalanceOfAfter: ", ticketBalanceOfAfter.toString()); + console.log("TicketBalanceAtAfter: ", ticketBalanceAtAfter.toString()); + expect(ticketBalanceOfAfter, `PoolTogether Ticket balance equals 200`).to.be.eq(ethers.utils.parseUnits("2000", 6)); + expect(ticketBalanceAtAfter, `PoolTogether Ticket elgible to win balance equals 200`).to.be.eq(ethers.utils.parseUnits("2000", 6)); + }); + + it("Should withdraw 2000 USDC from USDC Prize Pool", async function () { + const amount = ethers.utils.parseUnits("2000", 6) // USDC + const setId = "83478237" + const spells = [ + { + connector: ptConnectorName, + method: "withdrawFrom", + args: [PRIZE_POOL_ADDR, amount, 0, 0] + } + ] + + // Before Spell + let usdcToken = await ethers.getContractAt(abis.basic.erc20, PRIZE_POOL_TOKEN_ADDR) + const balance = await usdcToken.balanceOf(dsaWallet0.address) + console.log("TokenBalanceBefore: ", balance.toString()); + expect(balance, `USDC balance is 8000`).to.be.eq(ethers.utils.parseUnits("8000", 6)); + + const ticketToken = await ethers.getContractAt(TICKET_ABI, PRIZE_POOL_TICKET_ADDR); + const ticketBalanceOf = await ticketToken.balanceOf(dsaWallet0.address); + const ticketBalanceAt = await ticketToken.getBalanceAt(dsaWallet0.address, (await ethers.provider.getBlock('latest')).timestamp); + console.log("TicketBalanceOfBefore: ", ticketBalanceOf.toString()); + console.log("TicketBalanceAtBefore: ", ticketBalanceAt.toString()); + expect(ticketBalanceOf, `PoolTogether Ticket balance equals 200`).to.be.eq(ethers.utils.parseUnits("2000", 6)); + expect(ticketBalanceAt, `PoolTogether Ticket elgible to win balance equals 200`).to.be.eq(ethers.utils.parseUnits("2000", 6)); + + // Run spell transaction + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet1.address) + const receipt = await tx.wait() + + // After spell + const balanceAfter = await usdcToken.balanceOf(dsaWallet0.address); + console.log("TokenBalanceAfter: ", balanceAfter.toString()); + expect(balanceAfter, `Token balance equals 10000`).to.be.eq(ethers.utils.parseUnits("10000", 6)); + + const ticketBalanceOfAfter = await ticketToken.balanceOf(dsaWallet0.address); + const ticketBalanceAtAfter = await ticketToken.getBalanceAt(dsaWallet0.address, (await ethers.provider.getBlock('latest')).timestamp); + console.log("TicketBalanceOfAfter: ", ticketBalanceOfAfter.toString()); + console.log("TicketBalanceAtAfter: ", ticketBalanceAtAfter.toString()); + expect(ticketBalanceAtAfter, `PoolTogether Ticket elgible to win balance equals 0`).to.be.eq(ethers.utils.parseUnits("0", 6)); + expect(ticketBalanceOfAfter, `PoolTogether Ticket balance equals 0`).to.be.eq(ethers.utils.parseUnits("0", 6)); + }); + + it("Should deposit USDC into USDC Prize Pool and delegate to dsaWallet0", async function () { + const amount = ethers.utils.parseUnits("10000", 6) // 1000 USDC + const setId = "83478237" + const spells = [ + { + connector: ptConnectorName, + method: "depositTo", + args: [PRIZE_POOL_ADDR, amount, 0, 0] + }, + { + connector: ptConnectorName, + method: "delegate", + args: [PRIZE_POOL_TICKET_ADDR, dsaWallet0.address] + } + ] + const usdcToken = await ethers.getContractAt(abis.basic.erc20, PRIZE_POOL_TOKEN_ADDR); + + // Before Spell + const balance = await usdcToken.balanceOf(dsaWallet0.address) + console.log("TokenBalanceBefore: ", balance.toString()); + expect(balance, `USDC balance is 10000`).to.be.eq(ethers.utils.parseUnits("10000", 6)); + + const ticketToken = await ethers.getContractAt(TICKET_ABI, PRIZE_POOL_TICKET_ADDR); + const ticketBalanceOf = await ticketToken.balanceOf(dsaWallet0.address); + const ticketBalanceAt = await ticketToken.getBalanceAt(dsaWallet0.address, (await ethers.provider.getBlock('latest')).timestamp); + console.log("TicketBalanceOfBefore: ", ticketBalanceOf.toString()); + console.log("TicketBalanceAtBefore: ", ticketBalanceAt.toString()); + expect(ticketBalanceAt, `PoolTogether Ticket elgible to win balance equals 0`).to.be.eq(ethers.utils.parseUnits("0", 6)); + expect(ticketBalanceOf, `PoolTogether Ticket balance equals 0`).to.be.eq(ethers.utils.parseUnits("0", 6)); + + // Run spell transaction + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet1.address) + const receipt = await tx.wait() + + // After spell + const balanceAfter = await usdcToken.balanceOf(dsaWallet0.address); + console.log("TokenBalanceAfter: ", balanceAfter.toString()); + expect(balanceAfter, `Token balance equals 0`).to.be.eq(ethers.utils.parseUnits("0", 6)); + + const ticketBalanceOfAfter = await ticketToken.balanceOf(dsaWallet0.address); + const ticketBalanceAtAfter = await ticketToken.getBalanceAt(dsaWallet0.address, (await ethers.provider.getBlock('latest')).timestamp); + console.log("TicketBalanceOfAfter: ", ticketBalanceOfAfter.toString()); + console.log("TicketBalanceAtAfter: ", ticketBalanceAtAfter.toString()); + expect(ticketBalanceOfAfter, `PoolTogether Ticket balance equals 100`).to.be.eq(ethers.utils.parseUnits("10000", 6)); + expect(ticketBalanceAtAfter, `PoolTogether Ticket elgible to win balance equals 1000`).to.be.eq(ethers.utils.parseUnits("10000", 6)); + }); + + it("Trigger draw, claim winnings and withdraw winnings to dsaWallet0", async function () { + const amount = ethers.utils.parseUnits("1000", 6) // 1000 USDC + const setId = "83478237" + + // Impersonate owner to set mock RNGService so we can set random number to always win + const OWNER_ADDR = "0x029Aa20Dcc15c022b1b61D420aaCf7f179A9C73f" + const owner = await impersonate([OWNER_ADDR]); + await wallet0.sendTransaction({ + to: OWNER_ADDR, + value: ethers.utils.parseEther("1") + }); + const drawBeaconContract = await ethers.getContractAt(DrawBeacon.abi, DRAW_BEACON_ADDR); + await drawBeaconContract.connect(owner[0]).setRngService(rng.address); + + // Get next drawId + const drawCalculatorContract = await ethers.getContractAt(DRAW_CALCULATOR_ABI, DRAW_CALCULATOR_ADDR); + const prizeDistributionBufferContract = await ethers.getContractAt(PrizeDistributionBuffer.abi, PRIZE_DISTRIBUTION_BUFFER_ADDR); + const drawId = await drawBeaconContract.getNextDrawId(); + + // Get previous prizeDistribution + // https://v4.docs.pooltogether.com/protocol/concepts/prize-distribution + const prizeDistribution = await prizeDistributionBufferContract.getPrizeDistribution(drawId-2); + console.log(prizeDistribution); + console.log("NumberofPicks:", prizeDistribution.numberOfPicks.toString()); + console.log("prize:", prizeDistribution.prize.toString()); + + console.log("\nCan Start Draw: ", await drawBeaconContract.canStartDraw()); + console.log("Can Complete Draw: ", await drawBeaconContract.canCompleteDraw()); + console.log("Beacon Period Remaining Seconds: ", (await drawBeaconContract.beaconPeriodRemainingSeconds()).toString()); + console.log("Next Draw Id: ", (await drawBeaconContract.getNextDrawId()).toString()); + + // Make sure beacon period ended by increasing time so we can startDraw + await ethers.provider.send("evm_increaseTime", [1*24*60*60]); + await ethers.provider.send("evm_mine"); + + console.log("\nIncrease Time"); + console.log("Can Start Draw: ", await drawBeaconContract.canStartDraw()); + console.log("Can Complete Draw: ", await drawBeaconContract.canCompleteDraw()); + console.log("Beacon Period Remaining Seconds: ", (await drawBeaconContract.beaconPeriodRemainingSeconds()).toString()); + + // Start Draw + await drawBeaconContract.startDraw(); + console.log("\nStart Draw"); + console.log("Can Start Draw: ", await drawBeaconContract.canStartDraw()); + console.log("Can Complete Draw: ", await drawBeaconContract.canCompleteDraw()); + console.log("Beacon Period Remaining Seconds: ", (await drawBeaconContract.beaconPeriodRemainingSeconds()).toString()); + console.log("Next Draw Id: ", drawId); + + // Complete Draw + await drawBeaconContract.completeDraw(); + console.log("\nComplete Draw"); + console.log("Can Start Draw: ", await drawBeaconContract.canStartDraw()); + console.log("Can Complete Draw: ", await drawBeaconContract.canCompleteDraw()); + console.log("Beacon Period Remaining Seconds: ", (await drawBeaconContract.beaconPeriodRemainingSeconds()).toString()); + console.log("Next Draw Id: ", (await drawBeaconContract.getNextDrawId()).toString()); + + // Push previous prizeDistribution, then latest one for draw + await prizeDistributionBufferContract.connect(owner[0]).pushPrizeDistribution(drawId-1, prizeDistribution); + await prizeDistributionBufferContract.connect(owner[0]).pushPrizeDistribution(drawId, prizeDistribution); + + // User normalized Balances, used to determine number of picks + const normalizedBalances = await drawCalculatorContract.getNormalizedBalancesForDrawIds(dsaWallet0.address,[drawId]); + console.log("\nNormalized Balances For DrawIds: ", normalizedBalances.toString()); + console.log("User number of Picks", calculateNumberOfPicksForUser(prizeDistribution, ethers.BigNumber.from(normalizedBalances.toString()))); + + const drawBufferContract = await ethers.getContractAt(DrawBuffer.abi, DRAW_BUFFER_ADDR); + const draw = await drawBufferContract.getDraw(drawId); + console.log("\nDRAW DATA"); + console.log("drawId: ", draw.drawId); + console.log("timestamp:: ", draw.timestamp.toString()); + console.log("winningRandomNumber: ", draw.winningRandomNumber); + + const user = { + address: dsaWallet0.address, + normalizedBalances + } + + // Use draw data to determine what prizes user won + const results = batchCalculateDrawResults([prizeDistribution], [draw], user) + for (let i = 0; i < results.length; i++) { + console.log("\nDraw id: ", results[i].drawId); + for (let j = 0; j < results[i].prizes.length; j++) { + console.log("\tPrize Pick", results[i].prizes[j].pick.toString()); + console.log("\t\tamount", results[i].prizes[j].amount.toString()); + console.log("\t\tindex (Tier)", results[i].prizes[j].distributionIndex); + } + console.log("TotalValue: ", results[i].totalValue.toString()); + } + + // Prepare data for claim transaction + const claim = prepareClaims(user, results); + + const spells = [ + { + connector: ptConnectorName, + method: "claim", + args: [PRIZE_DISTRIBUTOR_ADDR, claim.drawIds, claim.encodedWinningPickIndices, setId] + }, + { + connector: ptConnectorName, + method: "withdrawFrom", + args: [PRIZE_POOL_ADDR, amount, setId, 0] + } + ] + + // Before Spell + let usdcToken = await ethers.getContractAt(abis.basic.erc20, PRIZE_POOL_TOKEN_ADDR) + let balance = await usdcToken.balanceOf(dsaWallet0.address); + console.log("\nTokenBalanceBefore: ", balance.toString()); + expect(balance, `USDC balance is 0`).to.be.eq(ethers.utils.parseEther("0")); + + const ticketToken = await ethers.getContractAt(TICKET_ABI, PRIZE_POOL_TICKET_ADDR); + const ticketBalanceOf = await ticketToken.balanceOf(dsaWallet0.address); + const ticketBalanceAt = await ticketToken.getBalanceAt(dsaWallet0.address, (await ethers.provider.getBlock('latest')).timestamp); + console.log("TicketBalanceOfBefore: ", ticketBalanceOf.toString()); + console.log("TicketBalanceAtBefore: ", ticketBalanceAt.toString()); + expect(ticketBalanceOf, `PoolTogether Ticket balance equals 0`).to.be.eq(ethers.utils.parseUnits("10000", 6)); + expect(ticketBalanceAt, `PoolTogether Ticket elgible to win balance equals 0`).to.be.eq(ethers.utils.parseUnits("10000", 6)); + + // // Run spell transaction + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet1.address) + const receipt = await tx.wait() + + // After spell + const balanceAfter = await usdcToken.balanceOf(dsaWallet0.address); + console.log("TokenBalanceAfter: ", balanceAfter.toString()); + expect(balanceAfter, `Expect USDC balance be greater than 0 since withdraw some winnings`).to.be.gt(0); + + const ticketBalanceOfAfter = await ticketToken.balanceOf(dsaWallet0.address); + const ticketBalanceAtAfter = await ticketToken.getBalanceAt(dsaWallet0.address, (await ethers.provider.getBlock('latest')).timestamp); + console.log("TicketBalanceOfAfter: ", ticketBalanceOfAfter.toString()); + console.log("TicketBalanceAtAfter: ", ticketBalanceAtAfter.toString()); + expect(ticketBalanceOfAfter, `PoolTogether Ticket balance equals 100`).to.be.eq(ethers.utils.parseUnits("10000", 6)); + expect(ticketBalanceAtAfter, `PoolTogether Ticket elgible to win balance equals 100`).to.be.eq(ethers.utils.parseUnits("10000", 6)); + }); + }) +}) \ No newline at end of file