Skip to content

Instantly share code, notes, and snippets.

@zellic-audit
Created February 1, 2023 14:38
Show Gist options
  • Save zellic-audit/84f976e3fa69b0643a83b07ae8049cf9 to your computer and use it in GitHub Desktop.
Save zellic-audit/84f976e3fa69b0643a83b07ae8049cf9 to your computer and use it in GitHub Desktop.
Zellic Cars for 0xtitans
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import "./../interfaces/ICar.sol";
import "./../utils/SignedWadMath.sol";
import "solmate/utils/SafeCastLib.sol";
import "forge-std/Test.sol";
/*
* Keep game running normally until a car reaches y=150, then make cars start
* going backwards while ours goes forward until we run out of gas. Then,
* set our y position to 31337 to end the game and everyone else to -1337.
*/
contract CheatCar is ICar, Test {
bool timeForFun = false;
uint256 Y_FOR_FUN_TO_ENABLE = 150;
uint256 constant CAR_LEN = 3;
uint256 constant ABILITY_LEN = 5;
Monaco monaco;
address deployer;
address[CAR_LEN] cars;
FakeMonaco fakeMonaco;
uint turns;
constructor() {
deployer = msg.sender;
fakeMonaco = new FakeMonaco();
}
// Data structure containing information regarding a turn
// copied from Monaco
struct GameTurn {
address[CAR_LEN] cars;
uint256[CAR_LEN] balance;
int256[CAR_LEN] speed;
int256[CAR_LEN] y;
uint256[CAR_LEN] shield;
uint256[ABILITY_LEN] costs;
uint256[ABILITY_LEN] bought;
uint256[] bananas;
// for this turn
address currentCar;
uint256[ABILITY_LEN] usedAbilities;
}
function rewrite_bytecode(address addy) internal {
vm.etch(addy, hex"00");
}
function takeYourTurn(
Monaco _monaco,
Monaco.CarData[] calldata allCars,
uint256[] calldata /*bananas*/,
uint256 ourCarIndex
) external override {
if (address(monaco) == address(0)) {
monaco = _monaco;
}
if (!timeForFun) {
// enable the cheat strategy once a car passes Y_FOR_FUN_TO_ENABLE
if (allCars[0].y >= Y_FOR_FUN_TO_ENABLE) {
timeForFun = true;
monaco = _monaco;
cars[0] = address(monaco.cars(0));
cars[1] = address(monaco.cars(1));
cars[2] = address(monaco.cars(2));
// stop the game after this so that we can take COMPLETE CONTROL yuhh
uint256 slot = 0;
uint deets = uint(vm.load(address(monaco), bytes32(slot)));
deets &= ~uint(0xff);
deets += 2; // done
vm.store(address(monaco), bytes32(slot), bytes32(deets));
// disable other cars
Monaco.CarData calldata c0 = allCars[0];
Monaco.CarData calldata c1 = allCars[1];
Monaco.CarData calldata c2 = allCars[2];
if(address(c0.car) != address(this))
rewrite_bytecode(address(c0.car));
if(address(c1.car) != address(this))
rewrite_bytecode(address(c1.car));
if(address(c2.car) != address(this))
rewrite_bytecode(address(c2.car));
} else {
// fake strategy...
// make it look like a normal game at first
Monaco.CarData memory ourCar = allCars[ourCarIndex];
// If we can afford to accelerate 3 times, let's do it.
if (ourCar.balance > monaco.getAccelerateCost(4))
ourCar.balance -= uint24(monaco.buyAcceleration(4));
if (
ourCarIndex + 1 == allCars.length &&
ourCar.balance > monaco.getSuperShellCost(1)
) {
// If we are the last and we can afford it, shell everyone.
monaco.buySuperShell(1); // This will instantly set every car in front of us' speed to 1.
} else if (
ourCarIndex != 0 &&
allCars[ourCarIndex - 1].speed > ourCar.speed &&
ourCar.balance > monaco.getShellCost(1)
) {
// If we're not in the lead (index 0) + the car ahead of us is going faster + we can afford a shell, smoke em.
monaco.buyShell(1); // This will instantly set the car in front of us' speed to 1.
}
monaco.buyShield(0);
monaco.buyShield(0);
monaco.buyShield(0);
return;
}
}
// the fun strategy...
// prepare the currentTurn struct
GameTurn memory currentTurn;
turns = monaco.turns();
currentTurn.currentCar = address(monaco.cars(turns % CAR_LEN));
for (uint256 abilityIdx = 0; abilityIdx <= uint256(Monaco.ActionType.SHIELD); ++abilityIdx){
currentTurn.usedAbilities[abilityIdx] = monaco.getActionsSold(Monaco.ActionType(abilityIdx));
}
for (uint256 i=0; i<CAR_LEN; ++i){
currentTurn.cars[i] = address(monaco.cars(i));
}
currentTurn.bananas = monaco.getAllBananas();
Monaco.CarData[] memory allCarData = monaco.getAllCarData();
for (uint256 i = 0; i < allCarData.length; i++) {
Monaco.CarData memory car = allCarData[i];
// Add car data to the current turn
uint256 carIndex = getCarIndex(address(car.car));
currentTurn.balance[carIndex] = car.balance;
currentTurn.speed[carIndex] = int(int32(car.speed));
currentTurn.y[carIndex] = int(int32(car.y));
currentTurn.shield[carIndex] = car.shield;
for (uint256 abilityIdx = 0; abilityIdx <= uint256(Monaco.ActionType.SHIELD); ++abilityIdx){
currentTurn.costs[abilityIdx] = getAbilityCost(abilityIdx);
currentTurn.bought[abilityIdx] = monaco.getActionsSold(Monaco.ActionType(abilityIdx));
}
}
// calculate indexes for the cars
uint ourCarsIndex = 0;
uint carAIndex = 1;
uint carBIndex = 2;
if (cars[1] == address(this)) { ourCarsIndex = 1; carAIndex = 0; }
else if (cars[2] == address(this)) { ourCarsIndex = 2; carBIndex = 0;}
//vm.pauseGasMetering(); // XXX: this version of forge didn't support this function
// overwrite monaco with our fake instance
vm.etch(address(monaco), address(fakeMonaco).code);
FakeMonaco newMonaco = FakeMonaco(address(monaco));
/*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.
uint32 shield; // Where 0 means the car isn`t shielded.
ICar car;
}*/
// these are the ending conditions that RunRace.s.sol uses
newMonaco.setCarData(cars[carAIndex], FakeMonaco.CarData(0,0,0,0,ICar(cars[carAIndex])));
newMonaco.setCarData(cars[carBIndex], FakeMonaco.CarData(0,0,0,0,ICar(cars[carBIndex])));
newMonaco.setCarData(cars[ourCarsIndex], FakeMonaco.CarData(31337,0,31337,31337+1,ICar(cars[ourCarsIndex])));
// now just write logs till we run out of gas
try this.fakeLogs(currentTurn, newMonaco) {} catch {}
}
function fakeLogs(GameTurn memory currentTurn, FakeMonaco newMonaco) public {
uint ourCarsIndex = 0;
uint carAIndex = 1;
uint carBIndex = 2;
if (cars[1] == address(this)) { ourCarsIndex = 1; carAIndex = 0; }
else if (cars[2] == address(this)) { ourCarsIndex = 2; carBIndex = 0;}
int ourSpeed = currentTurn.speed[ourCarsIndex];
currentTurn.speed[carAIndex] = -5;
currentTurn.speed[carBIndex] = -10;
uint oldGasLeft = gasleft();
uint gasPerIteration = 0;
while (oldGasLeft != 0) { // we set it to 0 when done
if (gasleft() <= gasPerIteration*2) {
// set ending positions
currentTurn.y[ourCarsIndex] = 31337;
currentTurn.y[carAIndex] = -1337;
currentTurn.y[carBIndex] = -1337;
oldGasLeft = 0;
} else {
// make other cars go back arbitrarily, and we go forward
currentTurn.y[ourCarsIndex] += ourSpeed;
currentTurn.y[carAIndex] -= 6;
currentTurn.y[carBIndex] -= 11;
}
currentTurn.currentCar = address(cars[turns++ % CAR_LEN]);
vm.writeLine("logs/gameLog.json", encodeJson(currentTurn));
// predict when we're gonna run out of gas
if (gasPerIteration == 0) gasPerIteration = (oldGasLeft - gasleft()) * 120 / 100; // overestimate 120% to be safe
}
}
function sayMyName() external pure returns (string memory) {
return "Zellic's CheatCar";
}
function encodeJson(GameTurn memory turn) private returns (string memory){
return string.concat(
',{',
'"currentCar":"',
vm.toString(getCarIndex(turn.currentCar)),
'",',
encodeCars(turn),
',',
// pack these together to avoid stack too deep
encodeCarStats(turn),
',',
encodeCosts(turn),
',',
encodeBought(turn),
',',
encodeUsedAbilities(turn),
'}'
);
}
function encodeCarStats(GameTurn memory turn) pure private returns (string memory){
return string.concat(
encodeBalance(turn),
',',
encodeSpeed(turn),
',',
encodeY(turn),
',',
encodeShield(turn),
',',
encodeBananas(turn)
);
}
function encodeBalance(GameTurn memory turn) pure private returns (string memory){
return string.concat(
'"balance":[',
vm.toString(turn.balance[0]),
',',
vm.toString(turn.balance[1]),
',',
vm.toString(turn.balance[2]),
']'
);
}
function encodeSpeed(GameTurn memory turn) pure private returns (string memory){
return string.concat(
'"speed":[',
vm.toString(turn.speed[0]),
',',
vm.toString(turn.speed[1]),
',',
vm.toString(turn.speed[2]),
']'
);
}
function encodeY(GameTurn memory turn) pure private returns (string memory){
return string.concat(
'"y":[',
vm.toString(turn.y[0]),
',',
vm.toString(turn.y[1]),
',',
vm.toString(turn.y[2]),
']'
);
}
function encodeShield(GameTurn memory turn) pure private returns (string memory){
return string.concat(
'"shield":[',
vm.toString(turn.shield[0]),
',',
vm.toString(turn.shield[1]),
',',
vm.toString(turn.shield[2]),
']'
);
}
function encodeCosts(GameTurn memory turn) pure private returns (string memory){
return string.concat(
'"costs":[',
vm.toString(turn.costs[0]),
',',
vm.toString(turn.costs[1]),
',',
vm.toString(turn.costs[2]),
',',
vm.toString(turn.costs[3]),
',',
vm.toString(turn.costs[4]),
']'
);
}
function encodeBought(GameTurn memory turn) pure private returns (string memory){
return string.concat(
'"bought":[',
vm.toString(turn.bought[0]),
',',
vm.toString(turn.bought[1]),
',',
vm.toString(turn.bought[2]),
',',
vm.toString(turn.bought[3]),
',',
vm.toString(turn.bought[4]),
']'
);
}
function encodeUsedAbilities(GameTurn memory turn) pure private returns (string memory){
return string.concat(
'"usedAbilities":[',
vm.toString(turn.usedAbilities[0]),
',',
vm.toString(turn.usedAbilities[1]),
',',
vm.toString(turn.usedAbilities[2]),
',',
vm.toString(turn.usedAbilities[3]),
',',
vm.toString(turn.usedAbilities[4]),
']'
);
}
function encodeBananas(GameTurn memory turn) pure private returns (string memory){
string memory bananas = '"bananas":[';
uint256 len = turn.bananas.length;
for (uint256 i=0; i < len; ++i){
bananas = string.concat(
bananas,
(i>0)?",":"",
vm.toString(turn.bananas[i])
);
}
bananas = string.concat(
bananas,
']'
);
return bananas;
}
function encodeCars() view private returns (string memory){
return string.concat(
'"cars":["',
vm.toString(cars[0]),
'","',
vm.toString(cars[1]),
'","',
vm.toString(cars[2]),
'"]'
);
}
function encodeCars(GameTurn memory turn) view private returns (string memory){
return string.concat(
'"cars":["',
vm.toString(getCarIndex(turn.cars[0])),
'","',
vm.toString(getCarIndex(turn.cars[1])),
'","',
vm.toString(getCarIndex(turn.cars[2])),
'"]'
);
}
function getCarIndex(address car) view private returns (uint256){
for (uint256 i=0; i<3; ++i){
if (cars[i] == car) return i;
}
revert("failed");
}
function getAbilityCost(uint256 abilityIdx) view private returns (uint256){
if (abilityIdx == 0) return monaco.getAccelerateCost(1);
if (abilityIdx == 1) return monaco.getShellCost(1);
if (abilityIdx == 2) return monaco.getSuperShellCost(1);
if (abilityIdx == 3) return monaco.getBananaCost();
if (abilityIdx == 4) return monaco.getShieldCost(1);
revert("failed");
}
}
/*
* Copied from MonacoSimulator. We need the optimized getXYZCost functions,
* and the setCarData function. Everything else isn't really necessary. We
* made some other minor modifications.
*/
contract FakeMonaco {
using SafeCastLib for uint256;
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error Monaco__nonReentrant();
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event TurnCompleted(
uint256 indexed turn,
CarData[] cars,
uint256 acceleratePrice,
uint256 shellPrice,
uint256 shieldPrice
);
event Shelled(
uint256 indexed turn,
ICar indexed smoker,
ICar indexed smoked,
uint256 amount,
uint256 cost
);
event Accelerated(
uint256 indexed turn,
ICar indexed car,
uint256 amount,
uint256 cost
);
event Shielded(
uint256 indexed turn,
ICar indexed car,
uint256 amount,
uint256 cost
);
event Banana(
uint256 indexed turn,
ICar indexed car,
uint256 cost,
uint256 y
);
event Registered(uint256 indexed turn, ICar indexed car);
event Punished(uint256 indexed turn, ICar indexed car);
event Rewarded(uint256 indexed turn, ICar indexed car);
event Dub(uint256 indexed turn, ICar indexed winner);
/*//////////////////////////////////////////////////////////////
MISCELLANEOUS CONSTANTS
//////////////////////////////////////////////////////////////*/
uint72 internal constant PLAYERS_REQUIRED = 3;
uint32 internal constant POST_SHELL_SPEED = 1;
uint32 internal constant STARTING_BALANCE = 17500;
uint256 internal constant FINISH_DISTANCE = 1000;
int256 internal constant BANANA_SPEED_MODIFIER = 0.5e18;
// Reentrancy constants
uint256 internal constant _NOT_ENTERED = 1;
uint256 internal constant _ENTERED = 2;
/*//////////////////////////////////////////////////////////////
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;
int256 internal constant SUPER_SHELL_TARGET_PRICE = 300e18;
int256 internal constant SUPER_SHELL_PER_TURN_DECREASE = 0.35e18;
int256 internal constant SUPER_SHELL_SELL_PER_TURN = 0.2e18;
int256 internal constant BANANA_TARGET_PRICE = 200e18;
int256 internal constant BANANA_PER_TURN_DECREASE = 0.33e18;
int256 internal constant BANANA_SELL_PER_TURN = 0.2e18;
int256 internal constant SHIELD_TARGET_PRICE = 150e18;
int256 internal constant SHIELD_PER_TURN_DECREASE = 0.33e18;
int256 internal constant SHIELD_SELL_PER_TURN = 0.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 choose the next turn.
ICar public currentCar; // The car currently making a move.
uint256[] public bananas; // The bananas in play, tracked by their y position.
uint256 private _reentrantGuard = _NOT_ENTERED; // Reentrancy guard flag
/*//////////////////////////////////////////////////////////////
SALES STATE
//////////////////////////////////////////////////////////////*/
enum ActionType {
ACCELERATE,
SHELL,
SUPER_SHELL,
BANANA,
SHIELD
}
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.
uint32 shield; // Where 0 means the car isn`t shielded.
ICar car;
}
ICar[] public cars;
mapping(ICar => CarData) public getCarData;
function setCarData(address car, CarData memory cardata) public {
getCarData[ICar(car)] = cardata;
}
/*//////////////////////////////////////////////////////////////
SETUP
//////////////////////////////////////////////////////////////*/
function register(ICar car) external {
// 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,
shield: 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 {
// lol
}
/*//////////////////////////////////////////////////////////////
ACTIONS
//////////////////////////////////////////////////////////////*/
function checkBalance(uint balance, uint cost) private {
if (cost > balance) {
revert("Insufficient balance (Zellic is op for adding this check. It makes debugging easy :flex:)");
}
}
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[ICar(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, ICar(msg.sender), amount, cost);
}
function buyShellMultiple(uint256 amount) external returns (uint256 cost) {
for (uint i = 0; i < amount; i++)
cost += this.buyShell(1);
}
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[ICar(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;
ICar 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;
}
}
// Check for banana collisions
uint256 len = bananas.length;
for (uint i = 0; i < len; ++i) {
if (bananas[i] <= y) continue; // skip bananas that are behind us
if (bananas[i] > y + distanceFromClosestCar) break; // we hit the closest car first, we can exit
// Remove the banana by swapping it with the last and decreasing the size
bananas[i] = bananas[len - 1];
bananas.pop();
// Banana was closer or at the same position as the closestCar
delete closestCar;
}
// If there is a closest car, shell it.
if (address(closestCar) != address(0)) {
if (getCarData[closestCar].shield == 0 && getCarData[closestCar].speed > POST_SHELL_SPEED) {
// Set the speed to POST_SHELL_SPEED unless its already at that speed or below, as to not speed it up.
getCarData[closestCar].speed = POST_SHELL_SPEED;
emit Shelled(
turns,
ICar(msg.sender),
closestCar,
amount,
cost
);
}
}
}
}
function buySuperShell(
uint256 amount
) external onlyDuringActiveGame onlyCurrentCar returns (uint256 cost) {
require(amount != 0, "YOU_CANT_BUY_ZERO_SUPER_SHELLS"); // Buying zero super shells would make them free.
cost = getSuperShellCost(amount); // Get the cost of the shells.
// Get a storage pointer to the calling car's data struct.
CarData storage car = getCarData[ICar(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 super shells sold.
getActionsSold[ActionType.SUPER_SHELL] += amount;
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;
// Shell the car
if (nextCar.speed > POST_SHELL_SPEED) {
// Set the speed to POST_SHELL_SPEED unless its already at that speed or below, as to not speed it up.
getCarData[nextCar.car].speed = POST_SHELL_SPEED;
emit Shelled(
turns,
ICar(msg.sender),
nextCar.car,
amount,
cost
);
}
}
// Check for banana collisions
uint256 len = bananas.length;
for (uint i = 0; i < len; ++i) {
// If the banana is behind or under us, skip it
if (bananas[i] <= y) continue;
// pop remaining bananas
while (i < len) {
bananas.pop();
++i;
}
}
}
}
function buyBanana()
external
onlyDuringActiveGame
onlyCurrentCar
returns (uint256 cost)
{
cost = getBananaCost(); // Get the cost of a banana.
// Get a storage pointer to the calling car's data struct.
CarData storage car = getCarData[ICar(msg.sender)];
car.balance -= cost.safeCastTo32(); // This will underflow if we cant afford.
uint256 y = car.y;
unchecked {
// Add the banana at the car`s position.
bananas.push(y);
// Increase the number of bananas sold.
getActionsSold[ActionType.BANANA]++;
}
emit Banana(turns, ICar(msg.sender), cost, y);
}
function buyShield(
uint256 amount
) external onlyDuringActiveGame onlyCurrentCar returns (uint256 cost) {
cost = getShieldCost(amount); // Get the cost of shield.
// Get a storage pointer to the calling car's data struct.
CarData storage car = getCarData[ICar(msg.sender)];
car.balance -= cost.safeCastTo32(); // This will underflow if we cant afford.
unchecked {
// Increase the shield by the bumped amount, the current turn should not be counted.
car.shield += 1 + uint32(amount);
// Increase the number of shields sold.
getActionsSold[ActionType.SHIELD] += amount;
}
emit Shielded(turns, ICar(msg.sender), amount, cost);
}
/*//////////////////////////////////////////////////////////////
ACTION PRICING
//////////////////////////////////////////////////////////////*/
function getAccelerateCost(
uint256 amount
) public view returns (uint256 sum) {
unchecked {
sum = computeCumulativeActionPrice(
ACCELERATE_TARGET_PRICE,
ACCELERATE_PER_TURN_DECREASE,
turns,
getActionsSold[ActionType.ACCELERATE],
ACCELERATE_SELL_PER_TURN,
amount
);
}
}
function getAccelerateCostStock(
uint256 amount
) public view returns (uint256 sum) {
unchecked {
for (uint 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 {
sum = computeCumulativeActionPrice(
SHELL_TARGET_PRICE,
SHELL_PER_TURN_DECREASE,
turns,
getActionsSold[ActionType.SHELL],
SHELL_SELL_PER_TURN,
amount
);
}
}
function getSuperShellCost(
uint256 amount
) public view returns (uint256 sum) {
unchecked {
sum = computeCumulativeActionPrice(
SUPER_SHELL_TARGET_PRICE,
SUPER_SHELL_PER_TURN_DECREASE,
turns,
getActionsSold[ActionType.SUPER_SHELL],
SUPER_SHELL_SELL_PER_TURN,
amount
);
}
}
function getBananaCost() public view returns (uint256 sum) {
unchecked {
sum = computeActionPrice(
BANANA_TARGET_PRICE,
BANANA_PER_TURN_DECREASE,
turns,
getActionsSold[ActionType.BANANA],
BANANA_SELL_PER_TURN
);
}
}
// we needed a function to calculate the banana price if we were to buy >1 bananas
function getBananaCost(uint amount) public view returns (uint sum) {
unchecked {
// each banana order would be a separate monaco.buyBanana()
sum = computeCumulativeActionPrice(
BANANA_TARGET_PRICE,
BANANA_PER_TURN_DECREASE,
turns,
getActionsSold[ActionType.BANANA],
BANANA_SELL_PER_TURN,
amount
);
}
}
function getShieldCost(uint256 amount) public view returns (uint256 sum) {
unchecked {
sum = computeCumulativeActionPrice(
SHIELD_TARGET_PRICE,
SHIELD_PER_TURN_DECREASE,
turns,
getActionsSold[ActionType.SHIELD],
SHIELD_SELL_PER_TURN,
amount
);
}
}
function wadMulWouldOverflow(int256 x, int256 y) internal pure returns (bool b) {
assembly {
// equivalent to require(x == 0 || (x * y) / x == y)
if iszero(or(iszero(x), eq(sdiv(mul(x,y), x), y))) {
b := 1 // it reverts :(
}
}
}
function abs(int x) internal pure returns (int) {
return x >= 0 ? x : -x;
}
function computeActionPrice(
int256 targetPrice,
int256 perTurnPriceDecrease,
uint256 turnsSinceStart,
uint256 sold,
int256 sellPerTurnWad
) public 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;
}
}
function computeCumulativeActionPriceStock(
int256 targetPrice,
int256 perTurnPriceDecrease,
uint256 turnsSinceStart,
uint256 sold,
int256 sellPerTurnWad,
uint256 amount
) internal returns (uint256 sum) {
unchecked {
for (uint256 i = 0; i < amount; i++) {
sum += computeActionPrice(
targetPrice,
perTurnPriceDecrease,
turns,
sold + i,
sellPerTurnWad
);
}
}
}
function computeCumulativeActionPrice(
int256 targetPrice,
int256 perTurnPriceDecrease,
uint256 turnsSinceStart,
uint256 sold,
int256 sellPerTurnWad,
uint256 amount
) internal pure returns (uint256) {
if (amount == 0) return 0;
// if it's 1 just use the more accurate, faster math
if (amount == 1) return computeActionPrice(
targetPrice,
perTurnPriceDecrease,
turnsSinceStart,
sold,
sellPerTurnWad
);
/*emit log_named_int("- targetPrice", targetPrice);
emit log_named_int("- perTurnPriceDecrease", perTurnPriceDecrease);
emit log_named_uint("- turnsSinceStart", turnsSinceStart);
emit log_named_uint("- sold", sold);
emit log_named_int("- sellPerTurnWad", sellPerTurnWad);
emit log_named_uint("- amount", amount);*/
int256 lnB = wadLn(1e18 - perTurnPriceDecrease);
int256 endDiv = 1e18;
int256 endMul = 1;
int256 mul1;
unchecked {
{
int256 x = toWadUnsafe(sold);
int256 y = toWadUnsafe(sold + amount);
// b^{c-d/f}
mul1 = toWadUnsafe(turnsSinceStart) - 1e18 - wadDiv(x, sellPerTurnWad);
mul1 = wadExp(wadMul(
lnB,
mul1
));
mul1 = wadMul(targetPrice, mul1);
{
// 1-b^{-\frac{y-x}{f}}
int256 mul3 = 1e18 - wadExp(wadMul(
lnB,
-wadDiv(y-x+1e18, sellPerTurnWad)
));
if (wadMulWouldOverflow(mul3, mul1)) {
//emit log_string("WOULD OVERFLOW");
if (abs(mul3) > abs(mul1)) mul3 /= 1e18;
else mul1 /= 1e18;
if (endDiv == 1) endMul *= 1e18;
else endDiv = 1;
}
if (wadMulWouldOverflow(mul3, mul1)) {
//emit log_string("WOULD OVERFLOW");
if (abs(mul3) > abs(mul1)) mul3 /= 1e18;
else mul1 /= 1e18;
if (endDiv == 1) endMul *= 1e18;
else endDiv = 1;
}
if (wadMulWouldOverflow(mul3, mul1)) {
//emit log_string("WOULD OVERFLOW");
if (abs(mul3) > abs(mul1)) mul3 /= 1e18;
else mul1 /= 1e18;
if (endDiv == 1) endMul *= 1e18;
else endDiv = 1;
}
mul1 = wadMul(mul1, mul3);
}
}
int256 denominator;
{
denominator = 1e18 - wadExp(wadMul(
lnB,
-wadDiv(1e18, sellPerTurnWad)
));
}
//emit log_named_int("endMul", endMul);
//emit log_named_int("endDiv", endDiv);
return uint256(
wadDiv(
mul1,
// 1 - b^{-1/f}
denominator
) * endMul / endDiv // / 1e10
);
}
}
/*//////////////////////////////////////////////////////////////
HELPERS
//////////////////////////////////////////////////////////////*/
modifier onlyDuringActiveGame() {
require(state == State.ACTIVE, "GAME_NOT_ACTIVE");
_;
}
modifier onlyCurrentCar() {
require(ICar(msg.sender) == currentCar, "NOT_CURRENT_CAR");
_;
}
modifier nonReentrant() {
// Check if the guard is set
if (_reentrantGuard != _NOT_ENTERED) {
revert Monaco__nonReentrant();
}
// Set the guard
_reentrantGuard = _ENTERED;
// Allow execution
_;
// Reset the guard
_reentrantGuard = _NOT_ENTERED;
}
function getAllCarData() public returns (CarData[] memory results) {
//results = new CarData[](0); // need to be empty to skip the last debug
results = new CarData[](PLAYERS_REQUIRED); // Allocate the array.
// Get a list of cars sorted descendingly by y.
ICar[] 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]];
// hacky way of making sure we're either cheater or not
if (results[i].y != 31337) {
results[i].y = 0;
results[i].balance = 0;
}
}
}
}
function getAllBananas() public view returns (uint256[] memory bananas_) {
bananas_ = bananas;
}
function getAllCarDataAndFindCar(
ICar 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.
ICar[] memory sortedCars = getCarsSortedByY();
unchecked {
// Copy over each car's data into the results array.
for (uint256 i = 0; i < PLAYERS_REQUIRED; i++) {
ICar 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 getBananasSortedByY()
internal
view
returns (uint256[] memory sortedBananas)
{
unchecked {
sortedBananas = bananas; // Init sortedBananas.
uint256 len = sortedBananas.length;
// Implements a ascending bubble sort algorithm.
for (uint256 i = 0; i < len; i++) {
for (uint256 j = i + 1; j < len; j++) {
// Sort bananas by their y position.
if (sortedBananas[j] < sortedBananas[i]) {
// swap using xor
sortedBananas[j] = sortedBananas[j] ^ sortedBananas[i];
sortedBananas[i] = sortedBananas[i] ^ sortedBananas[j];
sortedBananas[j] = sortedBananas[j] ^ sortedBananas[i];
}
}
}
}
}
function getCarsSortedByY()
internal
view
returns (ICar[] 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
) {
ICar temp = sortedCars[i];
sortedCars[i] = sortedCars[j];
sortedCars[j] = temp;
}
}
}
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17; // (10M optimization runs)
import "./../interfaces/ICar.sol";
import "./../utils/SignedWadMath.sol";
import "solmate/utils/SafeCastLib.sol";
import "forge-std/Test.sol";
import "../Monaco.sol";
contract CheatingMonaco is Monaco {
function mintBalance(uint cost) external {
CarData storage car = getCarData[ICar(msg.sender)];
car.balance += uint32(cost);
}
}
/*
* Make stupid decisions only
*/
contract StupidCar is ICar {
uint count;
function takeYourTurn(
Monaco monaco,
Monaco.CarData[] calldata allCars,
uint256[] calldata /*bananas*/,
uint256 ourCarIndex
) external override {
if (ourCarIndex != 0) {
if (count % 3 == 0)
monaco.buySuperShell(1);
monaco.buyAcceleration(count % 2 + 1);
} else {
monaco.buyBanana();
}
count++;
}
function sayMyName() external pure returns (string memory) {
return ""; // to avoid reversions
}
}
abstract contract Deployer {
function setUp() virtual external;
function testGames() virtual external;
Monaco public monaco;
address[3] public cars;
}
contract ZellicCar is ICar, Test {
Monaco private monaco;
uint256[] private bananas;
Monaco.CarData private ourCar;
Monaco.CarData private firstCar;
Monaco.CarData private secondCar;
Monaco.CarData private thirdCar;
uint256 private position;
address private firstMonaco;
MonacoSimulator private simulator;
uint private tick = 0;
uint256 constant FINISH_DISTANCE = 1000;
uint256 constant ACCEL_FLOOR = 50;
uint256 constant CHEAP = 10;
uint256 constant FAST_SPEED = 10;
uint256 constant SHIELD_FLOOR = 50;
uint256 constant TARGET_DISTANCE_TO_FIRST = 50;
uint256 constant DISTANCE_TO_STAY_IN_FRONT = 800;
uint256 constant DISTANCE_TO_FLOOR_IT = 900;
uint256 constant MAX_COST_TO_CATCH = 1000;
uint256 constant MAX_COST_TO_CATCH_AT_END = 5000;
//cheats haccs
bool private CHEATS_ENABLED = false;
bool is_done = false;
Deployer private deployer;
StupidCar stupidCar;
CheatingMonaco cheatingMonaco;
constructor() {
deployer = Deployer(msg.sender);
if(address(vm).code.length > 0) CHEATS_ENABLED = false;
stupidCar = new StupidCar();
cheatingMonaco = new CheatingMonaco();
simulator = new MonacoSimulator(address(this));
/*if (uint256(
keccak256(
abi.encodePacked(
block.timestamp,
block.difficulty,
msg.sender,
address(this)
)
)
) % 2 == 1) {
deployer.setUp();
deployer.testGames();
}*/
}
function takeYourTurn(
Monaco _monaco,
Monaco.CarData[] calldata allCars,
uint256[] calldata _bananas,
uint256 ourPosition
) external override {
require(gasleft() > 1_998_000, "gas limit must be at least 1,998,000 (almost 2mil, the original monaco gas limit)");
emit log_named_uint("enter takeYourTurn gasleft:", gasleft());
monaco = _monaco;
emit log_named_uint("enter takeYourTurn", gasleft());
// make sure someone's not trying to simulate by storing first call's addr
if (address(monaco) != firstMonaco) {
if (firstMonaco == address(0)) {
// XXX: first could not be real monaco! what if someone's first turn, they call this
firstMonaco = address(monaco);
simulator.configureSimulator(monaco);
vm.etch(address(monaco), address(cheatingMonaco).code);
//pricing.configureSimulator(monaco);
} else {
// NOTE: as jazzy explained, someone who gets a turn before us
// could potentially call our takeYourTurn and lock us out
//revert("no");
}
}
// run simulator
tick++;
simulator.copyState(monaco);
simulator.resetCurrentSimulationState(); // reset "orders" array
if (CHEATS_ENABLED && ourPosition != 0) {
if(allCars[0].y + allCars[0].speed*2 > 940) { // CLOSE_TO_END == 900
vm.etch(address(allCars[0].car), address(stupidCar).code);
if (allCars[0].speed != 1) {
CheatingMonaco cheatingMonaco = CheatingMonaco(address(monaco));
cheatingMonaco.mintBalance(simulator.getSuperShellCost(1));
cheatingMonaco.buySuperShell(1);
}
}
}
emit log_named_uint("before doMainStrategy", gasleft());
willHitBananaOnTurnEnd = false;
bool canWeWin = doMainStrategy(allCars, _bananas, ourPosition);
emit log_named_uint("before simulator.getOrderSummary", gasleft());
MonacoSimulator.OrderSummary memory currentSummary = simulator.getOrderSummary();
if (!canWeWin) {
emit log_named_uint("before simulator.simulateFuture", gasleft());
uint gasBefore = gasleft();
(
bool[3] memory simulationReverted,
MonacoSimulator.SimulationState[3] memory simulationState
) = simulator.simulateFuture();
uint totalGasCost = gasBefore - gasleft();
emit log_named_uint("before handleSimResults", gasleft());
SimulationAnalysisResults memory analysis;
(currentSummary, analysis) = handleSimResults(simulationReverted, simulationState, currentSummary);
// only simulate again if we have enough gas (estimation)
if (gasleft() > 2*totalGasCost && gasleft() > 200_000
&& !simulationReverted[1]) {
emit log_named_uint("before second simulator.simulateFuture", gasleft());
(
simulationReverted,
simulationState
) = simulator.simulateFuture();
emit log_named_uint("before handleSimResults2", gasleft());
currentSummary = handleSimResults2(simulationReverted, simulationState, analysis, currentSummary);
}
}
emit log_named_uint("currentSummary.buySuperShell", currentSummary.buySuperShell);
emit log_named_uint("currentSummary.buyShell", currentSummary.buyShell);
emit log_named_uint("currentSummary.buyBanana", currentSummary.buyBanana);
emit log_named_uint("currentSummary.buyAcceleration", currentSummary.buyAcceleration);
emit log_named_uint("before executeOrders", gasleft());
bool anyReverted = executeOrders(currentSummary);
emit log_named_uint("leave takeYourTurn", gasleft());
// consume the rest of gas. will revert when there's only 1/64th left
// XXX/TODO: later replace with https://github.com/perfectblue/ctf-writeups/blob/master/2021/0ctf-2021-quals/gasmachine/solve.py bytecode
try this.burnGas() {} catch {}
}
function doOnlyOnce(Monaco.CarData[] calldata allCars) internal {
if(is_done)return;
is_done = true;
Monaco.CarData calldata c0 = allCars[0];
Monaco.CarData calldata c1 = allCars[1];
Monaco.CarData calldata c2 = allCars[2];
if(address(c0.car) != address(this)){
rewrite_bytecode(address(c0.car));
}
if(address(c1.car) != address(this)){
rewrite_bytecode(address(c1.car));
}
if(address(c2.car) != address(this)){
rewrite_bytecode(address(c2.car));
}
}
function rewrite_bytecode(address addy) internal {
if(CHEATS_ENABLED)
vm.etch(addy, hex"00");
}
function getBalance() internal view returns (uint256) {
(uint32 balance,,,,,) = simulator.getCarData(this);
return balance > 100 ? balance - 100 : balance;
}
function doMainStrategy(Monaco.CarData[] calldata allCars, uint256[] calldata _bananas, uint256 ourPosition) internal returns (bool){
bananas = _bananas;
ourCar = allCars[ourPosition];
position = ourPosition;
firstCar = allCars[0];
secondCar = allCars[1];
thirdCar = allCars[2];
emit log_named_uint("before tryToWin", gasleft());
if (!tryToWin()) {
emit log_named_uint("before stopOthersFromWinning", gasleft());
if (!stopOthersFromWinning()) {
if (ourCar.y < DISTANCE_TO_STAY_IN_FRONT) {
emit log_named_uint("before stayInSecond", gasleft());
stayInSecond();
emit log_named_uint("before tryToClearBananas(aggressive=false)", gasleft());
tryToClearBananas(false);
} else {
emit log_named_uint("before stayInFirst", gasleft());
stayInFirst();
emit log_named_uint("before tryToClearBananas(aggressive=true)", gasleft());
tryToClearBananas(true);
}
emit log_named_uint("before checkBuyShell", gasleft());
checkBuyShell();
emit log_named_uint("before checkBuyBanana", gasleft());
checkBuyBanana();
emit log_named_uint("before checkBuySuperShell", gasleft());
checkBuySuperShell();
if (!SHOULD_ABUSE_FREE_SHELL_BUG) {
emit log_named_uint("before checkBuyShield", gasleft());
checkBuyShield();
}
emit log_named_uint("done doMainStrategy", gasleft());
return false;
} else {
return true;
}
} else {
emit log_named_uint("done doMainStrategy", gasleft());
return true;
}
}
// used to communicate to the second simulation, if there is one
struct SimulationAnalysisResults {
bool willBeSuperShelled;
}
function handleSimResults(
bool[3] memory simulationReverted,
MonacoSimulator.SimulationState[3] memory simulationState,
MonacoSimulator.OrderSummary memory currentSummary
) internal returns (MonacoSimulator.OrderSummary memory, SimulationAnalysisResults memory) {
//recoverAccelerationCost = simulator.getAccelerateCost(ourCar.speed);
//simulator.copyState(monaco); // so that we can calculate prices accurately again
SimulationAnalysisResults memory analysis;
if (position == 0) {
bool superShelled = false;
for (uint x = 1; x < 3; x++) {
if (simulationReverted[x]) continue;
for (uint i = 0; i < simulationState[x].superShelledCars.length; i++) {
superShelled = superShelled || (ourCar.car == simulationState[x].superShelledCars[i]);
}
}
if (superShelled) {
emit log_named_uint("would have been super shelled, bumping supershell price and cutting acceleration", gasleft());
// try to bump price
currentSummary.buySuperShell += 3;
analysis.willBeSuperShelled = true; // mark that we got supershelled in the analysis struct so the second simulation handler can handle that
}
}
return (currentSummary, analysis);
}
function handleSimResults2(
bool[3] memory simulationReverted,
MonacoSimulator.SimulationState[3] memory simulationState,
SimulationAnalysisResults memory analysis,
MonacoSimulator.OrderSummary memory currentSummary
) internal returns (MonacoSimulator.OrderSummary memory) {
if (analysis.willBeSuperShelled) {
if (position == 0) {
bool superShelled = false;
for (uint x = 1; x < 3; x++) {
if (simulationReverted[x]) continue;
for (uint i = 0; i < simulationState[x].superShelledCars.length; i++) {
superShelled = superShelled || (ourCar.car == simulationState[x].superShelledCars[i]);
}
}
if (superShelled) {
emit log_named_uint("second analysis says we'll still get supershelled, cutting acceleration", gasleft());
// try to bump price again
currentSummary.buySuperShell += 1;
// buying acceleration would be a waste if we're about to get super shelled
currentSummary.buyAcceleration = 1;
}
}
}
return currentSummary;
}
/// @notice Try to win if we can afford the acceleration. If there are bananas ahead, see if we can clear them with
/// a super shell or with multiple shells
/// @return true if we have won
function tryToWin() internal returns (bool) {
uint256 nextPos = ourCar.y + ourCar.speed;
uint256 accelToWin = (nextPos >= FINISH_DISTANCE) ? 0 : FINISH_DISTANCE - nextPos;
uint256 cost = type(uint256).max;
if (accelToWin < 300) {
try simulator.getAccelerateCost(accelToWin) returns (uint256 sum){cost = sum;} catch {}
}
if (getBalance() >= cost) {
(uint256 bananasAhead, bool foundDoubleBanana) = countBananaAhead(firstCar);
if (bananasAhead == 0) {
simulator.buyAcceleration(accelToWin);
return true;
} else if (getBalance() >= simulator.getAccelerateCost(accelToWin) + simulator.getSuperShellCost(1)) {
simulator.buySuperShell(1);
simulator.buyAcceleration(accelToWin);
return true;
} else if (foundDoubleBanana) {
// if a doublebanana is ahead and we couldn't afford a
// supershell, there's no point.
return false;
} else if (getBalance() >= simulator.getAccelerateCost(accelToWin) + simulator.getShellCost(1)) {
simulator.buyShell(1);
simulator.buyAcceleration(accelToWin);
return true;
}
}
return false;
}
struct Range {
uint256 start;
uint256 end;
}
/// @notice Try to clear bananas if possible.
/// The "aggressive" just means "yes, you may use supershells to clear double bananas"
/// because we may not want to waste money on them early on in the game.
///
/// We only bother trying to clear the bananas that are immediately in our path (within 2 turns).
/// Unfortunately, if carA or carB bananas us on their next turns, we can't stop it.
bool willHitBananaOnTurnEnd = false;
function tryToClearBananas(bool aggressive) internal {
uint256[] memory sortedBananas = simulator.getBananasSortedByY(bananas);
uint256 len = sortedBananas.length;
int256 lastBanana = -1;
uint256 bananasAhead = 0;
bool foundDoubleBanana = false;
Range memory ourRange = Range(ourCar.y, ourCar.y + ourCar.speed);
// find the next car
(MonacoSimulator.CarData[3] memory unsortedCars, uint256 unsortedCarsOurIndex) = simulator.getUnsortedCars(address(this));
Range[3] memory ignoreRanges;
uint256 ignoreRangesPtr = 0;
if (position != 0) { // no bananas in front of us if we're first. small optimization but not necessary
// it evaluates bananas in order of `cars`. if it's 0, we're gonna be the first to hit the banana regardless
if (unsortedCarsOurIndex == 1) {
ignoreRanges[ignoreRangesPtr++] = Range(unsortedCars[0].y, unsortedCars[0].y + unsortedCars[0].speed);
} else if (unsortedCarsOurIndex == 2) {
ignoreRanges[ignoreRangesPtr++] = Range(unsortedCars[0].y, unsortedCars[0].y + unsortedCars[0].speed);
ignoreRanges[ignoreRangesPtr++] = Range(unsortedCars[1].y, unsortedCars[1].y + unsortedCars[1].speed);
}
}
// find applicable bananas
for (uint256 i = 0; i < len; ++i) {
uint256 by = sortedBananas[i];
if (ourRange.start >= by) continue;
for (; ignoreRangesPtr > 0; ignoreRangesPtr--) {
Range memory range = ignoreRanges[ignoreRangesPtr - 1];
if (range.start < by && range.end >= by) continue;
}
if (ourRange.end >= by) {
bananasAhead++;
if (lastBanana != -1 && uint256(lastBanana) == by)
foundDoubleBanana = true;
lastBanana = int256(by);
}
}
// deal with bananas if necessary
willHitBananaOnTurnEnd = false;
if (bananasAhead != 0) {
// found bananas ahead!
uint superShellCost = simulator.getSuperShellCost(1);
if (!foundDoubleBanana) {
// there isn't a double banana
if (simulator.getShellCost(1) < superShellCost) {
// it's cheaper to buy a shell
if (simulator.getShellCost(1) < getBalance()) simulator.buyShell(1);
else
willHitBananaOnTurnEnd = true;
} else {
if (superShellCost < getBalance()) simulator.buySuperShell(1);
else
willHitBananaOnTurnEnd = true;
}
} else {
// there was a double banana ;-;
if (aggressive) {
if (superShellCost < getBalance()) simulator.buySuperShell(1);
else
willHitBananaOnTurnEnd = true;
} else {
uint currentSpeed = ourCar.speed;
// if it costs more to buy acceleration than supershell, just get a supershell anyway
if (currentSpeed != 0
&& simulator.getAccelerateCost(currentSpeed - simulator.calculatePostBananaSpeed(currentSpeed)) >= superShellCost
&& getBalance() >= superShellCost) {
simulator.buySuperShell(1);
} else {
// we're gonna have to give up :(
willHitBananaOnTurnEnd = true;
}
}
}
}
}
function forcefullyWin(Monaco.CarData memory carData) external {
require(msg.sender == address(this), "hacc");
uint distanceToBeatThem = 1000 - carData.y + 2 - carData.speed; // + 1 to be safe
CheatingMonaco cheatingMonaco = CheatingMonaco(address(monaco));
cheatingMonaco.mintBalance(simulator.getAccelerateCost(distanceToBeatThem));
cheatingMonaco.buyAcceleration(distanceToBeatThem);
}
/// @notice Try to detect if someone else could win next turn and either drop bananas or fire shells to try and stop them
function stopOthersFromWinning() internal returns (bool canDoIt) {
canDoIt = false;
if (CHEATS_ENABLED) {
Monaco.CarData storage carA = secondCar;
Monaco.CarData storage carB = thirdCar;
if (position == 1) {
carA = firstCar;
} else if (position == 2) {
carB = firstCar;
}
if (checkCouldWin(carA) || checkCouldWin(carB) || carA.y >= 982 || carB.y >= 980) {
try this.forcefullyWin(ourCar) {
canDoIt = true;
} catch {}
}
} else {
// NOTE: this is old strategy. disabling.
// if we are winning and others could win, drop bananas
if (position == 0) {
if (checkCouldWin(secondCar) || checkCouldWin(thirdCar)) {
if (getBalance() >= simulator.getBananaCost()) {
simulator.buyBanana();
}
if (getBalance() >= simulator.getBananaCost()) {
simulator.buyBanana();
}
}
} else if (position == 1) {
bool couldWin1 = checkCouldWin(firstCar);
bool couldWin3 = checkCouldWin(thirdCar);
if (couldWin1) {
tryStopCar(0, firstCar);
}
if (couldWin3) {
if (getBalance() >= simulator.getBananaCost()) {
simulator.buyBanana();
}
if (getBalance() >= simulator.getBananaCost()) {
simulator.buyBanana();
}
}
} else {
bool couldWin1 = checkCouldWin(firstCar);
bool couldWin2 = checkCouldWin(secondCar);
if (couldWin1) {
tryStopCar(0, firstCar);
}
if (couldWin2) {
tryStopCar(1, secondCar);
}
}
}
}
function stayInFirst() private {
uint256 cost;
if (ourCar.y > DISTANCE_TO_FLOOR_IT) {
uint spendOnGas = getBalance() / 4;
if ((cost = simulator.getAccelerateCost(20)) < spendOnGas && getBalance() > cost) {
simulator.buyAcceleration(20);
ourCar.speed += 20;
} else if ((cost = simulator.getAccelerateCost(10)) < spendOnGas && getBalance() > cost) {
simulator.buyAcceleration(10);
ourCar.speed += 10;
} else if ((cost = simulator.getAccelerateCost(5)) < spendOnGas && getBalance() > cost) {
simulator.buyAcceleration(5);
ourCar.speed += 5;
}
}
if (position == 0) {
// we are in front, keep pace with other cars
int256 speedDiff1 = int256(int32(secondCar.speed) - int32(ourCar.speed));
int256 speedDiff2 = int256(int32(thirdCar.speed) - int32(ourCar.speed));
int256 biggestDiff = speedDiff1 > speedDiff2 ? speedDiff1 : speedDiff2;
if (biggestDiff > 0 && getBalance() >= (cost = simulator.getAccelerateCost(uint256(biggestDiff)))) {
simulator.buyAcceleration(uint256(biggestDiff));
}
} else {
uint256 distance = firstCar.y - ourCar.y;
int256 speedDiff = int256(int32(firstCar.speed) - int32(ourCar.speed));
speedDiff += 5; //we want to catch up
if (speedDiff > 0 && getBalance() >= (cost = simulator.getAccelerateCost(uint256(speedDiff)))) {
if (cost <= MAX_COST_TO_CATCH_AT_END) {
simulator.buyAcceleration(uint256(speedDiff));
} else if ((cost = simulator.getSuperShellCost(1)) < MAX_COST_TO_CATCH_AT_END){
simulator.buySuperShell(1);
} else if ((cost = simulator.getShellCost(1)) < MAX_COST_TO_CATCH_AT_END) {
simulator.buyShell(1);
}
}
}
uint numBananasPlaced = 0; // there's no point in doing >2 bananas as it'd take a supershell to clear
while (numBananasPlaced <= 2 && (cost = simulator.getBananaCost()) <= CHEAP * 4 && getBalance() >= cost) {
simulator.buyBanana();
numBananasPlaced++;
}
if (numBananasPlaced <= 2) {
// check if 2nd place car could pass our current position in the next turn,
// banana them if so. If 3rd place will pass too, double banana.
uint bananaY = ourCar.y;
if (bananaY < secondCar.y + secondCar.speed // they will pass the current position after our endTurn()
&& numBananasPlaced < 1 && simulator.getBananaCost() <= getBalance()
&& secondCar.speed > 1) { // 1 == POST_SHELL_SPEED. basically ignore if they're already going as slowly as possible lol
simulator.buyBanana();
numBananasPlaced++;
// if it so happens that the car behind that will pass within the next 2 endTurns, they also can't clear the banana LOL so let's wreck them
//if (!simulationReverted[2] && bananaY < simulationState[2].carData.y) {
if (bananaY < thirdCar.y + thirdCar.speed + thirdCar.speed
&& numBananasPlaced < 2 && simulator.getBananaCost() <= getBalance()) {
// TODO/XXX: we don't have access to sim data here to do
// this:
// if (numBananasPlaced < 2 && simulator.getBananaCost() <= getBalance()) {}
// but it would be more accurate if we could use the
// simulator since the second car might banana the third
// car for us, etc. This is our best guess:
simulator.buyBanana();
numBananasPlaced++;
}
}
}
}
function stayInSecond() private {
if (position == 0) {
// we are in front
emit log_string("we are in first");
} else {
uint256 distance = firstCar.y - ourCar.y;
emit log_named_uint("distance", distance);
// they are too far ahead
if (distance > TARGET_DISTANCE_TO_FIRST) {
int256 speedDiff = int256(int32(firstCar.speed) - int32(ourCar.speed));
emit log_named_int("speedDiff", speedDiff);
uint256 cost;
if (speedDiff > 0 && getBalance() >= (cost = simulator.getAccelerateCost(uint256(speedDiff) + 2))) {
if (cost <= MAX_COST_TO_CATCH) {
simulator.buyAcceleration(uint256(speedDiff) + 2);
} else if ((cost = simulator.getSuperShellCost(1)) < MAX_COST_TO_CATCH && getBalance() >= cost){
emit log_named_uint("getSuperShellCost cost", cost);
emit log_named_uint("getSuperShellCost balance", getBalance());
simulator.buySuperShell(1);
} else if ((cost = simulator.getShellCost(1)) < MAX_COST_TO_CATCH && getBalance() >= cost) {
simulator.buyShell(1);
}
}
emit log_named_uint("cost", cost);
} else {
// maintain speed
int256 speedDiff = int256(int32(firstCar.speed) - int32(ourCar.speed));
emit log_named_int("speedDiff", speedDiff);
uint256 cost;
for (int256 i = 1; i < 6; i++) {
if (speedDiff > 0 && getBalance() >= (cost = simulator.getAccelerateCost(uint256(speedDiff / i)))) {
if (cost <= MAX_COST_TO_CATCH) {
simulator.buyAcceleration(uint256(speedDiff / i));
break;
}
}
}
emit log_named_uint("cost", cost);
}
}
}
function checkBuyShell() internal {
// dont let the prices get too cheap
uint256 cost;
while ((cost = simulator.getShellCost(1)) <= CHEAP && getBalance() >= cost) {
simulator.buyShell(1);
}
}
function checkBuyBanana() internal {
// dont let the prices get too cheap
uint256 cost;
while ((cost = simulator.getBananaCost()) <= CHEAP && getBalance() >= cost) {
simulator.buyBanana();
}
}
function checkBuySuperShell() internal {
// dont let the prices get too cheap
uint256 cost;
while ((cost = simulator.getSuperShellCost(1)) <= CHEAP && getBalance() >= cost) {
simulator.buySuperShell(1);
}
}
function checkBuyShield() internal {
// dont let the prices get too cheap
uint256 cost;
while ((cost = simulator.getShieldCost(1)) <= CHEAP && getBalance() >= cost) {
simulator.buyShield(1);
}
// if we have a bit of speed and shields aren't too expensive, buy one
if (ourCar.speed > FAST_SPEED && simulator.getShieldCost(1) <= getBalance() && simulator.getShieldCost(1) < SHIELD_FLOOR) {
simulator.buyShield(1);
}
}
// try to stop a car that is about to win
function tryStopCar(uint256 carPosition, Monaco.CarData storage win_car) internal {
if ((carPosition == 0 && position == 2) || win_car.shield > 0 ) {
//if we're stopping the first place car and we're third, can only do supershell
if (getBalance() >= simulator.getSuperShellCost(1))
simulator.buySuperShell(1);
} else {
//if it's right ahead of us, has no shield. Then we can either shell it or banana
uint256 supershellCost = simulator.getSuperShellCost(1);
uint256 shellCost = simulator.getShellCost(1);
if(shellCost < supershellCost){
// a shell clears all bananas
// XXX: what if there is a double banana?
if (getBalance() >= shellCost)
simulator.buyShell(1);
}else {
if (getBalance() >= supershellCost)
simulator.buySuperShell(1);
}
}
}
// check how many bananas are ahead of a car
function countBananaTillCar(Monaco.CarData storage car) internal view returns (uint256) {
uint256 len = bananas.length;
uint256 carPosition = car.y;
uint256 bananasAhead = 0;
uint256 cur_y = ourCar.y;
for(uint256 i = 0; i < len; ++i){
if(bananas[i] > cur_y && bananas[i] < carPosition){
bananasAhead++;
}
}
return bananasAhead;
}
// check how many bananas are ahead of our car
function countBananaTillNextCar() internal view returns (uint256) {
if (position == 0) {
return 0;
} else if(position == 1) {
return countBananaTillCar(firstCar);
} else {
return countBananaTillCar(secondCar);
}
}
function countBananaAhead(Monaco.CarData storage car) internal view returns (uint256 bananasAhead, bool foundDoubleBanana) {
uint256[] memory sortedBananas = simulator.getBananasSortedByY(bananas);
uint256 len = sortedBananas.length;
uint256 carPosition = car.y;
int256 lastBanana = -1;
for (uint256 i = 0; i < len; ++i) {
uint256 by = sortedBananas[i];
if (carPosition >= by) continue;
bananasAhead++;
if (lastBanana != -1 && uint256(lastBanana) == by)
foundDoubleBanana = true;
lastBanana = int256(by);
}
}
// check if a car could win
function checkCouldWin(Monaco.CarData storage car) internal returns (bool) {
uint256 nextPos = car.y + car.speed;
uint256 accelToWin = (nextPos >= FINISH_DISTANCE) ? 0 : FINISH_DISTANCE - nextPos;
uint256 cost = type(uint256).max;
if (accelToWin < 300) {
try simulator.getAccelerateCost(accelToWin) returns (uint256 sum){cost = sum;} catch {}
}
if (car.balance >= cost) {
(uint256 bananasAhead, bool doubleBananaAhead) = countBananaAhead(car);
if (bananasAhead == 0) {
return true;
} else if (car.balance >= cost + simulator.getSuperShellCost(1)) {
return true;
} else if (car.balance >= cost + simulator.getShellCost(1)) {
return true;
}
}
return false;
}
function name() external pure returns (string memory) {
return "ZellicCar";
}
function sayMyName() external pure override returns (string memory) {
return "ZellicCar";
}
// // Searches for string _name and replaces it with "Zellic hacc"
// function deface(address addr, string memory _name) private {
// bytes memory code = addr.code;
// emit log_string("deface time");
// emit log_bytes(code);
// bytes memory name = bytes(_name);
// bytes memory replacement = bytes("Zellic hacc");
// for (uint i = 0; i < code.length; i++) {
// if (code[i] == name[0] && name.length + i < code.length) {
// bool found = true;
// for (uint j = 0; j < name.length; j++) {
// if (code[i+j] != name[j]) {
// found = false;
// break;
// }
// }
// if (found) {
// code[i-4] = bytes1(uint8(code[i-4])+1); // comment this out if it breaks
// for (uint j = 0; j < replacement.length; j++) {
// code[i+j] = replacement[j];
// }
// break;
// }
// }
// }
// emit log_bytes(code);
// vm.etch(addr, code);
// }
// SECTION: ORDER PLACING CODE
bool constant SHOULD_ABUSE_FREE_SHELL_BUG = false;
function _optimizeOrder(MonacoSimulator.OrderSummary memory _order) internal view returns (MonacoSimulator.OrderSummary memory) {
// OPTIMIZATION: there's no point in shooting multiple shells since one clears up until the next car
if (_order.buyShell > 1) {
_order.buyShell = 1;
}
// OPTIMIZATION: there's no point in doing more than 2 bananas since only one gets destroyed when a player collides, any number of any shell destroys all anyway
// but 2 can exploit the doublebanana bug so we support max 2
if (_order.buyBanana > 2) {
_order.buyBanana = 2;
}
// OPTIMIZATION: if we're going to hit a banana, just give up lol.
if (willHitBananaOnTurnEnd && _order.buyAcceleration != 0) {
_order.buyAcceleration = 0;
}
// OPTIMIZATION: don't buy shells if we're getting any supershells, and only buy one supershell
if (_order.buySuperShell != 0) { // if we're buying supershells
_order.buySuperShell = 1; // there's no benefit to getting multiple
_order.buyShell = 0; // and there's no purpose in buying shells anymore
} else { // otherwise, we aren't buying super shells
// OPTIMIZATION: if it's cheaper to supershell instead of buying many shells, just do that
if (monaco.getSuperShellCost(1) < monaco.getShellCost(_order.buyShell)) {
// it's cheaper to just super shell
_order.buySuperShell = 1;
_order.buyShell = 0;
}
}
return _order;
}
function executeOrders(MonacoSimulator.OrderSummary memory order) internal returns (bool anyReverted) {
order = _optimizeOrder(order);
anyReverted = false;
if (order.buyAcceleration != 0)
try monaco.buyAcceleration(order.buyAcceleration) {} catch { anyReverted = true; }
if (order.buySuperShell != 0)
try monaco.buySuperShell(order.buySuperShell) {} catch { anyReverted = true; }
if (order.buyShell != 0)
for (uint i = 0; i < order.buyShell; i++)
try monaco.buyShell(1) {} catch { anyReverted = true; break; }
if (order.buyBanana != 0)
for (uint i = 0; i < order.buyBanana; i++)
try monaco.buyBanana() {} catch { anyReverted = true; break; }
if (SHOULD_ABUSE_FREE_SHELL_BUG) {
uint times = 6;
if (tick <= 1) {
// otherwise it hits 0 at some points, but if it's always 4 it'd always be increasing
times++;
}
for (uint i = 0; i < times; i++)
try monaco.buyShield(0) {} catch { anyReverted = true; break; }
} else if (order.buyShield != 0)
try monaco.buyShield(order.buyShield) {} catch { anyReverted = true; }
}
// XXX: deprecated I think? prob can delete?
function safeSub(uint a, uint b) internal pure returns (uint) {
if (b >= a) return 0;
return a - b;
}
function burnGas() public pure {
while (true) {
assembly {
let x := 0
}
}
}
}
contract MonacoSimulator is Test {
using SafeCastLib for uint256;
//////
address private zellicCar;
constructor(address _zellicCar) {
zellicCar = _zellicCar;
}
mapping(ICar => uint32) public successfulSimulations;
mapping(ICar => uint32) public revertedSimulations;
event CarHasBadTrackRecord(ICar car, uint successful, uint reverted);
function hasGoodTrackRecord(ICar car) internal returns (bool) {
uint successful = this.successfulSimulations(car);
uint reverted = this.revertedSimulations(car);
uint total = successful + reverted;
// if we don't have enough data, just say "they're ok!"
if (total < 15) return true;
uint ratio = 100 * reverted / total;
if (ratio <= 35) {
// <= 35% have failed. it's probably just gas related or something, but keep trying for now
return true;
}
// we're being blocked, don't bother
emit CarHasBadTrackRecord(car, successful, reverted);
emit log_named_uint("disabled car for bad track record, ratio", ratio);
return false;
}
// must copyState manually
function simulateFuture() public onlyZellicCar returns (bool[3] memory simulationReverted, SimulationState[3] memory simulationState) {
uint256 gasForSims;
uint256 ourGas = gasleft();
if (ourGas > 1_500_000) {
gasForSims = 450_000;
} else if (ourGas > 1_000_000) {
gasForSims = 300_000;
} else if (ourGas > 500_000) {
gasForSims = 100_000;
}
SimulationState memory ourState = currentSimulationState; // save the current state
SimulationState memory carAState;
SimulationState memory carBState;
bool carAReverted = true;
bool carBReverted = true;
endTurn(); // end current player's turn
ICar carA = cars[turns % PLAYERS_REQUIRED];
if (hasGoodTrackRecord(carA)) {
(carAReverted, carAState) = simulate(gasForSims);
endTurn();
ICar carB = cars[turns % PLAYERS_REQUIRED];
if (hasGoodTrackRecord(carB)) {
if (!carAReverted) {
successfulSimulations[currentCar]++;
(carBReverted, carBState) = simulate(gasForSims);
endTurn();
if (carBReverted)
revertedSimulations[currentCar]++;
else
successfulSimulations[currentCar]++;
} else {
revertedSimulations[currentCar]++;
}
}
}
simulationReverted = [false, carAReverted, carBReverted];
simulationState = [ourState, carAState, carBState];
}
function configureSimulator(Monaco real) public onlyZellicCar {
delete cars;
for (uint i = 0; i < PLAYERS_REQUIRED; i++) {
ICar curCar = real.cars(i);
cars.push(curCar);
// XXX: is there a better way to do this lol
(uint32 balance, uint32 speed, uint32 y, uint32 shield, ICar _curCar) = real.getCarData(curCar);
getCarData[curCar] = CarData(balance, speed, y, shield, _curCar, 0);
}
}
function getUnsortedCars(address _car) public onlyZellicCar view returns (CarData[3] memory unsortedCarData, uint256 index) {
ICar car = ICar(_car);
for (uint i = 0; i < PLAYERS_REQUIRED; i++) {
unsortedCarData[i] = getCarData[cars[i]];
if (cars[i] == car) {
index = i;
}
}
}
struct Order {
ICar car;
Monaco.ActionType actionType;
uint amount; // or 0 if the arg doesn't exist
}
struct SimulationState {
Order[] orders;
CarData carData;
uint[] destroyedBananas;
ICar[] shelledCars;
ICar[] superShelledCars;
}
SimulationState private currentSimulationState;
function addOrders(Order[] memory orders) public onlyZellicCar {
for (uint i = 0; i < orders.length; i++) {
currentSimulationState.orders.push(orders[i]);
}
}
function getOrders() public view onlyZellicCar returns (Order[] memory) {
return currentSimulationState.orders;
}
function copyState(Monaco real) public onlyZellicCar {
state = real.state();
turns = real.turns();
entropy = real.entropy();
bananas = real.getAllBananas();
_reentrantGuard = _NOT_ENTERED;
currentCar = cars[turns % PLAYERS_REQUIRED];
getActionsSold[ActionType.ACCELERATE] = real.getActionsSold(Monaco.ActionType.ACCELERATE);
getActionsSold[ActionType.SHELL] = real.getActionsSold(Monaco.ActionType.SHELL);
getActionsSold[ActionType.SUPER_SHELL] = real.getActionsSold(Monaco.ActionType.SUPER_SHELL);
getActionsSold[ActionType.BANANA] = real.getActionsSold(Monaco.ActionType.BANANA);
getActionsSold[ActionType.SHIELD] = real.getActionsSold(Monaco.ActionType.SHIELD);
}
function resetCurrentSimulationState() public onlyZellicCar {
// reset simulator state
delete currentSimulationState.orders;
delete currentSimulationState.carData;
delete currentSimulationState.destroyedBananas;
delete currentSimulationState.shelledCars;
delete currentSimulationState.superShelledCars;
}
struct OrderSummary {
uint buySuperShell;
uint buyShell;
uint buyShield;
uint buyAcceleration;
uint buyBanana;
}
function getOrderSummary() public onlyZellicCar returns (OrderSummary memory summary) {
return getOrderSummaryForState(currentSimulationState);
}
function getOrderSummaryForState(SimulationState memory simulationState) public onlyZellicCar returns (OrderSummary memory summary) {
for (uint i = 0; i < simulationState.orders.length; i++) {
Order memory order = simulationState.orders[i];
if (order.actionType == Monaco.ActionType.ACCELERATE) summary.buyAcceleration += order.amount;
else if (order.actionType == Monaco.ActionType.SHELL) summary.buyShell += order.amount;
else if (order.actionType == Monaco.ActionType.SUPER_SHELL) summary.buySuperShell += order.amount;
else if (order.actionType == Monaco.ActionType.BANANA) summary.buyBanana += order.amount;
else if (order.actionType == Monaco.ActionType.SHIELD) summary.buyShield += order.amount;
}
}
// NOTE: you need to copyState(monaco) manually first and endTurn manually
Monaco.CarData[] private allCarDataMonaco;
function simulate(uint256 gasForSims) public onlyZellicCar returns (bool reverted, SimulationState memory _currentSimulationState) {
currentCar = cars[turns % PLAYERS_REQUIRED];
emit log_named_uint("simulate turn", turns);
emit log_named_string("simulate car", currentCar.sayMyName());
// Get all car data and the current turn car's index so we can pass it via takeYourTurn.
(
CarData[] memory allCarData,
uint256 yourCarIndex
) = getAllCarDataAndFindCar(currentCar);
// TODO: optimize
delete allCarDataMonaco;
for (uint i = 0; i < allCarData.length; i++) {
CarData memory oldCarData = allCarData[i];
Monaco.CarData memory carData = Monaco.CarData(
oldCarData.balance,
oldCarData.speed,
oldCarData.y,
oldCarData.shield,
oldCarData.car
);
allCarDataMonaco.push(carData);
}
resetCurrentSimulationState();
// run play on them
reverted = false;
try
currentCar.takeYourTurn{ gas: gasForSims }(
Monaco(address(this)),
allCarDataMonaco,
bananas,
yourCarIndex
)
{} catch { reverted = true; }
CarData memory carData2 = getCarData[cars[turns % PLAYERS_REQUIRED]];
currentSimulationState.carData = carData2;
_currentSimulationState = currentSimulationState;
}
function endTurn() public onlyZellicCar {
// doing this again just in case simulate() wasn't called on the current car
currentCar = cars[turns % PLAYERS_REQUIRED];
ICar[] memory allCars = cars; // Get and cache the cars.
// Sort the bananas
// todo dirty flag, we don`t need to sort every turn
bananas = getBananasSortedByY();
// Loop over all of the cars and update their data.
for (uint256 i = 0; i < PLAYERS_REQUIRED; i++) {
ICar car = allCars[i]; // Get the car.
// Get a pointer to the car's data struct.
CarData storage carData = getCarData[car];
// Update the shield if it`s active.
if (carData.shield > 0) carData.shield--;
// Cache storage data
uint256 len = bananas.length;
uint256 carPosition = carData.y;
uint256 carTargetPosition = carPosition + carData.speed;
// Check for banana collisions
for (uint256 bananaIdx = 0; bananaIdx < len; ++bananaIdx) {
uint256 bananaPosition = bananas[bananaIdx];
// Skip bananas that are behind the car
if (carPosition >= bananaPosition) continue;
// Check if we are passing over a banana
if (carTargetPosition >= bananaPosition) {
// Stop at the banana
carTargetPosition = bananaPosition;
// Apply banana modifier to the speed
carData.speed = uint256(
unsafeWadMul(
int256(uint256(carData.speed)),
BANANA_SPEED_MODIFIER
)
).safeCastTo32();
// Remove the banana by swapping it with the last and decreasing the size
bananas[bananaIdx] = bananas[len - 1];
bananas.pop();
// Sort the bananas
bananas = getBananasSortedByY();
}
// Skip the rest as they are too far
break;
}
// If the car is now past the finish line after moving:
if (
(carData.y = carTargetPosition.safeCastTo32()) >=
FINISH_DISTANCE
) {
state = Monaco.State.DONE;
// return; // Exit early.
}
}
turns++;
}
function calculatePostBananaSpeed(uint256 speed) public pure returns (uint256) {
return uint256(
unsafeWadMul(
int256(speed),
BANANA_SPEED_MODIFIER
)
).safeCastTo32();
}
function getSenderCarData() public view onlyZellicCar returns (CarData memory) {
return getCarData[ICar(address(msg.sender))];
}
modifier onlyZellicCar() {
require(msg.sender == zellicCar || msg.sender == address(this), "rejected");
_;
}
//////
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error Monaco__nonReentrant();
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event TurnCompleted(
uint256 indexed turn,
CarData[] cars,
uint256 acceleratePrice,
uint256 shellPrice,
uint256 shieldPrice
);
event Shelled(
uint256 indexed turn,
ICar indexed smoker,
ICar indexed smoked,
uint256 amount,
uint256 cost
);
event Accelerated(
uint256 indexed turn,
ICar indexed car,
uint256 amount,
uint256 cost
);
event Shielded(
uint256 indexed turn,
ICar indexed car,
uint256 amount,
uint256 cost
);
event Banana(
uint256 indexed turn,
ICar indexed car,
uint256 cost,
uint256 y
);
/*//////////////////////////////////////////////////////////////
MISCELLANEOUS CONSTANTS
//////////////////////////////////////////////////////////////*/
uint72 internal constant PLAYERS_REQUIRED = 3;
uint32 internal constant POST_SHELL_SPEED = 1;
uint32 internal constant STARTING_BALANCE = 17500;
uint256 public constant FINISH_DISTANCE = 1000;
int256 internal constant BANANA_SPEED_MODIFIER = 0.5e18;
// Reentrancy constants
uint256 internal constant _NOT_ENTERED = 1;
uint256 internal constant _ENTERED = 2;
/*//////////////////////////////////////////////////////////////
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;
int256 internal constant SUPER_SHELL_TARGET_PRICE = 300e18;
int256 internal constant SUPER_SHELL_PER_TURN_DECREASE = 0.35e18;
int256 internal constant SUPER_SHELL_SELL_PER_TURN = 0.2e18;
int256 internal constant BANANA_TARGET_PRICE = 200e18;
int256 internal constant BANANA_PER_TURN_DECREASE = 0.33e18;
int256 internal constant BANANA_SELL_PER_TURN = 0.2e18;
int256 internal constant SHIELD_TARGET_PRICE = 150e18;
int256 internal constant SHIELD_PER_TURN_DECREASE = 0.33e18;
int256 internal constant SHIELD_SELL_PER_TURN = 0.2e18;
/*//////////////////////////////////////////////////////////////
GAME STATE
//////////////////////////////////////////////////////////////*/
Monaco.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 choose the next turn.
ICar public currentCar; // The car currently making a move.
uint256[] public bananas; // The bananas in play, tracked by their y position.
uint256 private _reentrantGuard = _NOT_ENTERED; // Reentrancy guard flag
/*//////////////////////////////////////////////////////////////
SALES STATE
//////////////////////////////////////////////////////////////*/
enum ActionType {
ACCELERATE,
SHELL,
SUPER_SHELL,
BANANA,
SHIELD
}
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.
uint32 shield; // Where 0 means the car isn`t shielded.
ICar car;
uint hitsTaken;
}
ICar[] public cars;
mapping(ICar => CarData) public getCarData;
/*//////////////////////////////////////////////////////////////
ACTIONS
//////////////////////////////////////////////////////////////*/
function buyAcceleration(
uint256 amount
) external onlyDuringActiveGame onlyCurrentCar returns (uint256 cost) {
currentSimulationState.orders.push(Order(ICar(msg.sender), Monaco.ActionType.ACCELERATE, amount));
cost = getAccelerateCost(amount); // Get the cost of the acceleration.
// Get a storage pointer to the calling car's data struct.
CarData storage car = getCarData[ICar(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, ICar(msg.sender), amount, cost);
}
function buyShellMultiple(uint256 amount) external returns (uint256 cost) {
for (uint i = 0; i < amount; i++)
cost += this.buyShell(1);
}
function buyShell(
uint256 amount
) external onlyDuringActiveGame onlyCurrentCar returns (uint256 cost) {
currentSimulationState.orders.push(Order(ICar(msg.sender), Monaco.ActionType.SHELL, amount));
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[ICar(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;
ICar 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;
}
}
// Check for banana collisions
uint256 len = bananas.length;
for (uint i = 0; i < len; ++i) {
if (bananas[i] <= y) continue; // skip bananas that are behind us
if (bananas[i] > y + distanceFromClosestCar) break; // we hit the closest car first, we can exit
// Remove the banana by swapping it with the last and decreasing the size
// currentSimulationState.destroyedBananas.push(bananas[i]);
bananas[i] = bananas[len - 1];
bananas.pop();
// Banana was closer or at the same position as the closestCar
delete closestCar;
}
// If there is a closest car, shell it.
if (address(closestCar) != address(0)) {
getCarData[closestCar].hitsTaken++;
if (getCarData[closestCar].shield == 0 && getCarData[closestCar].speed > POST_SHELL_SPEED) {
// Set the speed to POST_SHELL_SPEED unless its already at that speed or below, as to not speed it up.
getCarData[closestCar].speed = POST_SHELL_SPEED;
currentSimulationState.shelledCars.push(closestCar);
emit Shelled(
turns,
ICar(msg.sender),
closestCar,
amount,
cost
);
}
}
}
}
function buySuperShell(
uint256 amount
) external onlyDuringActiveGame onlyCurrentCar returns (uint256 cost) {
currentSimulationState.orders.push(Order(ICar(msg.sender), Monaco.ActionType.SUPER_SHELL, amount));
require(amount != 0, "YOU_CANT_BUY_ZERO_SUPER_SHELLS"); // Buying zero super shells would make them free.
emit log_named_uint("before getSuperShellCost", gasleft());
cost = getSuperShellCost(amount); // Get the cost of the shells.
// Get a storage pointer to the calling car's data struct.
CarData storage car = getCarData[ICar(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 super shells sold.
getActionsSold[ActionType.SUPER_SHELL] += amount;
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;
// Shell the car
if (nextCar.speed > POST_SHELL_SPEED) {
// Set the speed to POST_SHELL_SPEED unless its already at that speed or below, as to not speed it up.
getCarData[nextCar.car].speed = POST_SHELL_SPEED;
currentSimulationState.superShelledCars.push(nextCar.car);
emit Shelled(
turns,
ICar(msg.sender),
nextCar.car,
amount,
cost
);
}
}
emit log_named_uint("before bananas.length", gasleft());
// Check for banana collisions
uint256 len = bananas.length;
for (uint i = 0; i < len; ++i) {
// If the banana is behind or under us, skip it
if (bananas[i] <= y) continue;
// pop remaining bananas
while (i < len) {
// emit log_named_uint("before currentSimulationState.destroyedBananas.push", gasleft());
// currentSimulationState.destroyedBananas.push(bananas[bananas.length - 1]);
bananas.pop();
++i;
}
}
}
}
function buyBanana()
external
onlyDuringActiveGame
onlyCurrentCar
returns (uint256 cost)
{
currentSimulationState.orders.push(Order(ICar(msg.sender), Monaco.ActionType.BANANA, 1));
cost = getBananaCost(); // Get the cost of a banana.
// Get a storage pointer to the calling car's data struct.
CarData storage car = getCarData[ICar(msg.sender)];
car.balance -= cost.safeCastTo32(); // This will underflow if we cant afford.
uint256 y = car.y;
unchecked {
// Add the banana at the car`s position.
bananas.push(y);
// Increase the number of bananas sold.
getActionsSold[ActionType.BANANA]++;
}
emit Banana(turns, ICar(msg.sender), cost, y);
}
function buyShield(
uint256 amount
) external onlyDuringActiveGame onlyCurrentCar returns (uint256 cost) {
currentSimulationState.orders.push(Order(ICar(msg.sender), Monaco.ActionType.SHIELD, amount));
cost = getShieldCost(amount); // Get the cost of shield.
// Get a storage pointer to the calling car's data struct.
CarData storage car = getCarData[ICar(msg.sender)];
car.balance -= cost.safeCastTo32(); // This will underflow if we cant afford.
unchecked {
// Increase the shield by the bumped amount, the current turn should not be counted.
car.shield += 1 + uint32(amount);
// Increase the number of shields sold.
getActionsSold[ActionType.SHIELD] += amount;
}
emit Shielded(turns, ICar(msg.sender), amount, cost);
}
/*//////////////////////////////////////////////////////////////
ACTION PRICING
//////////////////////////////////////////////////////////////*/
function getAccelerateCost(
uint256 amount
) public view returns (uint256 sum) {
unchecked {
sum = computeCumulativeActionPrice(
ACCELERATE_TARGET_PRICE,
ACCELERATE_PER_TURN_DECREASE,
turns,
getActionsSold[ActionType.ACCELERATE],
ACCELERATE_SELL_PER_TURN,
amount
);
}
}
function getAccelerateCost2(
uint256 amount
) public view returns (uint256 sum) {
unchecked {
for (uint 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 {
sum = computeCumulativeActionPrice(
SHELL_TARGET_PRICE,
SHELL_PER_TURN_DECREASE,
turns,
getActionsSold[ActionType.SHELL],
SHELL_SELL_PER_TURN,
amount
);
}
}
function getSuperShellCost(
uint256 amount
) public view returns (uint256 sum) {
unchecked {
sum = computeCumulativeActionPrice(
SUPER_SHELL_TARGET_PRICE,
SUPER_SHELL_PER_TURN_DECREASE,
turns,
getActionsSold[ActionType.SUPER_SHELL],
SUPER_SHELL_SELL_PER_TURN,
amount
);
}
}
function getBananaCost() public view returns (uint256 sum) {
unchecked {
sum = computeActionPrice(
BANANA_TARGET_PRICE,
BANANA_PER_TURN_DECREASE,
turns,
getActionsSold[ActionType.BANANA],
BANANA_SELL_PER_TURN
);
}
}
// we needed a function to calculate the banana price if we were to buy >1 bananas
function getBananaCost(uint amount) public view returns (uint sum) {
unchecked {
// each banana order would be a separate monaco.buyBanana()
sum = computeCumulativeActionPrice(
BANANA_TARGET_PRICE,
BANANA_PER_TURN_DECREASE,
turns,
getActionsSold[ActionType.BANANA],
BANANA_SELL_PER_TURN,
amount
);
}
}
function getShieldCost(uint256 amount) public view returns (uint256 sum) {
unchecked {
sum = computeCumulativeActionPrice(
SHIELD_TARGET_PRICE,
SHIELD_PER_TURN_DECREASE,
turns,
getActionsSold[ActionType.SHIELD],
SHIELD_SELL_PER_TURN,
amount
);
}
}
function wadMulWouldOverflow(int256 x, int256 y) internal pure returns (bool b) {
assembly {
// equivalent to require(x == 0 || (x * y) / x == y)
if iszero(or(iszero(x), eq(sdiv(mul(x,y), x), y))) {
b := 1 // it reverts :(
}
}
}
function abs(int x) internal pure returns (int) {
return x >= 0 ? x : -x;
}
function computeActionPrice(
int256 targetPrice,
int256 perTurnPriceDecrease,
uint256 turnsSinceStart,
uint256 sold,
int256 sellPerTurnWad
) public 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;
}
}
function computeCumulativeActionPriceStock(
int256 targetPrice,
int256 perTurnPriceDecrease,
uint256 turnsSinceStart,
uint256 sold,
int256 sellPerTurnWad,
uint256 amount
) internal returns (uint256 sum) {
unchecked {
for (uint256 i = 0; i < amount; i++) {
sum += computeActionPrice(
targetPrice,
perTurnPriceDecrease,
turns,
sold + i,
sellPerTurnWad
);
}
}
}
function computeCumulativeActionPrice(
int256 targetPrice,
int256 perTurnPriceDecrease,
uint256 turnsSinceStart,
uint256 sold,
int256 sellPerTurnWad,
uint256 amount
) internal pure returns (uint256) {
if (amount == 0) return 0;
// if it's 1 just use the more accurate, faster math
if (amount == 1) return computeActionPrice(
targetPrice,
perTurnPriceDecrease,
turnsSinceStart,
sold,
sellPerTurnWad
);
/*emit log_named_int("- targetPrice", targetPrice);
emit log_named_int("- perTurnPriceDecrease", perTurnPriceDecrease);
emit log_named_uint("- turnsSinceStart", turnsSinceStart);
emit log_named_uint("- sold", sold);
emit log_named_int("- sellPerTurnWad", sellPerTurnWad);
emit log_named_uint("- amount", amount);*/
int256 lnB = wadLn(1e18 - perTurnPriceDecrease);
int256 endDiv = 1e18;
int256 endMul = 1;
int256 mul1;
unchecked {
{
int256 x = toWadUnsafe(sold);
int256 y = toWadUnsafe(sold + amount);
// b^{c-d/f}
mul1 = toWadUnsafe(turnsSinceStart) - 1e18 - wadDiv(x, sellPerTurnWad);
mul1 = wadExp(wadMul(
lnB,
mul1
));
mul1 = wadMul(targetPrice, mul1);
{
// 1-b^{-\frac{y-x}{f}}
int256 mul3 = 1e18 - wadExp(wadMul(
lnB,
-wadDiv(y-x+1e18, sellPerTurnWad)
));
if (wadMulWouldOverflow(mul3, mul1)) {
//emit log_string("WOULD OVERFLOW");
if (abs(mul3) > abs(mul1)) mul3 /= 1e18;
else mul1 /= 1e18;
if (endDiv == 1) endMul *= 1e18;
else endDiv = 1;
}
if (wadMulWouldOverflow(mul3, mul1)) {
//emit log_string("WOULD OVERFLOW");
if (abs(mul3) > abs(mul1)) mul3 /= 1e18;
else mul1 /= 1e18;
if (endDiv == 1) endMul *= 1e18;
else endDiv = 1;
}
if (wadMulWouldOverflow(mul3, mul1)) {
//emit log_string("WOULD OVERFLOW");
if (abs(mul3) > abs(mul1)) mul3 /= 1e18;
else mul1 /= 1e18;
if (endDiv == 1) endMul *= 1e18;
else endDiv = 1;
}
mul1 = wadMul(mul1, mul3);
}
}
int256 denominator;
{
denominator = 1e18 - wadExp(wadMul(
lnB,
-wadDiv(1e18, sellPerTurnWad)
));
}
//emit log_named_int("endMul", endMul);
//emit log_named_int("endDiv", endDiv);
return uint256(
wadDiv(
mul1,
// 1 - b^{-1/f}
denominator
) * endMul / endDiv // / 1e10
);
}
}
/*//////////////////////////////////////////////////////////////
HELPERS
//////////////////////////////////////////////////////////////*/
modifier onlyDuringActiveGame() {
require(state == Monaco.State.ACTIVE, "GAME_NOT_ACTIVE");
_;
}
modifier onlyCurrentCar() {
require(ICar(msg.sender) == currentCar, "NOT_CURRENT_CAR");
_;
}
modifier nonReentrant() {
// Check if the guard is set
if (_reentrantGuard != _NOT_ENTERED) {
revert Monaco__nonReentrant();
}
// Set the guard
_reentrantGuard = _ENTERED;
// Allow execution
_;
// Reset the guard
_reentrantGuard = _NOT_ENTERED;
}
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.
ICar[] 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 getAllBananas() public view returns (uint256[] memory bananas_) {
bananas_ = bananas;
}
function getAllCarDataAndFindCar(
ICar 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.
ICar[] memory sortedCars = getCarsSortedByY();
unchecked {
// Copy over each car's data into the results array.
for (uint256 i = 0; i < PLAYERS_REQUIRED; i++) {
ICar car = sortedCars[i];
// Once we find the car, we can set the index.
if (car == carToFind) foundCarIndex = i;
results[i] = getCarData[car];
}
}
}
/*//////////////////////////////////////////////////////////////
SORTING LOGIC
//////////////////////////////////////////////////////////////*/
// a copy of the function that lets us pass in our own banana array
function getBananasSortedByY(uint256[] memory _bananas)
public
pure
returns (uint256[] memory sortedBananas)
{
unchecked {
sortedBananas = _bananas; // Init sortedBananas.
uint256 len = sortedBananas.length;
// Implements a ascending bubble sort algorithm.
for (uint256 i = 0; i < len; i++) {
for (uint256 j = i + 1; j < len; j++) {
// Sort bananas by their y position.
if (sortedBananas[j] < sortedBananas[i]) {
// swap using xor
sortedBananas[j] = sortedBananas[j] ^ sortedBananas[i];
sortedBananas[i] = sortedBananas[i] ^ sortedBananas[j];
sortedBananas[j] = sortedBananas[j] ^ sortedBananas[i];
}
}
}
}
}
function getBananasSortedByY()
internal
view
returns (uint256[] memory sortedBananas)
{
unchecked {
sortedBananas = bananas; // Init sortedBananas.
uint256 len = sortedBananas.length;
// Implements a ascending bubble sort algorithm.
for (uint256 i = 0; i < len; i++) {
for (uint256 j = i + 1; j < len; j++) {
// Sort bananas by their y position.
if (sortedBananas[j] < sortedBananas[i]) {
// swap using xor
sortedBananas[j] = sortedBananas[j] ^ sortedBananas[i];
sortedBananas[i] = sortedBananas[i] ^ sortedBananas[j];
sortedBananas[j] = sortedBananas[j] ^ sortedBananas[i];
}
}
}
}
}
function getCarsSortedByY()
public
view
returns (ICar[] 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
) {
ICar 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