Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,550 changes: 865 additions & 685 deletions backend/src/abi/aviator.json

Large diffs are not rendered by default.

29 changes: 3 additions & 26 deletions backend/src/services/chain.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,42 +125,19 @@ export class ChainService {
async validatePlayerFunds(player: string, amount: number) {
try {
await this.ensureProviderReady(2);
const chainConfig = getChainConfig(this.chainId);
const usdcToken = chainConfig.usdcAddress;

if (!usdcToken) {
throw new Error(`USDC address not configured for chain ${this.chainId}`);
}

const usdcContract = new ethers.Contract(
usdcToken,
[
'function balanceOf(address) view returns (uint256)',
'function allowance(address owner, address spender) view returns (uint256)'
],
this.provider
);

const contractAddress = await this.contract.getAddress();
const betAmountUint = BigInt(Math.round(amount * 1e6)); // USDC 6 decimals

const [balance, allowance] = await Promise.all([
usdcContract.balanceOf(player),
usdcContract.allowance(player, contractAddress)
]);
const balance = await this.contract.playerBalances(player);

if (BigInt(balance) < betAmountUint) {
return { ok: false, reason: `Insufficient USDC balance. Have ${Number(balance) / 1e6}, need ${amount}` };
}

if (BigInt(allowance) < betAmountUint) {
return { ok: false, reason: `Insufficient allowance. Contract not approved to spend your USDC.` };
return { ok: false, reason: `Insufficient game balance. Have ${Number(balance) / 1e6}, need ${amount}. Please deposit funds.` };
}

return { ok: true };
} catch (err) {
logger.error('Failed to validate player funds', { error: (err as Error).message, player, amount });
return { ok: false, reason: 'Failed to verify balance/allowance: ' + (err as Error).message };
return { ok: false, reason: 'Failed to verify game balance: ' + (err as Error).message };
}
}

Expand Down
1 change: 1 addition & 0 deletions contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Compiler files
cache/
out/
lib/

# Ignores development broadcast logs
!/broadcast
Expand Down

Large diffs are not rendered by default.

131 changes: 131 additions & 0 deletions contracts/broadcast/UpgradeAviatorGame.s.sol/42220/run-latest.json

Large diffs are not rendered by default.

99 changes: 89 additions & 10 deletions contracts/src/AviatorGame.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
PausableUpgradeable
} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import {
Initializable
} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
Expand All @@ -36,6 +37,10 @@ contract AviatorGame is
// Server operator (trusted for game operations)
address public serverOperator;

// Player balances for deposit/withdrawal flow
mapping(address => uint256) public playerBalances;
uint256 public totalPlayerBalances;

// Snapshot storage
struct RoundSnapshotData {
bytes32 snapshotHash;
Expand All @@ -62,6 +67,10 @@ contract AviatorGame is

event ServerOperatorUpdated(address indexed newOperator);

// Player funds events
event Deposit(address indexed player, uint256 amount);
event Withdrawal(address indexed player, uint256 amount);

// Snapshot event
event RoundSnapshot(
uint256 indexed roundId,
Expand Down Expand Up @@ -133,6 +142,74 @@ contract AviatorGame is
if (!success) revert ETHTransferFailed();
}

// ============ Player Deposit / Withdraw Functions ============

/**
* @notice Deposit USDC into the game contract.
* @param amount Amount of USDC to deposit.
*/
function deposit(uint256 amount) external whenNotPaused nonReentrant {
if (amount == 0) revert InvalidBetAmount();

bool success = usdcToken.transferFrom(msg.sender, address(this), amount);
if (!success) revert TransferFailed();

playerBalances[msg.sender] += amount;
totalPlayerBalances += amount;

emit Deposit(msg.sender, amount);
}

/**
* @notice Deposit USDC using ERC-2612 Permit for a single-transaction flow.
* @param amount Amount of USDC to deposit.
*/
function depositWithPermit(
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external whenNotPaused nonReentrant {
if (amount == 0) revert InvalidBetAmount();

// Call permit on the USDC token
IERC20Permit(address(usdcToken)).permit(
msg.sender,
address(this),
amount,
deadline,
v,
r,
s
);

bool success = usdcToken.transferFrom(msg.sender, address(this), amount);
if (!success) revert TransferFailed();

playerBalances[msg.sender] += amount;
totalPlayerBalances += amount;

emit Deposit(msg.sender, amount);
}

/**
* @notice Withdraw deposited USDC and winnings from the game contract.
* @param amount Amount of USDC to withdraw.
*/
function withdraw(uint256 amount) external nonReentrant {
if (amount == 0) revert InvalidBetAmount();
if (playerBalances[msg.sender] < amount) revert InsufficientBalance();

playerBalances[msg.sender] -= amount;
totalPlayerBalances -= amount;

bool success = usdcToken.transfer(msg.sender, amount);
if (!success) revert TransferFailed();

emit Withdrawal(msg.sender, amount);
}

// ============ Core Game Functions ============

/**
Expand All @@ -145,11 +222,11 @@ contract AviatorGame is
uint256 amount
) external nonReentrant whenNotPaused onlyServerOperator {
if (amount < MIN_BET || amount > MAX_BET) revert InvalidBetAmount();
if (playerBalances[player] < amount) revert InsufficientBalance();

// Transfer USDC from player to contract
// Note: The player must have approved the contract to spend this amount
bool success = usdcToken.transferFrom(player, address(this), amount);
if (!success) revert TransferFailed();
// Deduct from player's deposited balance
playerBalances[player] -= amount;
totalPlayerBalances -= amount;

emit BetPlaced(roundId, player, amount);
}
Expand All @@ -165,11 +242,13 @@ contract AviatorGame is
uint256 multiplier
) external nonReentrant whenNotPaused onlyServerOperator {
if (payout > MAX_PAYOUT) revert InsufficientHouseBalance();
if (usdcToken.balanceOf(address(this)) < payout) revert InsufficientHouseBalance();

uint256 houseBalance = usdcToken.balanceOf(address(this)) - totalPlayerBalances;
if (houseBalance < payout) revert InsufficientHouseBalance();

// Transfer winnings in USDC to the player
bool success = usdcToken.transfer(player, payout);
if (!success) revert TransferFailed();
// Credit the payout to the player's balance
playerBalances[player] += payout;
totalPlayerBalances += payout;

emit CashOut(roundId, player, payout, multiplier);
}
Expand Down Expand Up @@ -200,8 +279,8 @@ contract AviatorGame is
}

function withdrawHouseProfits(uint256 amount) external onlyOwner {
if (amount > usdcToken.balanceOf(address(this)))
revert InsufficientBalance();
uint256 houseBalance = usdcToken.balanceOf(address(this)) - totalPlayerBalances;
if (amount > houseBalance) revert InsufficientHouseBalance();

bool success = usdcToken.transfer(owner(), amount);
if (!success) revert TransferFailed();
Expand Down
68 changes: 58 additions & 10 deletions contracts/test/AviatorGame.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -104,19 +104,29 @@ contract AviatorGameTest is Test {
// We act as server operator (this contract is owner and initial operator)
uint256 roundId = 123;

// Player must deposit first
vm.prank(PLAYER);
aviator.deposit(BET_AMOUNT);
assertEq(aviator.playerBalances(PLAYER), BET_AMOUNT);
assertEq(aviator.totalPlayerBalances(), BET_AMOUNT);

vm.expectEmit(true, true, false, true);
emit BetPlaced(roundId, PLAYER, BET_AMOUNT);

aviator.placeBetFor(roundId, PLAYER, BET_AMOUNT);

assertEq(usdc.balanceOf(address(aviator)), BET_AMOUNT);
assertEq(aviator.playerBalances(PLAYER), 0);
assertEq(aviator.totalPlayerBalances(), 0);
assertEq(usdc.balanceOf(address(aviator)), BET_AMOUNT); // House keeps the bet
assertEq(usdc.balanceOf(PLAYER), 1000e6 - BET_AMOUNT);
assertEq(usdc.balanceOf(address(aviator)), BET_AMOUNT);
}

function test_CannotPlaceLowOrHighBet() public {
uint256 roundId = 1;

vm.prank(PLAYER);
aviator.deposit(1000e6); // Deposit the player's full balance

vm.expectRevert(
abi.encodeWithSelector(AviatorGame.InvalidBetAmount.selector)
);
Expand All @@ -143,7 +153,11 @@ contract AviatorGameTest is Test {
function test_CashOutSuccessFlow() public {
uint256 roundId = 123;

// 1. Place bet to fund house
// Player must deposit first
vm.prank(PLAYER);
aviator.deposit(BET_AMOUNT);

// 1. Place bet to fund house (bet becomes house funds)
aviator.placeBetFor(roundId, PLAYER, BET_AMOUNT);
assertEq(usdc.balanceOf(address(aviator)), BET_AMOUNT);

Expand All @@ -159,8 +173,16 @@ contract AviatorGameTest is Test {
aviator.cashOutFor(roundId, PLAYER, payout, 200);

// House balance should decrease
assertEq(usdc.balanceOf(address(aviator)), 0);
// Player should have original balance + winnings (net +1 bet amount)
uint256 houseBal = usdc.balanceOf(address(aviator)) - aviator.totalPlayerBalances();
assertEq(houseBal, 0);

// Player's game balance should have original + winnings
assertEq(aviator.playerBalances(PLAYER), payout);

// Player withdraws
vm.prank(PLAYER);
aviator.withdraw(payout);
assertEq(aviator.playerBalances(PLAYER), 0);
assertEq(usdc.balanceOf(PLAYER), 1000e6 + BET_AMOUNT);
}

Expand Down Expand Up @@ -188,6 +210,9 @@ contract AviatorGameTest is Test {
}

function test_PausePreventsActions() public {
vm.prank(PLAYER);
aviator.deposit(BET_AMOUNT * 2);

aviator.pause();

vm.expectRevert();
Expand All @@ -209,13 +234,13 @@ contract AviatorGameTest is Test {
ERC1967Proxy badProxy = new ERC1967Proxy(address(badImpl), badInit);
AviatorGame bad = AviatorGame(payable(address(badProxy)));

// Attempt to place bet should revert because transferFrom returns false
// Need to be owner/operator to call placeBetFor, which we are (this contract)
// Attempt to deposit should revert because transferFrom returns false

vm.prank(PLAYER);
vm.expectRevert(
abi.encodeWithSelector(AviatorGame.TransferFailed.selector)
);
bad.placeBetFor(1, PLAYER, BET_AMOUNT);
bad.deposit(BET_AMOUNT);
}

function test_SnapshotOnlyServerOperator() public {
Expand Down Expand Up @@ -316,20 +341,43 @@ contract AviatorGameTest is Test {
uint256 bet1 = 100e6;
uint256 bet2 = 200e6;

vm.prank(PLAYER);
aviator.deposit(bet1);

vm.prank(PLAYER2);
aviator.deposit(bet2);

// Player 1 places bet
aviator.placeBetFor(roundId, PLAYER, bet1);

// Player 2 places bet
aviator.placeBetFor(roundId, PLAYER2, bet2);

// House balance should reflect both
// House balance should reflect both (since balances are now 0)
assertEq(usdc.balanceOf(address(aviator)), bet1 + bet2);
assertEq(aviator.totalPlayerBalances(), 0);

// Balances updated
// Wallet Balances updated
assertEq(usdc.balanceOf(PLAYER), 1000e6 - bet1);
assertEq(usdc.balanceOf(PLAYER2), 1000e6 - bet2);
}

function test_DepositAndWithdraw() public {
uint256 amount = 50e6;

vm.prank(PLAYER);
aviator.deposit(amount);
assertEq(aviator.playerBalances(PLAYER), amount);
assertEq(aviator.totalPlayerBalances(), amount);
assertEq(usdc.balanceOf(PLAYER), 1000e6 - amount);

vm.prank(PLAYER);
aviator.withdraw(amount);
assertEq(aviator.playerBalances(PLAYER), 0);
assertEq(aviator.totalPlayerBalances(), 0);
assertEq(usdc.balanceOf(PLAYER), 1000e6);
}

function test_InitializationProtection() public {
vm.expectRevert(); // Initializable: contract is already initialized
aviator.initialize(address(usdc), PLAYER);
Expand Down
9 changes: 7 additions & 2 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-onchain`](https://www.npmjs.com/package/create-onchain).

## Supported Chains

The Aviator game is currently available on:

- **Base** - Fully supported
- **Celo** - Fully supported
- **Stella** - Coming Soon

## Getting Started

First, install dependencies:


```bash
npm install
# or
Expand All @@ -32,7 +38,6 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.


## Learn More

To learn more about OnchainKit, see our [documentation](https://docs.base.org/onchainkit).
Expand Down
Loading
Loading