Skip to content

Commit

Permalink
Moved ticket chance code into separate Ticket contract
Browse files Browse the repository at this point in the history
  • Loading branch information
asselstine authored and robsecord committed Sep 9, 2020
1 parent 2d557b8 commit 9dc3bf6
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 135 deletions.
19 changes: 17 additions & 2 deletions contracts/builders/CompoundPrizePoolBuilder.sol
Expand Up @@ -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";

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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;
}
}
48 changes: 2 additions & 46 deletions contracts/prize-strategy/PrizeStrategy.sol
Expand Up @@ -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";
Expand All @@ -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(
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)));
}
}

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 2 additions & 4 deletions contracts/prize-strategy/PrizeStrategyStorage.sol
Expand Up @@ -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";

Expand All @@ -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;
Expand Down
78 changes: 78 additions & 0 deletions 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)));
}
}

}
8 changes: 8 additions & 0 deletions 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);
}
25 changes: 25 additions & 0 deletions 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), ""));
}
}

0 comments on commit 9dc3bf6

Please sign in to comment.