To register we have to interact with Setup contract by calling the function Register.
1
2
3
4
5
functionregister() external {
require(player ==address(0));
player = msg.sender;
coin.transfer(msg.sender, 1337); // free coins for new players :)
}
For almost every blockchain interaction in this challenge, I used foundry toolkit.
Setup
But before all of that we got to start instance, i will be short on this part but all we had to do is interact with the server and choose to start an instance with the command:
ncat --ssl play-to-earn.chals.sekai.team 1337.
And it will give us the setup address, my address, my private key, and URL and a personnal UUID for identifiying myself and get the flag.
And we can get Coin and ArcadeMachine contracts addresses by using cast on Setup contract as they are public and defined in the latter:
1
2
3
4
5
6
7
8
contractSetup {
Coin public coin;
ArcadeMachine public arcadeMachine;
address player;
...
For Coin : cast call -r $RPC $SETUP "coin()"
For ArcadeMachine : cast call -r $RPC $SETUP "arcadeMachine()"
I set up an environment variable via a file .env file to go faster and hardcoded a way to get Coin and ArcadeMachine address automatically each time I source the .env file with source .env.
pragma solidity0.8.25;
import { Coin } from"./Coin.sol";
import { ArcadeMachine } from"./ArcadeMachine.sol";
contractSetup {
Coin public coin;
ArcadeMachine public arcadeMachine;
address player;
constructor() payable {
coin =new Coin();
arcadeMachine =new ArcadeMachine(coin);
// Assume that many people have played before you ;)
require(msg.value ==20ether);
coin.deposit{value:20ether}();
coin.approve(address(arcadeMachine), 19ether);
arcadeMachine.play(19);
}
functionregister() external {
require(player ==address(0));
player = msg.sender;
coin.transfer(msg.sender, 1337); // free coins for new players :)
}
functionisSolved() externalviewreturns (bool) {
return player !=address(0) && player.balance >=13.37ether;
}
}
I can register by interacting with setup contract : cast send -r $RPC --private-key $PKEY $SETUP "register()" which grant me 1337 coins.
Using withdraw() from Coin contract I can convert those coins to ether.
Also the challenge name Play to earn stand for the ability to play the arcade machine using the function play(uint256). uint256 is the type of the argument taken by the function play().
1
2
3
4
5
functionplay(uint256 times) external {
// burn the coins
require(coin.transferFrom(msg.sender, address(0), 1ether* times));
// Have fun XD
}
It calls transferFrom(src, dst) so it transfer something from sender to address(0) (which is an address with only zero, so it corresponds to no one, its just here like a void, keeping all our gambled coins …) : 0x0000000000000000000000000000000000000000)
If dst != src and allowance[src][sender] (interpreted as the allowance of the sender on src allowance list) != 115792089237316195423570985008687907853269984665640564039457584007913129639936 (these conditions are skipped in this challenge if you transfer coin from yur account to another account)
Then it require msg.sender to have enough allowance on src account.
Meaning that if you want to transfer coins from another account you’re not the owner, you have to be allowed to transfer a certain amount. And this amount is stored in allowance mapping (Coin.sol:line 23).
So it transfers coin from src account to dst account (and if src != msg.sender, substract the value to transfer from msg.sender allowance on allowance[src][msg.sender] so we can’t transfer an infinite amount) and then substract the transferred amount from src coin balance and increase dst balance.
Then, play(uint256 times) function transfer 1 ether == 10**18 * times to msg.sender coin balance to address(0) coin balance. So you’re coin balance needs to have at least 10^18 coins.
From the challenge scenario, play(19) is used at setup that’s why there is a comment saying people played before us.
1
2
3
4
5
6
7
8
9
10
constructor() payable {
coin =new Coin();
arcadeMachine =new ArcadeMachine(coin);
// Assume that many people have played before you ;)
require(msg.value ==20ether);
coin.deposit{value:20ether}();
coin.approve(address(arcadeMachine), 19ether);
arcadeMachine.play(19);
}
So, address(0) got a balance of 19 * 10**18 coins, which will makes 19 ether if they are withdrawn.
To get these we need to find a way to transfer coins from address(0) balance to our balance using transferFrom(), for that we need to have an allowance amount superior or equal to the value we want to transfer and then withdraw them.
To make short, we have two interesting functions, privilegeWithdraw and permit.
The first one transfer address(0) coin to msg.sender but we need to be the owner of the contract so it sucks.
The second one allow us to set allowance value for a spender address on owner address if we manage to confirm that spender is the owner (specified in the function, not the contract owner) by verifiying a signature with ecdsa.
I can now check my allowance on address(0) allowance mapping : cast call $COIN -r $RPC "allowance(address, address)(uint)" $(cast address-zero) $ADDRESS -> 1.8e+19
I now transfer my self the coin : cast send -r $RPC --private-key $PKEY $COIN "transferFrom(address,address,uint256)" $(cast address-zero) $ADDRESS 18000000000000000000
And withdraw them : cast send $COIN -r $RPC --private-key $PKEY "withdraw(uint)" 18000000000000000000 -> 18.999999999999776957
Now i can interact with instance, submit my uuid and get the flag : SEKAI{0wn3r:wh3r3_4r3_mY_c01n5_:<}