diff --git a/contracts/builders/CompoundPrizePoolBuilder.sol b/contracts/builders/CompoundPrizePoolBuilder.sol index ccd70093..3eaab686 100644 --- a/contracts/builders/CompoundPrizePoolBuilder.sol +++ b/contracts/builders/CompoundPrizePoolBuilder.sol @@ -5,6 +5,7 @@ import "../comptroller/ComptrollerInterface.sol"; import "../prize-strategy/PrizeStrategyProxyFactory.sol"; import "../prize-pool/compound/CompoundPrizePoolProxyFactory.sol"; import "../token/ControlledTokenProxyFactory.sol"; +import "../token/TicketProxyFactory.sol"; import "../external/compound/CTokenInterface.sol"; import "../external/openzeppelin/OpenZeppelinProxyFactoryInterface.sol"; @@ -39,6 +40,7 @@ contract CompoundPrizePoolBuilder { ComptrollerInterface public comptroller; CompoundPrizePoolProxyFactory public compoundPrizePoolProxyFactory; ControlledTokenProxyFactory public controlledTokenProxyFactory; + TicketProxyFactory public ticketProxyFactory; PrizeStrategyProxyFactory public prizeStrategyProxyFactory; OpenZeppelinProxyFactoryInterface public proxyFactory; address public trustedForwarder; @@ -49,14 +51,17 @@ contract CompoundPrizePoolBuilder { address _trustedForwarder, CompoundPrizePoolProxyFactory _compoundPrizePoolProxyFactory, ControlledTokenProxyFactory _controlledTokenProxyFactory, - OpenZeppelinProxyFactoryInterface _proxyFactory + OpenZeppelinProxyFactoryInterface _proxyFactory, + TicketProxyFactory _ticketProxyFactory ) public { require(address(_comptroller) != address(0), "CompoundPrizePoolBuilder/comptroller-not-zero"); require(address(_prizeStrategyProxyFactory) != address(0), "CompoundPrizePoolBuilder/prize-strategy-factory-not-zero"); require(address(_compoundPrizePoolProxyFactory) != address(0), "CompoundPrizePoolBuilder/compound-prize-pool-builder-not-zero"); require(address(_controlledTokenProxyFactory) != address(0), "CompoundPrizePoolBuilder/controlled-token-proxy-factory-not-zero"); require(address(_proxyFactory) != address(0), "CompoundPrizePoolBuilder/proxy-factory-not-zero"); + require(address(_ticketProxyFactory) != address(0), "CompoundPrizePoolBuilder/ticket-proxy-factory-not-zero"); proxyFactory = _proxyFactory; + ticketProxyFactory = _ticketProxyFactory; comptroller = _comptroller; prizeStrategyProxyFactory = _prizeStrategyProxyFactory; trustedForwarder = _trustedForwarder; @@ -124,7 +129,7 @@ contract CompoundPrizePoolBuilder { ) internal returns (CompoundPrizePool prizePool, address[] memory tokens) { prizePool = compoundPrizePoolProxyFactory.create(); tokens = new address[](2); - tokens[0] = address(createControlledToken(prizePool, ticketName, ticketSymbol)); + tokens[0] = address(createTicket(prizePool, ticketName, ticketSymbol)); tokens[1] = address(createControlledToken(prizePool, sponsorshipName, sponsorshipSymbol)); prizePool.initialize( trustedForwarder, @@ -145,4 +150,14 @@ contract CompoundPrizePoolBuilder { token.initialize(string(name), string(symbol), trustedForwarder, controller); return token; } + + function createTicket( + TokenControllerInterface controller, + string memory name, + string memory symbol + ) internal returns (Ticket) { + Ticket ticket = ticketProxyFactory.create(); + ticket.initialize(string(name), string(symbol), trustedForwarder, controller); + return ticket; + } } diff --git a/contracts/prize-strategy/PrizeStrategy.sol b/contracts/prize-strategy/PrizeStrategy.sol index 2543cb25..c970e043 100644 --- a/contracts/prize-strategy/PrizeStrategy.sol +++ b/contracts/prize-strategy/PrizeStrategy.sol @@ -7,8 +7,6 @@ import "@openzeppelin/contracts-ethereum-package/contracts/introspection/IERC182 import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts-ethereum-package/contracts/utils/ReentrancyGuard.sol"; import "@pooltogether/fixed-point/contracts/FixedPoint.sol"; -import "sortition-sum-tree-factory/contracts/SortitionSumTreeFactory.sol"; -import "@pooltogether/uniform-random-number/contracts/UniformRandomNumber.sol"; import "./PrizeStrategyStorage.sol"; import "./PrizeStrategyInterface.sol"; @@ -28,11 +26,8 @@ contract PrizeStrategy is PrizeStrategyStorage, using SafeMath for uint256; using SafeCast for uint256; - using SortitionSumTreeFactory for SortitionSumTreeFactory.SortitionSumTrees; using MappedSinglyLinkedList for MappedSinglyLinkedList.Mapping; - bytes32 constant private TREE_KEY = keccak256("PoolTogether/Ticket"); - uint256 constant private MAX_TREE_LEAVES = 5; uint256 internal constant ETHEREUM_BLOCK_TIME_ESTIMATE_MANTISSA = 13.4 ether; event PrizePoolOpened( @@ -76,7 +71,7 @@ contract PrizeStrategy is PrizeStrategyStorage, require(address(_sponsorship) != address(0), "PrizeStrategy/sponsorship-not-zero"); require(address(_rng) != address(0), "PrizeStrategy/rng-not-zero"); prizePool = _prizePool; - ticket = IERC20(_ticket); + ticket = TicketInterface(_ticket); rng = _rng; sponsorship = IERC20(_sponsorship); trustedForwarder = _trustedForwarder; @@ -94,33 +89,12 @@ contract PrizeStrategy is PrizeStrategyStorage, prizePeriodSeconds = _prizePeriodSeconds; prizePeriodStartedAt = _prizePeriodStart; - sortitionSumTrees.createTree(TREE_KEY, MAX_TREE_LEAVES); externalErc721s.initialize(); emit PrizePoolOpened(_msgSender(), prizePeriodStartedAt); } - /// @notice Returns the user's chance of winning. - function chanceOf(address user) external view returns (uint256) { - return sortitionSumTrees.stakeOf(TREE_KEY, bytes32(uint256(user))); - } - - /// @notice Selects a user using a random number. The random number will be uniformly bounded to the ticket totalSupply. - /// @param randomNumber The random number to use to select a user. - /// @return The winner - function draw(uint256 randomNumber) public view returns (address) { - uint256 bound = ticket.totalSupply(); - address selected; - if (bound == 0) { - selected = address(0); - } else { - uint256 token = UniformRandomNumber.uniform(randomNumber, bound); - selected = address(uint256(sortitionSumTrees.draw(TREE_KEY, token))); - } - return selected; - } - /// @notice Calculates and returns the currently accrued prize /// @return The current prize size function currentPrize() public returns (uint256) { @@ -223,8 +197,6 @@ contract PrizeStrategy is PrizeStrategyStorage, /// @param amount The amount of interest to mint as tickets. function _awardTickets(address user, uint256 amount) internal { prizePool.award(user, amount, address(ticket)); - uint256 userBalance = ticket.balanceOf(user); - sortitionSumTrees.set(TREE_KEY, userBalance.add(amount), bytes32(uint256(user))); } /// @notice Awards all external tokens with non-zero balances to the given user. The external tokens must be held by the PrizePool contract. @@ -288,12 +260,6 @@ contract PrizeStrategy is PrizeStrategyStorage, function beforeTokenTransfer(address from, address to, uint256 amount, address controlledToken) external override onlyPrizePool { if (controlledToken == address(ticket)) { _requireNotLocked(); - - uint256 fromBalance = ticket.balanceOf(from).sub(amount); - sortitionSumTrees.set(TREE_KEY, fromBalance, bytes32(uint256(from))); - - uint256 toBalance = ticket.balanceOf(to).add(amount); - sortitionSumTrees.set(TREE_KEY, toBalance, bytes32(uint256(to))); } } @@ -349,10 +315,6 @@ contract PrizeStrategy is PrizeStrategyStorage, } comptroller.afterDepositTo(to, amount, balance, totalSupply, controlledToken, referrer); - - if (controlledToken == address(ticket)) { - sortitionSumTrees.set(TREE_KEY, balance, bytes32(uint256(to))); - } } /// @notice Called by the prize pool after a withdrawal with timelock has been made. @@ -371,9 +333,6 @@ contract PrizeStrategy is PrizeStrategyStorage, { uint256 balance = IERC20(controlledToken).balanceOf(from); comptroller.afterWithdrawFrom(from, amount, balance, IERC20(controlledToken).totalSupply(), controlledToken); - if (controlledToken == address(ticket)) { - sortitionSumTrees.set(TREE_KEY, balance, bytes32(uint256(from))); - } } /// @notice Called by the prize pool after a user withdraws collateral instantly @@ -394,9 +353,6 @@ contract PrizeStrategy is PrizeStrategyStorage, { uint256 balance = IERC20(controlledToken).balanceOf(from); comptroller.afterWithdrawFrom(from, amount, balance, IERC20(controlledToken).totalSupply(), controlledToken); - if (controlledToken == address(ticket)) { - sortitionSumTrees.set(TREE_KEY, balance, bytes32(uint256(from))); - } } /// @notice Called by the prize pool after a timelocked withdrawal has been swept @@ -446,7 +402,7 @@ contract PrizeStrategy is PrizeStrategyStorage, _awardSponsorship(address(comptroller), reserveFee); } - address winner = draw(randomNumber); + address winner = ticket.draw(randomNumber); if (winner != address(0)) { _awardTickets(winner, prize); _awardAllExternalTokens(winner); diff --git a/contracts/prize-strategy/PrizeStrategyStorage.sol b/contracts/prize-strategy/PrizeStrategyStorage.sol index 1158f931..a1ca2ddc 100644 --- a/contracts/prize-strategy/PrizeStrategyStorage.sol +++ b/contracts/prize-strategy/PrizeStrategyStorage.sol @@ -7,6 +7,7 @@ import "../comptroller/ComptrollerInterface.sol"; import "../utils/MappedSinglyLinkedList.sol"; import "../token/TokenControllerInterface.sol"; import "../token/ControlledToken.sol"; +import "../token/TicketInterface.sol"; import "../prize-pool/PrizePool.sol"; import "../Constants.sol"; @@ -19,16 +20,13 @@ contract PrizeStrategyStorage { // Contract Interfaces PrizePool public prizePool; ComptrollerInterface public comptroller; - IERC20 public ticket; + TicketInterface public ticket; IERC20 public sponsorship; RNGInterface public rng; // Current RNG Request RngRequest internal rngRequest; - // EOA mapping for ticket-weighted odds - SortitionSumTreeFactory.SortitionSumTrees internal sortitionSumTrees; - // Prize period uint256 public prizePeriodSeconds; uint256 public prizePeriodStartedAt; diff --git a/contracts/token/Ticket.sol b/contracts/token/Ticket.sol new file mode 100644 index 00000000..4758fa16 --- /dev/null +++ b/contracts/token/Ticket.sol @@ -0,0 +1,78 @@ +pragma solidity 0.6.4; + +import "sortition-sum-tree-factory/contracts/SortitionSumTreeFactory.sol"; +import "@pooltogether/uniform-random-number/contracts/UniformRandomNumber.sol"; + +import "./ControlledToken.sol"; +import "./TicketInterface.sol"; + +contract Ticket is ControlledToken, TicketInterface { + using SortitionSumTreeFactory for SortitionSumTreeFactory.SortitionSumTrees; + + bytes32 constant private TREE_KEY = keccak256("PoolTogether/Ticket"); + uint256 constant private MAX_TREE_LEAVES = 5; + + // Ticket-weighted odds + SortitionSumTreeFactory.SortitionSumTrees internal sortitionSumTrees; + + /// @notice Initializes the Controlled Token with Token Details and the Controller + /// @param _name The name of the Token + /// @param _symbol The symbol for the Token + /// @param _trustedForwarder Address of the Forwarding Contract for GSN Meta-Txs + /// @param _controller Address of the Controller contract for minting & burning + function initialize( + string memory _name, + string memory _symbol, + address _trustedForwarder, + TokenControllerInterface _controller + ) + public + virtual + override + initializer + { + super.initialize(_name, _symbol, _trustedForwarder, _controller); + sortitionSumTrees.createTree(TREE_KEY, MAX_TREE_LEAVES); + } + + /// @notice Returns the user's chance of winning. + function chanceOf(address user) external view returns (uint256) { + return sortitionSumTrees.stakeOf(TREE_KEY, bytes32(uint256(user))); + } + + /// @notice Selects a user using a random number. The random number will be uniformly bounded to the ticket totalSupply. + /// @param randomNumber The random number to use to select a user. + /// @return The winner + function draw(uint256 randomNumber) public view override returns (address) { + uint256 bound = totalSupply(); + address selected; + if (bound == 0) { + selected = address(0); + } else { + uint256 token = UniformRandomNumber.uniform(randomNumber, bound); + selected = address(uint256(sortitionSumTrees.draw(TREE_KEY, token))); + } + return selected; + } + + /// @dev Controller hook to provide notifications & rule validations on token transfers to the controller. + /// This includes minting and burning. + /// May be overridden to provide more granular control over operator-burning + /// @param from Address of the account sending the tokens (address(0x0) on minting) + /// @param to Address of the account receiving the tokens (address(0x0) on burning) + /// @param amount Amount of tokens being transferred + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override { + super._beforeTokenTransfer(from, to, amount); + + if (from != address(0)) { + uint256 fromBalance = balanceOf(from).sub(amount); + sortitionSumTrees.set(TREE_KEY, fromBalance, bytes32(uint256(from))); + } + + if (to != address(0)) { + uint256 toBalance = balanceOf(to).add(amount); + sortitionSumTrees.set(TREE_KEY, toBalance, bytes32(uint256(to))); + } + } + +} \ No newline at end of file diff --git a/contracts/token/TicketInterface.sol b/contracts/token/TicketInterface.sol new file mode 100644 index 00000000..440e4533 --- /dev/null +++ b/contracts/token/TicketInterface.sol @@ -0,0 +1,8 @@ +pragma solidity 0.6.4; + +interface TicketInterface { + /// @notice Selects a user using a random number. The random number will be uniformly bounded to the ticket totalSupply. + /// @param randomNumber The random number to use to select a user. + /// @return The winner + function draw(uint256 randomNumber) external view returns (address); +} \ No newline at end of file diff --git a/contracts/token/TicketProxyFactory.sol b/contracts/token/TicketProxyFactory.sol new file mode 100644 index 00000000..1c71ad78 --- /dev/null +++ b/contracts/token/TicketProxyFactory.sol @@ -0,0 +1,25 @@ +pragma solidity 0.6.4; + +import "@openzeppelin/contracts-ethereum-package/contracts/Initializable.sol"; + +import "./Ticket.sol"; +import "../external/openzeppelin/ProxyFactory.sol"; + +/// @title Controlled ERC20 Token Factory +/// @notice Minimal proxy pattern for creating new Controlled ERC20 Tokens +contract TicketProxyFactory is ProxyFactory { + + /// @notice Contract template for deploying proxied tokens + Ticket public instance; + + /// @notice Initializes the Factory with an instance of the Controlled ERC20 Token + constructor () public { + instance = new Ticket(); + } + + /// @notice Creates a new Controlled ERC20 Token as a proxy of the template instance + /// @return A reference to the new proxied Controlled ERC20 Token + function create() external returns (Ticket) { + return Ticket(deployMinimal(address(instance), "")); + } +} diff --git a/deploy/deploy.js b/deploy/deploy.js index 7e3e4d29..73e2a2b7 100644 --- a/deploy/deploy.js +++ b/deploy/deploy.js @@ -13,6 +13,8 @@ const CTokenMock = require('../build/CTokenMock.json') // return solcOutput.contracts[contractPath].metadata // } +const debug = require('debug')('ptv3:deploy.js') + const chainName = (chainId) => { switch(chainId) { case 1: return 'Mainnet'; @@ -27,7 +29,7 @@ const chainName = (chainId) => { module.exports = async (buidler) => { const { getNamedAccounts, deployments, getChainId, ethers } = buidler - const { deploy, getOrNull, save, log } = deployments + const { deploy, getOrNull, save } = deployments let { deployer, rng, @@ -39,25 +41,23 @@ module.exports = async (buidler) => { let usingSignerAsAdmin = false const signer = await ethers.provider.getSigner(deployer) - // Run with CLI flag --silent to suppress log output - - log("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") - log("PoolTogether Pool Contracts - Deploy Script") - log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n") + debug("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + debug("PoolTogether Pool Contracts - Deploy Script") + debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n") const locus = isLocal ? 'local' : 'remote' - log(` Deploying to Network: ${chainName(chainId)} (${locus})`) + debug(` Deploying to Network: ${chainName(chainId)} (${locus})`) if (!adminAccount) { - log(" Using deployer as adminAccount;") + debug(" Using deployer as adminAccount;") adminAccount = signer._address usingSignerAsAdmin = true } - log("\n adminAccount: ", adminAccount) + debug("\n adminAccount: ", adminAccount) await deploy1820(signer) - log("\n Deploying ProxyAdmin...") + debug("\n Deploying ProxyAdmin...") const proxyAdminResult = await deploy("ProxyAdmin", { contract: ProxyAdmin, from: deployer, @@ -66,11 +66,11 @@ module.exports = async (buidler) => { const proxyAdmin = new ethers.Contract(proxyAdminResult.address, ProxyAdmin.abi, signer) if (await proxyAdmin.isOwner() && !usingSignerAsAdmin) { - log(`Transferring ProxyAdmin ownership to ${adminAccount}...`) + debug(`Transferring ProxyAdmin ownership to ${adminAccount}...`) await proxyAdmin.transferOwnership(adminAccount) } - log("\n Deploying ProxyFactory...") + debug("\n Deploying ProxyFactory...") const proxyFactoryResult = await deploy("ProxyFactory", { contract: ProxyFactory, from: deployer, @@ -79,27 +79,28 @@ module.exports = async (buidler) => { const proxyFactory = new ethers.Contract(proxyFactoryResult.address, ProxyFactory.abi, signer) if (isLocal) { - log("\n Deploying TrustedForwarder...") + debug("\n Deploying TrustedForwarder...") const deployResult = await deploy("TrustedForwarder", { from: deployer, skipIfAlreadyDeployed: true }); trustedForwarder = deployResult.address - log("\n Deploying RNGService...") + debug("\n Deploying RNGService...") const rngServiceMockResult = await deploy("RNGServiceMock", { from: deployer, skipIfAlreadyDeployed: true }) + rng = rngServiceMockResult.address - log("\n Deploying Dai...") + debug("\n Deploying Dai...") const daiResult = await deploy("Dai", { contract: ERC20Mintable, from: deployer, skipIfAlreadyDeployed: true }) - log("\n Deploying cDai...") + debug("\n Deploying cDai...") // should be about 20% APR let supplyRate = '8888888888888' await deploy("cDai", { @@ -113,10 +114,10 @@ module.exports = async (buidler) => { }) // Display Contract Addresses - log("\n Local Contract Deployments;\n") - log(" - TrustedForwarder: ", trustedForwarder) - log(" - RNGService: ", rng) - log(" - Dai: ", daiResult.address) + debug("\n Local Contract Deployments;\n") + debug(" - TrustedForwarder: ", trustedForwarder) + debug(" - RNGService: ", rng) + debug(" - Dai: ", daiResult.address) } const comptrollerImplementationResult = await deploy("ComptrollerImplementation", { @@ -128,7 +129,7 @@ module.exports = async (buidler) => { let comptrollerAddress const comptrollerDeployment = await getOrNull("Comptroller") if (!comptrollerDeployment) { - log("\n Deploying new Comptroller Proxy...") + debug("\n Deploying new Comptroller Proxy...") const salt = ethers.utils.hexlify(ethers.utils.randomBytes(32)) // form initialize() data @@ -149,25 +150,31 @@ module.exports = async (buidler) => { comptrollerAddress = comptrollerDeployment.address } - log("\n Deploying CompoundPrizePoolProxyFactory...") + debug("\n Deploying CompoundPrizePoolProxyFactory...") const compoundPrizePoolProxyFactoryResult = await deploy("CompoundPrizePoolProxyFactory", { from: deployer, skipIfAlreadyDeployed: true }) - log("\n Deploying ControlledTokenProxyFactory...") + debug("\n Deploying ControlledTokenProxyFactory...") const controlledTokenProxyFactoryResult = await deploy("ControlledTokenProxyFactory", { from: deployer, skipIfAlreadyDeployed: true }) - log("\n Deploying PrizeStrategyProxyFactory...") + debug("\n Deploying TicketProxyFactory...") + const ticketProxyFactoryResult = await deploy("TicketProxyFactory", { + from: deployer, + skipIfAlreadyDeployed: true + }) + + debug("\n Deploying PrizeStrategyProxyFactory...") const prizeStrategyProxyFactoryResult = await deploy("PrizeStrategyProxyFactory", { from: deployer, skipIfAlreadyDeployed: true }) - log("\n Deploying CompoundPrizePoolBuilder...") + debug("\n Deploying CompoundPrizePoolBuilder...") const compoundPrizePoolBuilderResult = await deploy("CompoundPrizePoolBuilder", { args: [ comptrollerAddress, @@ -175,21 +182,23 @@ module.exports = async (buidler) => { trustedForwarder, compoundPrizePoolProxyFactoryResult.address, controlledTokenProxyFactoryResult.address, - proxyFactoryResult.address + proxyFactoryResult.address, + ticketProxyFactoryResult.address ], from: deployer, skipIfAlreadyDeployed: true }) // Display Contract Addresses - log("\n Contract Deployments Complete!\n") - log(" - ProxyFactory: ", proxyFactoryResult.address) - log(" - ComptrollerImplementation: ", comptrollerImplementationResult.address) - log(" - Comptroller: ", comptrollerAddress) - log(" - CompoundPrizePoolProxyFactory: ", compoundPrizePoolProxyFactoryResult.address) - log(" - ControlledTokenProxyFactory: ", controlledTokenProxyFactoryResult.address) - log(" - PrizeStrategyProxyFactory: ", prizeStrategyProxyFactoryResult.address) - log(" - CompoundPrizePoolBuilder: ", compoundPrizePoolBuilderResult.address) - - log("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n") + debug("\n Contract Deployments Complete!\n") + debug(" - ProxyFactory: ", proxyFactoryResult.address) + debug(" - TicketProxyFactory: ", ticketProxyFactoryResult.address) + debug(" - ComptrollerImplementation: ", comptrollerImplementationResult.address) + debug(" - Comptroller: ", comptrollerAddress) + debug(" - CompoundPrizePoolProxyFactory: ", compoundPrizePoolProxyFactoryResult.address) + debug(" - ControlledTokenProxyFactory: ", controlledTokenProxyFactoryResult.address) + debug(" - PrizeStrategyProxyFactory: ", prizeStrategyProxyFactoryResult.address) + debug(" - CompoundPrizePoolBuilder: ", compoundPrizePoolBuilderResult.address) + + debug("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n") }; diff --git a/js/deployTestPool.js b/js/deployTestPool.js index 0a2fdecd..48df8b28 100644 --- a/js/deployTestPool.js +++ b/js/deployTestPool.js @@ -3,6 +3,7 @@ const RNGServiceMock = require('../build/RNGServiceMock.json') const TrustedForwarder = require('../build/TrustedForwarder.json') const ComptrollerHarness = require('../build/ComptrollerHarness.json') const ControlledToken = require('../build/ControlledToken.json') +const Ticket = require('../build/Ticket.json') const CompoundPrizePoolHarness = require('../build/CompoundPrizePoolHarness.json') const CTokenMock = require('../build/CTokenMock.json') const ERC20Mintable = require('../build/ERC20Mintable.json') @@ -65,7 +66,7 @@ async function deployTestPool({ debug('Deploying Ticket...') - let ticket = await deployContract(wallet, ControlledToken, [], overrides) + let ticket = await deployContract(wallet, Ticket, [], overrides) await ticket.initialize("Ticket", "TICK", forwarder.address, compoundPrizePool.address) debug('Initializing CompoundPrizePoolHarness...') diff --git a/test/CompoundPrizePoolBuilder.test.js b/test/CompoundPrizePoolBuilder.test.js index 2ba62540..98317026 100644 --- a/test/CompoundPrizePoolBuilder.test.js +++ b/test/CompoundPrizePoolBuilder.test.js @@ -15,10 +15,10 @@ describe('CompoundPrizePoolBuilder', () => { let comptroller, trustedForwarder, prizeStrategyProxyFactory, + proxyFactory, compoundPrizePoolProxyFactory, controlledTokenProxyFactory, rngServiceMock, - proxyFactory, cToken beforeEach(async () => { @@ -30,24 +30,26 @@ describe('CompoundPrizePoolBuilder', () => { wallet ) - comptroller = (await deployments.get("Comptroller")).address - trustedForwarder = (await deployments.get("TrustedForwarder")).address - prizeStrategyProxyFactory = (await deployments.get("PrizeStrategyProxyFactory")).address - compoundPrizePoolProxyFactory = (await deployments.get("CompoundPrizePoolProxyFactory")).address - controlledTokenProxyFactory = (await deployments.get("ControlledTokenProxyFactory")).address - rngServiceMock = (await deployments.get("RNGServiceMock")).address - proxyFactory = (await deployments.get("ProxyFactory")).address - cToken = (await deployments.get("cDai")).address + comptroller = (await deployments.get("Comptroller")) + trustedForwarder = (await deployments.get("TrustedForwarder")) + prizeStrategyProxyFactory = (await deployments.get("PrizeStrategyProxyFactory")) + compoundPrizePoolProxyFactory = (await deployments.get("CompoundPrizePoolProxyFactory")) + controlledTokenProxyFactory = (await deployments.get("ControlledTokenProxyFactory")) + ticketProxyFactory = (await deployments.get("TicketProxyFactory")) + proxyFactory = (await deployments.get("ProxyFactory")) + rngServiceMock = (await deployments.get("RNGServiceMock")) + cToken = (await deployments.get("cDai")) }) describe('initialize()', () => { it('should setup all factories', async () => { - expect(await builder.comptroller()).to.equal(comptroller) - expect(await builder.prizeStrategyProxyFactory()).to.equal(prizeStrategyProxyFactory) - expect(await builder.trustedForwarder()).to.equal(trustedForwarder) - expect(await builder.compoundPrizePoolProxyFactory()).to.equal(compoundPrizePoolProxyFactory) - expect(await builder.controlledTokenProxyFactory()).to.equal(controlledTokenProxyFactory) - expect(await builder.proxyFactory()).to.equal(proxyFactory) + expect(await builder.comptroller()).to.equal(comptroller.address) + expect(await builder.prizeStrategyProxyFactory()).to.equal(prizeStrategyProxyFactory.address) + expect(await builder.trustedForwarder()).to.equal(trustedForwarder.address) + expect(await builder.compoundPrizePoolProxyFactory()).to.equal(compoundPrizePoolProxyFactory.address) + expect(await builder.controlledTokenProxyFactory()).to.equal(controlledTokenProxyFactory.address) + expect(await builder.proxyFactory()).to.equal(proxyFactory.address) + expect(await builder.ticketProxyFactory()).to.equal(ticketProxyFactory.address) }) }) @@ -56,8 +58,8 @@ describe('CompoundPrizePoolBuilder', () => { const proxyAdmin = (await deployments.get("ProxyAdmin")).address const config = { proxyAdmin, - cToken: cToken, - rngService: rngServiceMock, + cToken: cToken.address, + rngService: rngServiceMock.address, prizePeriodStart: 0, prizePeriodSeconds: 10, ticketName: "Ticket", @@ -86,8 +88,8 @@ describe('CompoundPrizePoolBuilder', () => { it('should create a new prize strategy and pool', async () => { const config = { proxyAdmin: AddressZero, - cToken: cToken, - rngService: rngServiceMock, + cToken: cToken.address, + rngService: rngServiceMock.address, prizePeriodStart: 0, prizePeriodSeconds: 10, ticketName: "Ticket", @@ -112,7 +114,7 @@ describe('CompoundPrizePoolBuilder', () => { expect(await prizePool.cToken()).to.equal(config.cToken) expect(await prizeStrategy.prizePeriodSeconds()).to.equal(config.prizePeriodSeconds) - let ticket = await buidler.ethers.getContractAt('ControlledToken', await prizeStrategy.ticket(), wallet) + let ticket = await buidler.ethers.getContractAt('Ticket', await prizeStrategy.ticket(), wallet) expect(await ticket.name()).to.equal(config.ticketName) expect(await ticket.symbol()).to.equal(config.ticketSymbol) diff --git a/test/PrizeStrategy.test.js b/test/PrizeStrategy.test.js index df579793..e62ce9ac 100644 --- a/test/PrizeStrategy.test.js +++ b/test/PrizeStrategy.test.js @@ -2,13 +2,14 @@ const { deployContract } = require('ethereum-waffle') const { deployMockContract } = require('./helpers/deployMockContract') const { call } = require('./helpers/call') const { deploy1820 } = require('deploy-eip-1820') -const ComptrollerInterface = require('../build/ComptrollerInterface.json') +const ComptrollerInterface = require('../build/ComptrollerInterface.json') const PrizeStrategyHarness = require('../build/PrizeStrategyHarness.json') const PrizePool = require('../build/PrizePool.json') const RNGInterface = require('../build/RNGInterface.json') const IERC20 = require('../build/IERC20.json') const IERC721 = require('../build/IERC721.json') const ControlledToken = require('../build/ControlledToken.json') +const Ticket = require('../build/Ticket.json') const { expect } = require('chai') const buidler = require('@nomiclabs/buidler') @@ -54,7 +55,7 @@ describe('PrizeStrategy', function() { debug('mocking tokens...') token = await deployMockContract(wallet, IERC20.abi, overrides) prizePool = await deployMockContract(wallet, PrizePool.abi, overrides) - ticket = await deployMockContract(wallet, ControlledToken.abi, overrides) + ticket = await deployMockContract(wallet, Ticket.abi, overrides) sponsorship = await deployMockContract(wallet, ControlledToken.abi, overrides) rng = await deployMockContract(wallet, RNGInterface.abi, overrides) rngFeeToken = await deployMockContract(wallet, IERC20.abi, overrides) @@ -69,6 +70,9 @@ describe('PrizeStrategy', function() { await prizePool.mock.canAwardExternal.withArgs(externalERC20Award.address).returns(true) await prizePool.mock.canAwardExternal.withArgs(externalERC721Award.address).returns(true) + // wallet 1 always wins + await ticket.mock.draw.returns(wallet._address) + debug('initializing prizeStrategy...') await prizeStrategy.initialize( FORWARDER, @@ -262,26 +266,6 @@ describe('PrizeStrategy', function() { }) }) - describe('chanceOf()', () => { - it('should show the odds for a user to win the prize', async () => { - const amount = toWei('10') - await ticket.mock.balanceOf.withArgs(wallet._address).returns(amount) - await ticket.mock.totalSupply.returns(amount) - await comptroller.mock.afterDepositTo - .withArgs(wallet._address, amount, amount, amount, ticket.address, AddressZero) - .returns() - await prizePool.call(prizeStrategy, 'afterDepositTo', wallet._address, amount, ticket.address, []) - expect(await prizeStrategy.chanceOf(wallet._address)).to.be.equal(amount) - }) - }) - - describe('draw()', () => { - it('should return zero for an empty prize pool', async () => { - await ticket.mock.totalSupply.returns(0) - expect(await prizeStrategy.draw(123)).to.be.equal(AddressZero) - }) - }) - describe('afterDepositTo()', () => { it('should only be called by the prize pool', async () => { prizeStrategy2 = await prizeStrategy.connect(wallet2) @@ -294,7 +278,6 @@ describe('PrizeStrategy', function() { await ticket.mock.balanceOf.withArgs(wallet._address).returns(toWei('22')) await comptroller.mock.afterDepositTo.returns() await prizePool.call(prizeStrategy, 'afterDepositTo', wallet._address, toWei('10'), ticket.address, []) - expect(await prizeStrategy.draw(1)).to.equal(wallet._address) // they exist in the sortition sum tree }) it('should not be called if an rng request is in flight', async () => { @@ -534,9 +517,6 @@ describe('PrizeStrategy', function() { // have the mock update the number of prize tickets await prizePool.call(prizeStrategy, 'afterDepositTo', wallet._address, toWei('10'), ticket.address, []); - // confirm odds of winning - expect(await prizeStrategy.chanceOf(wallet._address)).to.equal(toWei('10')) - // ensure prize period is over await prizeStrategy.setCurrentTime(await prizeStrategy.prizePeriodEndAt()); diff --git a/test/Ticket.test.js b/test/Ticket.test.js new file mode 100644 index 00000000..3d3f8fc1 --- /dev/null +++ b/test/Ticket.test.js @@ -0,0 +1,52 @@ +const { deployContract } = require('ethereum-waffle') +const { deployMockContract } = require('./helpers/deployMockContract') +const TokenControllerInterface = require('../build/TokenControllerInterface.json') +const Ticket = require('../build/Ticket.json') + +const { expect } = require('chai') +const buidler = require('@nomiclabs/buidler') +const { AddressZero } = require('ethers').constants + +const debug = require('debug')('ptv3:Ticket.test') +const toWei = (val) => ethers.utils.parseEther('' + val) +let overrides = { gasLimit: 20000000 } + +describe('Ticket', function() { + + let ticket + + let controller + + beforeEach(async () => { + [wallet, wallet2, wallet3, wallet4] = await buidler.ethers.getSigners() + controller = await deployMockContract(wallet, TokenControllerInterface.abi) + ticket = await deployContract(wallet, Ticket, [], overrides) + await ticket.initialize("Name", "SYMBOL", AddressZero, controller.address) + + // allow all transfers + await controller.mock.beforeTokenTransfer.returns() + }) + + describe('chanceOf()', () => { + it('be correct after minting', async () => { + await controller.call(ticket, 'controllerMint', wallet._address, toWei('100')) + expect(await ticket.chanceOf(wallet._address)).to.equal(toWei('100')) + }) + + it('should be correct after transfer', async () => { + await controller.call(ticket, 'controllerMint', wallet._address, toWei('100')) + + await ticket.transfer(wallet3._address, toWei('20')) + + expect(await ticket.chanceOf(wallet._address)).to.equal(toWei('80')) + expect(await ticket.chanceOf(wallet3._address)).to.equal(toWei('20')) + }) + + it('should be correct after burning', async () => { + await controller.call(ticket, 'controllerMint', wallet._address, toWei('100')) + await controller.call(ticket, 'controllerBurn', wallet._address, toWei('33')) + expect(await ticket.chanceOf(wallet._address)).to.equal(toWei('67')) + }) + }) + +}); diff --git a/test/TicketProxyFactory.test.js b/test/TicketProxyFactory.test.js new file mode 100644 index 00000000..916067a0 --- /dev/null +++ b/test/TicketProxyFactory.test.js @@ -0,0 +1,44 @@ +const { expect } = require("chai"); +const TicketProxyFactory = require('../build/TicketProxyFactory.json') +const TokenControllerInterface = require('../build/TokenControllerInterface.json') +const buidler = require('@nomiclabs/buidler') +const { deployContract, deployMockContract } = require('ethereum-waffle') + +let overrides = { gasLimit: 20000000 } + +describe('TicketProxyFactory', () => { + + let wallet, wallet2 + + let controller + + let provider + + beforeEach(async () => { + [wallet, wallet2] = await buidler.ethers.getSigners() + provider = buidler.ethers.provider + + factory = await deployContract(wallet, TicketProxyFactory, [], overrides) + controller = await deployMockContract(wallet, TokenControllerInterface.abi) + }) + + describe('create()', () => { + it('should create a new prize pool', async () => { + let tx = await factory.create(overrides) + let receipt = await provider.getTransactionReceipt(tx.hash) + let event = factory.interface.parseLog(receipt.logs[0]) + expect(event.name).to.equal('ProxyCreated') + + const ticket = await buidler.ethers.getContractAt("Ticket", event.args.proxy, wallet) + + await ticket.initialize( + "NAME", + "SYMBOL", + wallet._address, // forwarder + controller.address, // controller + ) + + expect(await ticket.controller()).to.equal(controller.address) + }) + }) +})