SportCrypt Smart Contract
This repository contains the smart contract and associated testing code for the SportCrypt prediction market.
Please read our whitepaper for detailed design details.
SportCrypt.solThe contract source code.
scripts/Scripts for deploying the contract, generating keys, and interacting with it manually.
t/Tests and associated testing code.
solc solidity compiler somewhere in your path (
0.4.18 or later).
Make sure you have a recent
node installed (tested with version
Install npm dependencies:
Compile and test:
The contract uses a custom test harness library found in
t/lib/testlib.js. Each test starts a new instance of testrpc, creates a contract, adds admins, and then runs various actions specified in the test such as
finalize. Several of these actions take callbacks so that parameters can be modified (ie to provide bad signatures). There is a special
assert action that allows us to hand-calculate values in the test and ensure the contract computed them correctly.
After every action, a set of invariants are verified by querying the contract. We refer to these as the "white-box" invariant checks. They ensure that the contract is always in a consistent state. Some of the invariants are described in the Rounding Behaviour section below.
t/interface.js: Iterates over all methods in the ABI, ensures there are no unexpected non-constant functions, and that only
t/trade.js: Exercises the "happy-path" where trading suceeds. Position re-sale, position direction reversal, and the other various corner cases of the trade amount and finalization logic is tested.
t/depositWithdraw.js: Makes sure funding works as expected: Balance is changed, attempting to withdraw more than balance results in withdrawing full balance and no more. Makes sure that funds are returned if you try to send eth to contract directly.
t/orderCancel.js: Orders are cancelled and cannot be traded upon after, in both new order and partially-filled order cases.
t/orderExpiry.js: Orders cannot be traded on after they expire.
t/tradeError.js: Tests all the trade error statuses described below, including attempting to fake signatures (both by passing bad signature and changing order parameters).
t/finalization.js: Verifies finalized matches cannot be traded on, finalization signatures can't be faked, that the signing address is actually an admin, and that rounding loss behaves as expected when finalization price is not 0 or 100 (see Rounding Behaviour section below).
t/fundsRecovery.js: Verifies fund recovery can't be started prior to timeout, that funds can be succesfully recovered without finalization message after this time period, and that cancellation price is honored.
t/fuzz/fuzz.pl: Chooses random trade prices/directions and executes them, trying to trigger the invariant assertions in the contract, or find violations with the white-box invariant checks in the test library. Because this test is non-deterministic, and also because it is an infinite loop, it is not run by default.
The test-suite also tracks how much gas is consumed by each method and prints a short summary after each test run that lists the maximum, minimum, median, and average usage for each method.
Smart Contract Interface
This list of methods represent the entire non-constant interface to the contract. This is verified by the
changeOwner: Set a new owner.
addAdmin: Adds an address that is allowed to sign event finalization messages.
removeAdmin: Revokes the privileges added with
deposit: Adds funds to the sender's balance inside the smart contract. This is the only payable method (and that is verified by
withdraw: Deducts the sender's smart contract balance and sends it to their address with
transfer. This method attempts to withdraw the maximum up to the specified amount. If the balance is less than this amount, the entire balance will be withdrawn.
cancelOrder: Given a signed order, sets the
filledAmountsvalue for this order to the order amount, ensuring that this order can no longer be traded on. The order must be signed by the sender. If the order is already expired, saves gas by not writing to
trade: Given a maximum trade amount and a signed order, attempts to create a trade with the amount at risk up to the specified amount. This will debit or credit the balances, and increase or decrease positions on the event specified in the order. See our whitepaper for full details. In the event of an error, a
LogTradeErrorwill be logged (see the Trade Status section below).
claim: Given a signed finalization message and a match ID, finalizes the match and claims the sender's winnings for this match by decreasing the sender's position and crediting the sender's balance. This is an irreversible operation. After finalization, the signed finalization message is no longer required for claims.
recoverFunds: Given a match ID, if a match has not been finalized and its funds recovery timeout period has elapsed, finalizes the match at its cancellation price.
LogTradeError log message contains a field called
status which indicates why a trade was not made as the result of a
trade method call. This is an enum with the following values:
OK(0): Trade was created successfully.
MATCH_FINALIZED(1): Cannot trade after a match has been finalized.
ORDER_EXPIRED(2): The block this transaction was mined in has a
block.timestampgreater than the order's expiry.
ORDER_MALFORMED(3): The order was constructed incorrectly.
ORDER_BAD_SIG(4): The order's signature does not match the signature of the order creator.
AMOUNT_MALFORMED(5): An amount greater than
SELF_TRADE(6): The sender and the order creator are the same account.
ZERO_VALUE_TRADE(7): The trade amount was so small that, possibly due to rounding, one of the amounts at risk was zero.
Orders are packed into 3
uint256 values. This prevents us from exceeding Solidity's stack size limitation. The layout of the order array is as follows:
// : match hash // : amount // : 5-byte expiry, 5-byte nonce, 1-byte price, 1-byte direction, 20-byte address
Please see the whitepaper for details on these fields.
The contract is designed to be wei-exact except in one case (described below).
After determining the amounts at risk for each party to a new trade, the contract verifies that the following invariants hold:
- After adjusting positions, the sum of all the positions on a match must be 0.
- The net of the change in balances must be the negative of the change in exposure for the match.
The exposure is the amount that will be claimable when the match finalizes. Due to invariant 1, this can be calculated as the sum of all positive positions on the match.
If invariant 2 does not hold because it is off-by-one, then rounding has occurred when calculating the balance deltas. If the exposure is one more than it should be, then the position delta is reduced by 1. If the exposure is one less than it should be, the extra wei is arbitrarily added to the balance of the party creating the long-side of the trade. In any other case, an assertion is triggered.
After applying this rounding compensation, the invariants are rechecked.
In our test-suite, these invariants are checked after every operation using a white-box view into the contract. As well as carefully chosen test values, we also have an amount fuzzer that exercises these invariant checks.
When a match is finalized at 0 or 100, then the winning positions will be transferred to the winners' balances, and the losing positions will (if ever claimed) be 0, so will not affect balances.
As mentioned above, there is one case where wei can be lost to the contract as dust. If a match is finalized at a finalization price other than 0 or 100, then any claimed amount will be rounded down to the nearest wei. This is necessary because positions can be divided up and sold to any number of participants.
For any comments or questions about this smart contract, please file a github issue.