Created
February 1, 2023 14:38
-
-
Save zellic-audit/84f976e3fa69b0643a83b07ae8049cf9 to your computer and use it in GitHub Desktop.
Zellic Cars for 0xtitans
This file contains 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.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; | |
} | |
} | |
} | |
} | |
} | |
} | |
This file contains 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.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