Created
October 29, 2023 22:36
-
-
Save transmissions11/7f0439e137070d000d2068d10c157d6c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity 0.8.13; | |
import "solmate/auth/Owned.sol"; | |
import "solmate/utils/SafeCastLib.sol"; | |
import "solmate/utils/ReentrancyGuard.sol"; | |
import "./utils/SignedWadMath.sol"; | |
import "./cars/Car.sol"; | |
/// @title 0xMonaco: On-Chain Racing Game | |
/// @author transmissions11 <[email protected]> | |
/// @author Bobby Abbott <[email protected]> | |
/// @author Sina Sabet <[email protected]> | |
/// @dev Note: While 0xMonaco was originally written to be played as part | |
/// of the Paradigm CTF, it's not intended to have any hidden vulnerabilities. | |
contract Monaco is ReentrancyGuard, Owned(msg.sender) { | |
using SafeCastLib for uint256; | |
/*////////////////////////////////////////////////////////////// | |
EVENTS | |
//////////////////////////////////////////////////////////////*/ | |
event TurnCompleted(uint256 indexed turn, CarData[] cars, uint256 acceleratePrice, uint256 shellPrice); | |
event Shelled(uint256 indexed turn, Car indexed smoker, Car indexed smoked, uint256 amount, uint256 cost); | |
event Accelerated(uint256 indexed turn, Car indexed car, uint256 amount, uint256 cost); | |
event Registered(uint256 indexed turn, Car indexed car); | |
event Dub(uint256 indexed turn, Car indexed winner); | |
/*////////////////////////////////////////////////////////////// | |
MISCELLANEOUS CONSTANTS | |
//////////////////////////////////////////////////////////////*/ | |
uint256 internal constant FINISH_DISTANCE = 1000; | |
uint256 internal constant PLAYERS_REQUIRED = 3; | |
uint32 internal constant POST_SHELL_SPEED = 1; | |
uint32 internal constant STARTING_BALANCE = 15000; | |
/*////////////////////////////////////////////////////////////// | |
PRICING CONSTANTS | |
//////////////////////////////////////////////////////////////*/ | |
int256 internal constant SHELL_TARGET_PRICE = 200e18; | |
int256 internal constant SHELL_PER_TURN_DECREASE = 0.33e18; | |
int256 internal constant SHELL_SELL_PER_TURN = 0.2e18; | |
int256 internal constant ACCELERATE_TARGET_PRICE = 10e18; | |
int256 internal constant ACCELERATE_PER_TURN_DECREASE = 0.33e18; | |
int256 internal constant ACCELERATE_SELL_PER_TURN = 2e18; | |
/*////////////////////////////////////////////////////////////// | |
GAME STATE | |
//////////////////////////////////////////////////////////////*/ | |
enum State { | |
WAITING, | |
ACTIVE, | |
DONE | |
} | |
State public state; // The current state of the game: pre-start, started, done. | |
uint16 public turns = 1; // Number of turns played since the game started. | |
uint72 public entropy; // Random data used to shuffle the list of cars. | |
Car public currentCar; // The car that is currently taking its turn. | |
/*////////////////////////////////////////////////////////////// | |
SALES STATE | |
//////////////////////////////////////////////////////////////*/ | |
enum ActionType { | |
ACCELERATE, | |
SHELL | |
} | |
mapping(ActionType => uint256) public getActionsSold; | |
/*////////////////////////////////////////////////////////////// | |
CAR STORAGE | |
//////////////////////////////////////////////////////////////*/ | |
struct CarData { | |
uint32 balance; // Where 0 means the car has no money. | |
uint32 speed; // Where 0 means the car isn't moving. | |
uint32 y; // Where 0 means the car hasn't moved. | |
Car car; | |
} | |
Car[] public cars; | |
mapping(Car => CarData) public getCarData; | |
/*////////////////////////////////////////////////////////////// | |
SETUP | |
//////////////////////////////////////////////////////////////*/ | |
function register(Car car) external onlyOwner { | |
// Prevent accidentally or intentionally registering a car multiple times. | |
require(address(getCarData[car].car) == address(0), "DOUBLE_REGISTER"); | |
// Register the caller as a car in the race. | |
getCarData[car] = CarData({balance: STARTING_BALANCE, car: car, speed: 0, y: 0}); | |
cars.push(car); // Append to the list of cars. | |
// Retrieve and cache the total number of cars. | |
uint256 totalCars = cars.length; | |
// If the game is now full, kick things off. | |
if (totalCars == PLAYERS_REQUIRED) { | |
// Use the timestamp as random input. | |
entropy = uint72(block.timestamp); | |
// Mark the game as active. | |
state = State.ACTIVE; | |
} else require(totalCars < PLAYERS_REQUIRED, "MAX_PLAYERS"); | |
emit Registered(0, car); | |
} | |
/*////////////////////////////////////////////////////////////// | |
CORE GAME | |
//////////////////////////////////////////////////////////////*/ | |
function play(uint256 turnsToPlay) external onlyDuringActiveGame nonReentrant { | |
unchecked { | |
// We'll play turnsToPlay turns, or until the game is done. | |
for (; turnsToPlay != 0; turnsToPlay--) { | |
Car[] memory allCars = cars; // Get and cache the cars. | |
uint256 currentTurn = turns; // Get and cache the current turn. | |
// Get the current car by moduloing the turns variable by the player count. | |
Car currentTurnCar = allCars[currentTurn % PLAYERS_REQUIRED]; | |
// Get all car data and the current turn car's index so we can pass it via takeYourTurn. | |
(CarData[] memory allCarData, uint256 yourCarIndex) = getAllCarDataAndFindCar(currentTurnCar); | |
currentCar = currentTurnCar; // Set the current car temporarily. | |
// We use assembly here to prevent players from DoS-ing the game via the extcodesize check | |
// the compiler bakes in (which is not catchable via try catch) or via returndata bombing. | |
bytes memory inputData = abi.encodeWithSelector(Car.takeYourTurn.selector, allCarData, yourCarIndex); | |
assembly { | |
// Call the currentTurnCar with 2,000,000 gas and avoid copying any returndata. | |
pop(call(2000000, currentTurnCar, 0, add(inputData, 32), mload(inputData), 0, 0)) | |
} | |
delete currentCar; // Restore the current car to the zero address. | |
// Loop over all of the cars and update their data. | |
for (uint256 i = 0; i < PLAYERS_REQUIRED; i++) { | |
Car car = allCars[i]; // Get the car. | |
// Get a pointer to the car's data struct. | |
CarData storage carData = getCarData[car]; | |
// If the car is now past the finish line after moving: | |
if ((carData.y += carData.speed) >= FINISH_DISTANCE) { | |
emit Dub(currentTurn, car); // It won. | |
state = State.DONE; | |
return; // Exit early. | |
} | |
} | |
// If this is the last turn in the batch: | |
if (currentTurn % PLAYERS_REQUIRED == 0) { | |
// Knuth shuffle over the cars using our entropy as randomness. | |
for (uint256 j = 0; j < PLAYERS_REQUIRED; ++j) { | |
// Generate a new random number by hashing the old one. | |
uint256 newEntropy = (entropy = uint72(uint256(keccak256(abi.encode(entropy))))); | |
// Choose a random position in front of j to swap with. | |
uint256 j2 = j + (newEntropy % (PLAYERS_REQUIRED - j)); | |
Car temp = allCars[j]; | |
allCars[j] = allCars[j2]; | |
allCars[j2] = temp; | |
} | |
cars = allCars; // Reorder cars using the new shuffled ones. | |
} | |
// Note: If this line was deployed on-chain it would be a big waste of gas. | |
emit TurnCompleted(turns = uint16(currentTurn + 1), getAllCarData(), 0, 0); | |
} | |
} | |
} | |
/*////////////////////////////////////////////////////////////// | |
ACTIONS | |
//////////////////////////////////////////////////////////////*/ | |
function buyAcceleration(uint256 amount) external onlyDuringActiveGame onlyCurrentCar returns (uint256 cost) { | |
cost = getAccelerateCost(amount); // Get the cost of the acceleration. | |
// Get a storage pointer to the calling car's data struct. | |
CarData storage car = getCarData[Car(msg.sender)]; | |
car.balance -= cost.safeCastTo32(); // This will underflow if we cant afford. | |
unchecked { | |
car.speed += uint32(amount); // Increase their speed by the amount. | |
// Increase the number of accelerates sold. | |
getActionsSold[ActionType.ACCELERATE] += amount; | |
} | |
emit Accelerated(turns, Car(msg.sender), amount, cost); | |
} | |
function buyShell(uint256 amount) external onlyDuringActiveGame onlyCurrentCar returns (uint256 cost) { | |
require(amount != 0, "YOU_CANT_BUY_ZERO_SHELLS"); // Buying zero shells would make them free. | |
cost = getShellCost(amount); // Get the cost of the shells. | |
// Get a storage pointer to the calling car's data struct. | |
CarData storage car = getCarData[Car(msg.sender)]; | |
car.balance -= cost.safeCastTo32(); // This will underflow if we cant afford. | |
uint256 y = car.y; // Retrieve and cache the car's y. | |
unchecked { | |
// Increase the number of shells sold. | |
getActionsSold[ActionType.SHELL] += amount; | |
Car closestCar; // Used to determine who to shell. | |
uint256 distanceFromClosestCar = type(uint256).max; | |
for (uint256 i = 0; i < PLAYERS_REQUIRED; i++) { | |
CarData memory nextCar = getCarData[cars[i]]; | |
// If the car is behind or on us, skip it. | |
if (nextCar.y <= y) continue; | |
// Measure the distance from the car to us. | |
uint256 distanceFromNextCar = nextCar.y - y; | |
// If this car is closer than all other cars we've | |
// looked at so far, we'll make it the closest one. | |
if (distanceFromNextCar < distanceFromClosestCar) { | |
closestCar = nextCar.car; | |
distanceFromClosestCar = distanceFromNextCar; | |
} | |
} | |
// If there is a closest car, shell it. | |
if (address(closestCar) != address(0)) { | |
// Set the speed to POST_SHELL_SPEED unless its already at that speed or below, as to not speed it up. | |
if (getCarData[closestCar].speed > POST_SHELL_SPEED) getCarData[closestCar].speed = POST_SHELL_SPEED; | |
} | |
emit Shelled(turns, Car(msg.sender), closestCar, amount, cost); | |
} | |
} | |
/*////////////////////////////////////////////////////////////// | |
ACTION PRICING | |
//////////////////////////////////////////////////////////////*/ | |
function getAccelerateCost(uint256 amount) public view returns (uint256 sum) { | |
unchecked { | |
for (uint256 i = 0; i < amount; i++) { | |
sum += computeActionPrice( | |
ACCELERATE_TARGET_PRICE, | |
ACCELERATE_PER_TURN_DECREASE, | |
turns, | |
getActionsSold[ActionType.ACCELERATE] + i, | |
ACCELERATE_SELL_PER_TURN | |
); | |
} | |
} | |
} | |
function getShellCost(uint256 amount) public view returns (uint256 sum) { | |
unchecked { | |
for (uint256 i = 0; i < amount; i++) { | |
sum += computeActionPrice( | |
SHELL_TARGET_PRICE, | |
SHELL_PER_TURN_DECREASE, | |
turns, | |
getActionsSold[ActionType.SHELL] + i, | |
SHELL_SELL_PER_TURN | |
); | |
} | |
} | |
} | |
function computeActionPrice( | |
int256 targetPrice, | |
int256 perTurnPriceDecrease, | |
uint256 turnsSinceStart, | |
uint256 sold, | |
int256 sellPerTurnWad | |
) internal pure returns (uint256) { | |
unchecked { | |
// prettier-ignore | |
return uint256( | |
wadMul(targetPrice, wadExp(unsafeWadMul(wadLn(1e18 - perTurnPriceDecrease), | |
// Theoretically calling toWadUnsafe with turnsSinceStart and sold can overflow without | |
// detection, but under any reasonable circumstance they will never be large enough. | |
// Use sold + 1 as we need the number of the tokens that will be sold (inclusive). | |
// Use turnsSinceStart - 1 since turns start at 1 but here the first turn should be 0. | |
toWadUnsafe(turnsSinceStart - 1) - (wadDiv(toWadUnsafe(sold + 1), sellPerTurnWad)) | |
)))) / 1e18; | |
} | |
} | |
/*////////////////////////////////////////////////////////////// | |
HELPERS | |
//////////////////////////////////////////////////////////////*/ | |
modifier onlyDuringActiveGame() { | |
require(state == State.ACTIVE, "GAME_NOT_ACTIVE"); | |
_; | |
} | |
modifier onlyCurrentCar() { | |
require(Car(msg.sender) == currentCar, "NOT_CURRENT_CAR"); | |
_; | |
} | |
function getAllCarData() public view returns (CarData[] memory results) { | |
results = new CarData[](PLAYERS_REQUIRED); // Allocate the array. | |
// Get a list of cars sorted descendingly by y. | |
Car[] memory sortedCars = getCarsSortedByY(); | |
unchecked { | |
// Copy over each car's data into the results array. | |
for (uint256 i = 0; i < PLAYERS_REQUIRED; i++) results[i] = getCarData[sortedCars[i]]; | |
} | |
} | |
function getAllCarDataAndFindCar(Car carToFind) public view returns (CarData[] memory results, uint256 foundCarIndex) { | |
results = new CarData[](PLAYERS_REQUIRED); // Allocate the array. | |
// Get a list of cars sorted descendingly by y. | |
Car[] memory sortedCars = getCarsSortedByY(); | |
unchecked { | |
// Copy over each car's data into the results array. | |
for (uint256 i = 0; i < PLAYERS_REQUIRED; i++) { | |
Car car = sortedCars[i]; | |
// Once we find the car, we can set the index. | |
if (car == carToFind) foundCarIndex = i; | |
results[i] = getCarData[car]; | |
} | |
} | |
} | |
/*////////////////////////////////////////////////////////////// | |
SORTING LOGIC | |
//////////////////////////////////////////////////////////////*/ | |
function getCarsSortedByY() internal view returns (Car[] memory sortedCars) { | |
unchecked { | |
sortedCars = cars; // Initialize sortedCars. | |
// Implements a descending bubble sort algorithm. | |
for (uint256 i = 0; i < PLAYERS_REQUIRED; i++) { | |
for (uint256 j = i + 1; j < PLAYERS_REQUIRED; j++) { | |
// Sort cars descendingly by their y position. | |
if (getCarData[sortedCars[j]].y > getCarData[sortedCars[i]].y) { | |
Car temp = sortedCars[i]; | |
sortedCars[i] = sortedCars[j]; | |
sortedCars[j] = temp; | |
} | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment