1pragma solidity >=0.0;
2import "../Oracles/Oracle.sol";
3import "../Tokens/Token.sol";
4import "../Utils/Math.sol";
5
6
7/// @title Ultimate oracle contract - Allows to swap oracle result for ultimate oracle result
8/// @author Stefan George - <stefan@gnosis.pm>
9contract UltimateOracle is Oracle {
10    using Math for *;
11
12    /*
13     *  Events
14     */
15    event ForwardedOracleOutcomeAssignment(int outcome);
16    event OutcomeChallenge(address indexed sender, int outcome);
17    event OutcomeVote(address indexed sender, int outcome, uint amount);
18    event Withdrawal(address indexed sender, uint amount);
19
20    /*
21     *  Storage
22     */
23    Oracle public forwardedOracle;
24    Token public collateralToken;
25    uint8 public spreadMultiplier;
26    uint public challengePeriod;
27    uint public challengeAmount;
28    uint public frontRunnerPeriod;
29
30    int public forwardedOutcome;
31    uint public forwardedOutcomeSetTimestamp;
32    int public frontRunner;
33    uint public frontRunnerSetTimestamp;
34
35    uint public totalAmount;
36    mapping (int => uint) public totalOutcomeAmounts;
37    mapping (address => mapping (int => uint)) public outcomeAmounts;
38
39    /*
40     *  Public functions
41     */
42    /// @dev Constructor sets ultimate oracle properties
43    /// @param _forwardedOracle Oracle address
44    /// @param _collateralToken Collateral token address
45    /// @param _spreadMultiplier Defines the spread as a multiple of the money bet on other outcomes
46    /// @param _challengePeriod Time to challenge oracle outcome
47    /// @param _challengeAmount Amount to challenge the outcome
48    /// @param _frontRunnerPeriod Time to overbid the front-runner
49    constructor(
50        Oracle _forwardedOracle,
51        Token _collateralToken,
52        uint8 _spreadMultiplier,
53        uint _challengePeriod,
54        uint _challengeAmount,
55        uint _frontRunnerPeriod
56    )
57    {
58        // Validate inputs
59        require(   address(_forwardedOracle) != address(0)
60                && address(_collateralToken) != address(0)
61                && _spreadMultiplier >= 2
62                && _challengePeriod > 0
63                && _challengeAmount > 0
64                && _frontRunnerPeriod > 0);
65        forwardedOracle = _forwardedOracle;
66        collateralToken = _collateralToken;
67        spreadMultiplier = _spreadMultiplier;
68        challengePeriod = _challengePeriod;
69        challengeAmount = _challengeAmount;
70        frontRunnerPeriod = _frontRunnerPeriod;
71    }
72
73    /// @dev Allows to set oracle outcome
74    function setForwardedOutcome()
75        public
76    {
77        // There was no challenge and the outcome was not set yet in the ultimate oracle but in the forwarded oracle
78        require(   !isChallenged()
79                && forwardedOutcomeSetTimestamp == 0
80                && forwardedOracle.isOutcomeSet());
81        forwardedOutcome = forwardedOracle.getOutcome();
82        forwardedOutcomeSetTimestamp = block.timestamp;
83        emit ForwardedOracleOutcomeAssignment(forwardedOutcome);
84    }
85
86    /// @dev Allows to challenge the oracle outcome
87    /// @param _outcome Outcome to bid on
88    function challengeOutcome(int _outcome)
89        public
90    {
91        // There was no challenge yet or the challenge period expired
92        require(   !isChallenged()
93                && !isChallengePeriodOver()
94                && collateralToken.transferFrom(msg.sender, address(this), challengeAmount));
95        outcomeAmounts[msg.sender][_outcome] = challengeAmount;
96        totalOutcomeAmounts[_outcome] = challengeAmount;
97        totalAmount = challengeAmount;
98        frontRunner = _outcome;
99        frontRunnerSetTimestamp = block.timestamp;
100        emit OutcomeChallenge(msg.sender, _outcome);
101    }
102
103    /// @dev Allows to challenge the oracle outcome
104    /// @param _outcome Outcome to bid on
105    /// @param amount Amount to bid
106    function voteForOutcome(int _outcome, uint amount)
107        public
108    {
109        uint maxAmount = (totalAmount - totalOutcomeAmounts[_outcome]).mul(spreadMultiplier);
110        if (amount > maxAmount)
111            amount = maxAmount;
112        // Outcome is challenged and front runner period is not over yet and tokens can be transferred
113        require(   isChallenged()
114                && !isFrontRunnerPeriodOver()
115                && collateralToken.transferFrom(msg.sender, address(this), amount));
116        outcomeAmounts[msg.sender][_outcome] = outcomeAmounts[msg.sender][_outcome].add(amount);
117        totalOutcomeAmounts[_outcome] = totalOutcomeAmounts[_outcome].add(amount);
118        totalAmount = totalAmount.add(amount);
119        if (_outcome != frontRunner && totalOutcomeAmounts[_outcome] > totalOutcomeAmounts[frontRunner])
120        {
121            frontRunner = _outcome;
122            frontRunnerSetTimestamp = block.timestamp;
123        }
124        emit OutcomeVote(msg.sender, _outcome, amount);
125    }
126
127    /// @dev Withdraws winnings for user
128    /// @return amount Winnings
129    function withdraw()
130        public
131        returns (uint amount)
132    {
133        // Outcome was challenged and ultimate outcome decided
134        require(isFrontRunnerPeriodOver());
135        amount = totalAmount.mul(outcomeAmounts[msg.sender][frontRunner]) / totalOutcomeAmounts[frontRunner];
136        outcomeAmounts[msg.sender][frontRunner] = 0;
137        // Transfer earnings to contributor
138        require(collateralToken.transfer(msg.sender, amount));
139        emit Withdrawal(msg.sender, amount);
140    }
141
142    /// @dev Checks if time to challenge the outcome is over
143    /// @return Is challenge period over?
144    function isChallengePeriodOver()
145        public
146        view
147        returns (bool)
148    {
149        return forwardedOutcomeSetTimestamp != 0 && (block.timestamp).sub(forwardedOutcomeSetTimestamp) > challengePeriod;
150    }
151
152    /// @dev Checks if time to overbid the front runner is over
153    /// @return Is front runner period over?
154    function isFrontRunnerPeriodOver()
155        public
156        view
157        returns (bool)
158    {
159        return frontRunnerSetTimestamp != 0 && (block.timestamp).sub(frontRunnerSetTimestamp) > frontRunnerPeriod;
160    }
161
162    /// @dev Checks if outcome was challenged
163    /// @return Is challenged?
164    function isChallenged()
165        public
166        view
167        returns (bool)
168    {
169        return frontRunnerSetTimestamp != 0;
170    }
171
172    /// @dev Returns if winning outcome is set
173    /// @return Is outcome set?
174    function isOutcomeSet()
175        public
176        override
177        view
178        returns (bool)
179    {
180        return    isChallengePeriodOver() && !isChallenged()
181               || isFrontRunnerPeriodOver();
182    }
183
184    /// @dev Returns winning outcome
185    /// @return Outcome
186    function getOutcome()
187        public
188        override
189        view
190        returns (int)
191    {
192        if (isFrontRunnerPeriodOver())
193            return frontRunner;
194        return forwardedOutcome;
195    }
196}
197