My Coding Journey: From Finance to Smart Contract Auditor

Day 106

Code
AuthoriseExecutor

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

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";

/**
 * @title AuthorizedExecutor
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
abstract contract AuthorizedExecutor is ReentrancyGuard {
    using Address for address;

    bool public initialized;

    // action identifier => allowed
    mapping(bytes32 => bool) public permissions;

    error NotAllowed();
    error AlreadyInitialized();

    event Initialized(address who, bytes32[] ids);

    /**
     * @notice Allows first caller to set permissions for a set of action identifiers
     * @param ids array of action identifiers
     */
    function setPermissions(bytes32[] memory ids) external {
        if (initialized) {
            revert AlreadyInitialized();
        }

        for (uint256 i = 0; i < ids.length;) {
            unchecked {
                permissions[ids[i]] = true;
                ++i;
            }
        }
        initialized = true;

        emit Initialized(msg.sender, ids);
    }

    /**
     * @notice Performs an arbitrary function call on a target contract, if the caller is authorized to do so.
     * @param target account where the action will be executed
     * @param actionData abi-encoded calldata to execute on the target
     */
    function execute(address target, bytes calldata actionData) external nonReentrant returns (bytes memory) {
        // Read the 4-bytes selector at the beginning of `actionData`
        bytes4 selector;
        uint256 calldataOffset = 4 + 32 * 3; // calldata position where `actionData` begins
        assembly {
            selector := calldataload(calldataOffset)
        }

        if (!permissions[getActionId(selector, msg.sender, target)]) {
            revert NotAllowed();
        }

        _beforeFunctionCall(target, actionData);

        return target.functionCall(actionData);
    }

    function _beforeFunctionCall(address target, bytes memory actionData) internal virtual;

    function getActionId(bytes4 selector, address executor, address target) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(selector, executor, target));
    }
}

SelfAuthorisedVault

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "solady/src/utils/SafeTransferLib.sol";
import "./AuthorizedExecutor.sol";

/**
 * @title SelfAuthorizedVault
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract SelfAuthorizedVault is AuthorizedExecutor {
    uint256 public constant WITHDRAWAL_LIMIT = 1 ether;
    uint256 public constant WAITING_PERIOD = 15 days;

    uint256 private _lastWithdrawalTimestamp = block.timestamp;

    error TargetNotAllowed();
    error CallerNotAllowed();
    error InvalidWithdrawalAmount();
    error WithdrawalWaitingPeriodNotEnded();

    modifier onlyThis() {
        if (msg.sender != address(this)) {
            revert CallerNotAllowed();
        }
        _;
    }

    /**
     * @notice Allows to send a limited amount of tokens to a recipient every now and then
     * @param token address of the token to withdraw
     * @param recipient address of the tokens' recipient
     * @param amount amount of tokens to be transferred
     */
    function withdraw(address token, address recipient, uint256 amount) external onlyThis {
        if (amount > WITHDRAWAL_LIMIT) {
            revert InvalidWithdrawalAmount();
        }

        if (block.timestamp <= _lastWithdrawalTimestamp + WAITING_PERIOD) {
            revert WithdrawalWaitingPeriodNotEnded();
        }

        _lastWithdrawalTimestamp = block.timestamp;

        SafeTransferLib.safeTransfer(token, recipient, amount);
    }

    function sweepFunds(address receiver, IERC20 token) external onlyThis {
        SafeTransferLib.safeTransfer(address(token), receiver, token.balanceOf(address(this)));
    }

    function getLastWithdrawalTimestamp() external view returns (uint256) {
        return _lastWithdrawalTimestamp;
    }

    function _beforeFunctionCall(address target, bytes memory) internal view override {
        if (target != address(this)) {
            revert TargetNotAllowed();
        }
    }
}

Instructions
There’s a permissioned vault with 1 million DVT tokens deposited. The vault allows withdrawing funds periodically, as well as taking all funds out in case of emergencies.

The contract has an embedded generic authorization scheme, only allowing known accounts to execute specific actions. The dev team has received a responsible disclosure saying all funds can be stolen.

Before it’s too late, rescue all funds from the vault, transferring them back to the recovery account.

Breakdown
The key function to look to is contained in AuthorisedExecutor:

function execute(address target, bytes calldata actionData) external nonReentrant returns (bytes memory) {
        // Read the 4-bytes selector at the beginning of `actionData`
        bytes4 selector;
        uint256 calldataOffset = 4 + 32 * 3; // calldata position where `actionData` begins @audit-issue
        assembly {
            selector := calldataload(calldataOffset) // loads 32 bytes starting from 100 bytes shift in calldata
        }

        if (!permissions[getActionId(selector, msg.sender, target)]) {
            revert NotAllowed();
        }

        _beforeFunctionCall(target, actionData);

        return target.functionCall(actionData);
    }
  • functionCall executed a function call when the function selector from actionData is allowed for msg.sender; and
  • assumes calldataOffset is correctly assembled and correct selector will be assigned.

Solution
Remember previous posts where I have mentioned that calldata is the actual data sent in a transaction. It is encoded by concatenating the function selector to be triggered by the called contract, followed by all the arguments.

Basically, the way this function is built allows us to change actionData’s calldata (basically set it to sweepfunds).

  1. Have the following 4 bytes after 100th position(4 + 32 * 3) occupied with a function selector authorized for the caller. In our case, player is authorized to use withdraw;
  2. actionData 's size and content, containing the working calldata (sweepFunds) to drain the vault to the recovery address;
  3. Point actionData 's beginning to the new position; and
  4. Fill the freed space in between with zeroes.

Day 107

The Pike protocol was exploited by two hacks during the week (total loss ~US$1.68m):

Hack 1 (April 26)

This attack resulted in financial loss of 299,127 USDC incurred across 3 networks β€” Ethereum, Arbitrum, and Optimism, noting that only USDC assets were affected.

The critical flaw was in functions designed for burning USDC on a source chain and minting on a target chain. I investigated this a little further and the protocol referred to here is the Cross-Chain Transfer Protocol, designed by Circle. The purpose of this protocol is to enable USDC to be transferred across blockchains by minting and burning them rather than using a bridged version of the token.

For those unaware, when I say 'bridged' what I mean is best shown through example:

  • contract A holds 100 USDC tokens;
  • you are unable to transfer these USDC tokens to a chain running contract B;
  • contract B creates a token on-chain that represents a 1:1 value of the USDC token stored in contract A;
  • various smart contracts are set up in contract B that keep track of everything you transfer and use (almost as if you had transferred those tokens entirely from contract A to B); and
  • at the end of the transaction, say you want to sell the tokens back to contract A, the representative tokens in contract B are burnt and the actual USDC is re-accessible to the user.

The point I am trying to make is that the USDC token is not actually transferred to contract B, contract B merely holds a token that is representative of the USDC asset for interoperability purposes.

So, for Pike to have an entirely different measure to bridging, it is a profound deviation from the norm. The exploit came from the inadequate protection of this CCTP function and allowed attackers to manipulate receiver's address and amounts, which were processed by Pike protocol as valid. What is really interesting to note is that this vulnerability was actually outlined by Ottersec (an auditing firm) but the developer team was unable to address the identified vulnerability.

Remedies to Hack 1

A series of steps were taken

  • Disable USDC withdrawals via CCTP in the current version of Pike;
  • Implement delayed withdrawals for all assets to further enhance security; and
  • Unpause protocol operations to allow users to manage their funds.

These measures implies rolling back to Pike protocol version prior enabling their CCTP feature, as well as introducing delayed withdrawals β€” as an additional security measure.

Hack 2 (April 30)

Once the changes had been made to the Pike protocol, the hacker came back due to a new vulnerability...the storage layout.

This type of hack, which time and time again we have discussed in this series, is a real threat. The result of this mismatch is that the position of the initialized variable was taken over by other variables, leading to a misalignment in storage mapping. This misalignment caused the contract to behave as if it was uninitialized, since the initialized variable could no longer be accessed.

Remedies to Hack 2

Pike Beta is sunset, and users will be refunded. Furthermore, an insurance pool will be set up and Pike Beta v2 will commence in the future. See the announcement here.

For me, this hack was a bit of a facepalm because it was so avoidable. I know this is arrogant to say post-hack but to deploy something without seriously considering the security implications explicitly pointed to by an audit team, is quite ridiculous. Perhaps a justification is that we don’t know the commercial drivers behind the decision making at Pike. Irrespective, this hack goes to how important it is to take security findings seriously and ensuring there is a comprehensive series of tests before launch.

Day 108

Considering the Pike Hack I wanted to investigate more attacks that relied on a failure to validate inputs before sending them to the blockchain. As such, I came across short address / parameter attacks.

The nature of this attack is slightly different to those in the Pike case. When passing parameters to smart contracts, the parameters are encoded according to the ABI specification (i.e. the standard way to interact with contracts in the Ethereum ecosystem, both from outside the blockchain and for contract-to-contract interaction). As such, users can send encoded parameters that are shorter than the expected parameter length (e.g. inputting an address that is only 38 hex chars (19 bytes) instead of the standard 40 hex chars (20 bytes)). This is made possible by the EVM padding 0s at the end of the parameter to standardise the expected length.

For example, let’s say an exchange does not verify the address of USDC when the owner requests withdrawal. This is the transfer function:

function transfer(address to, uint USDC) public returns (bool success);

The user would submit their address and the number of USDC they want to withdraw. The exchange would encode these addresses and USDC such that:

  • The first 4 bytes are the transfer function;
  • The second 32 bytes are the address; and
  • The final 32 bytes are the number of USDC.
    But what if we copy and pasted the address wrong such that it was missing the 2 hex digits? As discussed above, two 0s are padded at the end of the encoding to make up for the short address. So? The value of the USDC is now 25600 tokens being withdrawn. This occurs because the address takes the front 00 from the USDC value and the 100 USDC value is changed to 25600 tokens provided it has 00 padded at the end of its hex digits.

To exploit this, a hacker would:

  1. Find a legit registered address;
  2. Brute force an address that omitted the last 2 digits of the encoded address and replace them with 00; and
  3. Call the withdraw function using the encoded value.
    I find this attack vector to be unlikely to occur provided it functions hold a lot of validation checks and also check-effect-interactions patterns are followed. Irrespective, such an attack is incredibly interesting and if others have seen this type of attack let me know because I think it would provide an incredibly interesting case study.

Day 109

Again, Pike is taking a beating from me but there are a few attacks that I have found in relation to this hack. This next one is uninitialised storage pointers.

As I have outlined in other posts when breaking down challenges code, the EVM stores data as either storage or memory. To recap, storage is more gas intensive than memory because it β€˜stores’ values on the blockchain as opposed to memory that holds a value for a transaction.

The vulnerability arises because local variables within functions default to storage or memory depending on their type. As we have seen in the various Ethernaut challenges we have completed, if a smart contract were to have something like: bool public locked = true; feature at storage slot 0, and say a struct is used without defining if it is storage or memory Solidity will default the struct to storage. What is the implication of this? Well if that same struct is not initalised, it becomes a pointer to storage slot 0. This means by using that struct in whatever capacity that may be, you will turn the true status to false, effectively exploiting the contract.

The preventative measure to avoid this is explicitly using the memory or storage keywords when dealing with complex types to ensure they behave as expected. As pointed at before (pun intended), Pike is a good example of this type of vulnerability!

Day 110

I didn’t realise but I’d touched on the issue of Denial of Service before without going too much into it (see Day 34). As the name suggests, the purpose of these hacks is to ensure that the services of the smart contract are rendered fundamentally useless.

I’ve only seen this attack where there is a failure to specify the maximum amount of gas capable of being used by an external caller. For example, say there is withdraw function that incrementally gets more expensive as more users call this function. If we were malicious we could create a script that would call that function from various address to the point that after 1000 calls the gas cost to withdraw your funds would be so high that it is no longer economically feasible for you to withdraw your funds.

Although the above is one way this attack can occur, looping through external mappings / arrays can be another measure to deploy this attack. An example that illustrates this point is; an owner wants to distribute the proceeds of an acquisition to investors via a dividend. In doing so, a function is called whereby there is a loop that runs over an investor array. In doing so, the exploit can occur because the array may become artificially inflated (i.e. an attacker can create many user accounts making the investor array large). Why? Because some people just want to watch the world burn.

The final attack vector that this attack may occur on is where contracts are written such that they progress based on state changes, attributable to an external source. For example, what if an ICO contract is written such that once it receives all its funding it distributes a portion to its investor base moving from pre-funding to funded status. Assuming none of the above DoS issues exist, a DoS attack may still occur whereby transactions revert because an investors contract does not accept Ether. Again, if the contract must make these dividends before progressing to funded, the contract will never achieve the new state as Ether can never be sent to the user's contract which does not accept ether.

The solution to the above attacks is making sure, contracts do not loop through data structures that can be artificially manipulated by external users and a withdrawal pattern is implemented whereby each user may call a withdraw function to claim tokens independently (also referred to a pull-push pattern).

Day 111

I found this interesting article and thought it might be worth summarising for those interested in the Defi space! Each of my points of summary come from this website but for a more comprehensive breakdown of ideas, I highly recommend checking out the linked papers!

For context, the article delves into the inherent tension between security professionals wanting to find implementation errors in the code but neglecting to appreciate the poor design specifications that may still mean funds are lost by users (e.g. unfair auctions, price slippage, liquidation incentives etc.). The paper goes on to explain several conclusions about optimal DeFi protocol designs, each of which I have summarised below:

  1. every lending protocol with a liquidation incentive (Aave, Compound Finance, and all their forks) has a risk of accumulating bad debt. Liquidation incentives poses a risk to lending protocols and the measures to solving them are:
    a. halting all liquidations once a user's loan-to-value (LTV) ratio surpasses a certain threshold value can prevent future toxic liquidation spirals;
    b. offer substantial improvement in the bad debt that a lending market can expect to incur; and
    c. protocols should enact dynamic liquidation incentives and closing factor policies moving forward for optimal management of protocol risk.
  2. A MIQADO mechanism is proposed, which in short, incentivises users to improve the health of unhealthy borrowing positions by adding collateral. This is facilitated by a β€œreversible call option.” Compared to a standard call option, this option provides an extra outcome: the seller can terminate the option contract at a premium before expiry;
  3. Dynamic control systems can be gamed by users and on-chain PID controllers (a simple proportional supply change in response to a pricing error repeated over many cycles to slowly move the price back to the target) are vulnerable to this attack;
  4. NFT auctions are vulnerable to shill-bidder manipulation (i.e. bids made by the seller to boost the price). Dutch auctions are suggested to be the only strongly shill-proof auction format (an auction whereby the sale starts at the highest possible price and walks down until the first seller pays the maximum amount, they are willing to bid for it); and
  5. An introduction to the term β€œloss-versus-rebalancing” (LVR) performance gap. This is the difference between a β€œrebalancing strategy” that replicates the AMM trades at market prices in a CEX (this assumes a CEX with infinite liquidity, so trades have no price impact) compared to holding a LP position. A comparison of the two strategies shows that holding the LP position returns a worse result due to price slippage on each trade. It is therefore in the interest of AMM designers to reduce this LVR gap as much as possible to improve capital efficiency.

Day 112

As a general note I can appreciate some of these posts are a bit technical in nature and I appreciate that people visiting have varying degrees of knowledge. I thought for the beginners (much like myself!) a really useful free resource for establishing a good baseline understanding of Solidity is CryptoZombies. This platform is a gamified way to learning all about different solidity fundamentals and is immensely clear in terms of what it is trying to teach.

I am incredibly sorry for not mentioning this resource earlier. I bring this up now because I admittedly wanted to refresh some of my understanding for Solidity as I felt I recently used tutorials or write ups to code. CryptoZombie was able to solidify my understanding of concepts such as storage and up to advanced concepts like deployment with Truffle and zksync.

Maybe @neal or @kxun can attest to the above resource (or think of others)? I’m conscious that I may not have pointed out other obvious resources that go to understanding some of the topics I’m talking about. I do want this blog to be as accessible as possible!

Day 113

It has been a few months since my capture the flag event with OpenZeppelin and it is a skill I want to improve. As such I’ve decided to participate in challenges set by CryptoHack which is basically a python-based cryptographic CTF with a loose relation to crytpo.

I’m a huge fan of their UI and the way challenges are categorised. I also think it is an honest system because many others can verify my score should I post about completing certain CTF events. The final thing I like about this CTF is that it is in Python and it will feel good to brush up on those skills! I note there is a soft ban on posting answers for hacks worth more than 10 points so I may sporadically post these throughout security-related topics.

The first issues revolve around set up:

So as such I will spend today setting these challenges up and revert in the next week with how I go about solving real challenges!

As of this post I believe I've kept my promise and will now revert back to daily postings as opposed to a slew of bulk uploads during the week!!

Cryptozombies is an excellent platform for beginners, Ethernaut is also a gamified learning option that many in the community have praised.

Knowledge can be gained from all kinds of places, those keen to learn should just look around and see what works for themselves.

Although somewhat ancient (2019 ... :smile:) Practical advice for Solidity developers is still worth a read, but of all the posts I have read on this forum, I particularly liked this piece by @AnAllergyToAnalogy.

The final point is gold :rocket:

1 Like

Absolutely agree that looking around is super important, I remember coming into the space everyone recommended Dapp University and it just didn't really seem to stick with me!

I hadn't seen that 2019 post before but I think the point around keeping it simple (what this blog hopefully does!) and making sure to re-use where appropriate. I certainly felt cheap ripping off OpenZeppelin contracts for my use but after understanding the variety of security concerns behind crafts these contracts, I can see why the source is there to be used.

I absolutely agree on that final point because it's the best way to learn! I also like:

So my number 1 recommendation is if something doesn’t make sense (which can an extremely demoralising thing for a beginner), just stick it out.

But I would add that never pretend you know what's going on, ask questions because you'll be the one reported on the next rekt article if you pretend!

1 Like

Day 114

The Australian Defi Association is a community I've had really positive interactions with, so I am always glad to see when they release content in a fairly frequent podcast format. I've recently come across an interview with one of the co-founders of the HashLock, a Web3 audit firm.

I thought this was an interesting conversation around manual audits and the different approach that boutique auditing firms take to remain competitive (especially in a crypto bear market).

The key takeaways for me in this were:

  • auditing firms need to remain conscious of Web3 trends because its rapid advancement means a lot of problems are found in compilers, underlying networks, and consensus mechanisms;
  • understand the tools hackers use - this wasn't elaborated on but I'd be interested on what researching this topic practically looks like;
  • a general point that about setting high expectations for oneself in this type of industry. I thought this resonated with me mostly because I've seen a real lack of quality control when it comes to developer generated documentation, course content and of course code; and
  • auditing AI via large scale output validation. What this type of audit looks to is for AI bias, misalignment etc. and seeing if thousands of bits of data is put into the AI how often a mistake is made and the cost associated with said mistake.

Day 115

On the back of yesterday's conversation I actually wanted to see if I could deploy my previous DAO contract (contained in days 90 - 93 Github). For context, I had drafted four smart contracts that integrated with each other to make a DAO, breaking down each one below:

  1. Box.sol = a simple contract that stored a user's fav number;
  2. GovToken.sol = really simple ERC20 token;
  3. MyGovernor.sol = the logic behind the DAO (setting proposal thresholds, quorum numbers, timing delay etc.); and
  4. TimeLock.sol = the function is to impose a waiting period before a specific operation can be performed to prevent owner abuse. I set mine to a minute for illustrative purposes.

The steps to setting up this DAO certainly provided a few learning points. The first being that I had slightly modified an OpenZeppelin contract (I don't remember the purpose - lesson learnt re commenting my logic throughout the code!) for the MyGovernor contract. Boy was that a mistake. I had multiple storage clashes and turns out I wasn't correctly overriding the queue function for proposals correctly (as inherited from a base OZ Governor contract). My way to resolving this was to look through the code for an idea as to why I had modified it (after 20 mins I couldn't find one), so I reverted back to the OZ wizard and used their DAO logic. Worst comes to worst, I could always vote to change unwanted behaviour.

After deploying all four contracts on the Sepolia testnet, I discovered that the Tally platform provided an on-chain IDE to govern the DAO. This platform creates a clean dashboard that pull from your smart contract and allows you to implement you contract logic (see below):

You might be looking at that picture and wondering why I hadn't implemented any proposals...well what I didn't account for is a) delay timing to propose anything and b) quorum / proposal thresholds.

I had made the rookie error of not minting myself with custom ERC20 tokens and then using them to create a proposal. So guess what I did? After having a few thousand coins, it turns that there appears to be a delay between when I've minted the coins and when the account will receive them. There will also be a further delay to when I then want to create a proposal.

I've given the shortened version of this exercise but I actually had a lot of fun (literally laughing at points because of some stupid mistakes and how I had to work around them). I admittedly did just accept a lot of the default OZ settings without understanding the full implications for settings. As such, I'm going to have a go at creating a smaller series of contracts to showcase a quick and easy DAO!

1 Like

Day 116

So an update from yesterday is I finally have my 1000 tokens:

I was going to create a whole new set of contracts but I sort of wanted to problem solve the current series so I decided to press on. Feeling the power, I decided to create a new proposal but I didn't realise that in the Tally interface I had to delegate to ... myself? Another lesson learnt that it's almost like a proxy to the smart contract I had created on chain. So after delegating my tokens to myself, I was met with a transaction pending message:
Screenshot 2024-05-08 172311

After taking some time to mine and also keeping in mind that I still had a delay set as a specification in my smart contract, the transaction successfully executed and I had 1000 tokens to vote for proposals that only I could create:

Interesting to note that I could delegate to other users to vote on my behalf but from my understanding they would not be able to create any proposals. In the above screenshot you can see the capacity to delegate on the right. I went on to create a proposal (noting you can attach actions to a proposal, for example transferring 100 tokens to a user). It was cool to see not only the proposals but the history of voting power and the voters and delegates:

You'll note that because of the delay I created, the proposal is still pending. You can see that the Tally website has also created a timeline for proposals according to my smart contract specifications:

In conclusion, I've solved yesterday's problems and for those wondering, of course I plan to vote in favor of the proposal so that I literally have on-chain evidence that I am the best Pokemon trainer. Again, I've actually had a lot of fun learning practically how this platform / smart contract interaction works and how easy it is to get to terms with the layout of a DAO!

Day 117

Off the back of constructing my DAO, I thought I would test it so I wrote the following in Foundry:

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

import {Test, console} from "forge-std/Test.sol";
import {MyGovernor} from "../src/MyGovernor.sol";
import {GovToken} from "../src/GovToken.sol";
import {TimeLock} from "../src/TimeLock.sol";
import {Box} from "../src/Box.sol";

contract MyGovernorTest is Test {
    GovToken token;
    TimeLock timelock;
    MyGovernor governor;
    Box box;

    uint256 public constant MIN_DELAY = 7200; // 1 day - after a vote passes, you have 1 day before you can enact
    uint256 public constant QUORUM_PERCENTAGE = 15; // Need 15% of voters to pass
    uint256 public constant VOTING_PERIOD = 50400; // This is how long voting lasts
    uint256 public constant VOTING_DELAY = 1; // How many blocks till a proposal vote becomes active

    address[] proposers;
    address[] executors;

    bytes[] functionCalls;
    address[] addressesToCall;
    uint256[] values;

    address public constant VOTER = address(1);

    function setUp() public {
        token = new GovToken();
        token.minting(VOTER, 100e18);

        vm.prank(VOTER);
        token.delegate(VOTER);
        timelock = new TimeLock(MIN_DELAY, proposers, executors);
        token = new GovToken();
        bytes32 proposerRole = timelock.PROPOSER_ROLE();
        bytes32 executorRole = timelock.EXECUTOR_ROLE();

        timelock.grantRole(proposerRole, address(governor));
        timelock.grantRole(executorRole, address(0));
        vm.stopPrank();

        box = new Box(address(msg.sender));
        box.transferOwnership(address(timelock));
    }

    function testCantUpdateBoxWithoutGovernance() public {
        vm.expectRevert();
        box.store(1);
    }

    function testGovernanceUpdatesBox() public {
        uint256 valuesToStore = 777;
        string memory description = "Store 1 in Box";
        bytes memory encodedFunctionCall = abi.encodeWithSignature(
            "store(uint256)",
            valuesToStore
        );
        addressesToCall.push(address(box));
        values.push(0);
        functionCalls.push(encodedFunctionCall);
        // 1. Propose to the DAO
        uint256 proposalId = governor.propose(
            addressesToCall,
            values,
            functionCalls,
            description
        );

        console.log("Proposal State:", uint256(governor.state(proposalId)));
        // governor.proposalSnapshot(proposalId)
        // governor.proposalDeadline(proposalId)

        vm.warp(block.timestamp + VOTING_DELAY + 1);
        vm.roll(block.number + VOTING_DELAY + 1);

        console.log("Proposal State:", uint256(governor.state(proposalId)));

        // 2. Vote
        string memory reason = "I like a do da cha cha";
        // 0 = Against, 1 = For, 2 = Abstain for this example
        uint8 voteWay = 1;
        vm.prank(VOTER);
        governor.castVoteWithReason(proposalId, voteWay, reason);

        vm.warp(block.timestamp + VOTING_PERIOD + 1);
        vm.roll(block.number + VOTING_PERIOD + 1);

        console.log("Proposal State:", uint256(governor.state(proposalId)));

        // 3. Queue
        bytes32 descriptionHash = keccak256(abi.encodePacked(description));
        governor.queue(addressesToCall, values, functionCalls, descriptionHash);
        vm.roll(block.number + MIN_DELAY + 1);
        vm.warp(block.timestamp + MIN_DELAY + 1);

        // 4. Execute
        governor.execute(
            addressesToCall,
            values,
            functionCalls,
            descriptionHash
        );
    }
}

The good thing is the testing wasn't super technical in regards to thinking how the logic would play out. Provided I had already completed the DAO, I made sure to write tests that would simulate on-chain transactions.

However, the code is not perfect! The trouble I am currently solving for is that when I run forge test the test file is unable to be carried out because the onlyOwner modifier is currently at work. This means my pranked account (address(1) in the above code) is not an owner. I have no solution to the above as of yet but I'll keep everyone posted, I just thought this was an interesting tidbit from the string of successes in setting up the DAO.

1 Like

Day 118

I've been a bit time poor today but I've managed to pinpoint the issue in the test (so that's a positive?!).

I've still run forge test as before but ensuring Foundry provided a breakdown of what went wrong. Hot tip, you can do this by the following forge test -vvvv or if you are testing a specific test its forge test --mt testcheckAccountIsOwner -vvvv

The result was as follows:

Ran 1 test for test/MyGovernorTest.t.sol:MyGovernorTest
[FAIL. Reason: setup failed: OwnableUnauthorizedAccount(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)] setUp() (gas: 0)
Traces:
  [5217648] MyGovernorTest::setUp()
    β”œβ”€ [1611826] β†’ new GovToken@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
    β”‚   └─ ← [Return] 7819 bytes of code
    β”œβ”€ [97058] GovToken::minting(0x0000000000000000000000000000000000000001, 100000000000000000000 [1e20])
    β”‚   β”œβ”€ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x0000000000000000000000000000000000000001, value: 100000000000000000000 [1e20])
    β”‚   └─ ← [Stop] 
    β”œβ”€ [0] VM::prank(0x0000000000000000000000000000000000000001)
    β”‚   └─ ← [Return] 
    β”œβ”€ [70257] GovToken::delegate(0x0000000000000000000000000000000000000001)
    β”‚   β”œβ”€ emit DelegateChanged(delegator: 0x0000000000000000000000000000000000000001, fromDelegate: 0x0000000000000000000000000000000000000000, toDelegate: 0x0000000000000000000000000000000000000001)
    β”‚   β”œβ”€ emit DelegateVotesChanged(delegate: 0x0000000000000000000000000000000000000001, previousVotes: 0, newVotes: 100000000000000000000 [1e20])
    β”‚   └─ ← [Stop] 
    β”œβ”€ [1417447] β†’ new TimeLock@0x2e234DAe75C793f67A35089C9d99245E1C58470b
    β”‚   β”œβ”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000000, account: TimeLock: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], sender: MyGovernorTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
    β”‚   β”œβ”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000000, account: MyGovernorTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], sender: MyGovernorTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
    β”‚   β”œβ”€ emit MinDelayChange(oldDuration: 0, newDuration: 7200)
    β”‚   └─ ← [Return] 6712 bytes of code
    β”œβ”€ [1611826] β†’ new GovToken@0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
    β”‚   └─ ← [Return] 7819 bytes of code
    β”œβ”€ [285] TimeLock::PROPOSER_ROLE() [staticcall]
    β”‚   └─ ← [Return] 0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1
    β”œβ”€ [286] TimeLock::EXECUTOR_ROLE() [staticcall]
    β”‚   └─ ← [Return] 0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63
    β”œβ”€ [27571] TimeLock::grantRole(0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1, 0x0000000000000000000000000000000000000001)
    β”‚   β”œβ”€ emit RoleGranted(role: 0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1, account: 0x0000000000000000000000000000000000000001, sender: MyGovernorTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
    β”‚   └─ ← [Stop] 
    β”œβ”€ [27571] TimeLock::grantRole(0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63, 0x0000000000000000000000000000000000000001)
    β”‚   β”œβ”€ emit RoleGranted(role: 0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63, account: 0x0000000000000000000000000000000000000001, sender: MyGovernorTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
    β”‚   └─ ← [Stop] 
    β”œβ”€ [0] VM::stopPrank()
    β”‚   └─ ← [Return] 
    β”œβ”€ [143117] β†’ new Box@0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9
    β”‚   β”œβ”€ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: DefaultSender: [0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38])
    β”‚   └─ ← [Return] 595 bytes of code
    β”œβ”€ [558] Box::transferOwnership(TimeLock: [0x2e234DAe75C793f67A35089C9d99245E1C58470b])
    β”‚   └─ ← [Revert] OwnableUnauthorizedAccount(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)
    └─ ← [Revert] OwnableUnauthorizedAccount(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)

Pretty ugly I know but the breakdown (as far as I follow) is as follows:

  • GovToken is created at the specified address;
  • GovToken mints 100 tokens to address 1 (aka VOTER), noting this emits a transfer event;
  • the VOTER is pranked meaning the voter address is equal to msg.sender for only the next transaction;
  • this allows the delegate function to be called whereby the msg.sender delegates its voting rights to VOTER;
  • this practically allows the VOTER the capacity to vote (with voting power being denominated with 100 tokens);
  • a new Timelock contract is created (allowing us to assign executor and proposer roles, alongside setting a min delay);
  • after assigning roles, the prank is stopped so the VOTER is no longer msg.sender; an d
  • a new Box contract is created and the ownership of this newly created implement is transferred to a new owner such that they would be able to store a number (which contains an onlyOwner modifier).

Based on the above, I think the error is amongst the granting of roles:

    β”‚   β”œβ”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000000, account: TimeLock: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], sender: MyGovernorTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
    β”‚   β”œβ”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000000, account: MyGovernorTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], sender: MyGovernorTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])

I think this because this is the first time the 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 address appears and is the reason for the later error of Box::transferOwnership I'll continue to work on this error and to check the logic I've explained above and revert once I solve it. If others have ideas, I'm always open to hearing them!

Day 119

Two steps forward, one step back with this test suite. I delayed posting in an attempt to report good news after a weekend of trying to solve this issue.

Starting with the good news:
setUp() and testCantUpdateBoxWithoutGovernance() work like a charm. The changes I made to the setUp were as follows to resolve the ownership issue:

  • changed vm.prank(VOTER) to vm.startPrank(VOTER) the difference being that the startPrank ensures that msg.sender is set for all subsequent calls until stopPrank is called...bit of a small brained move by me as this probably should have clicked as I explained the difference in my last post;
  • I had repeated token = new GovToken(); so I deleted the duplicate;
  • replaced the above line with the following governor = new MyGovernor(token, timelock); to ensure a new myGovernor contract is created with the relevant token and timelock parameters;
  • changed box = new Box(address(msg.sender)); to box = new Box(); provided this is strictly limiting any new creation of the box contract to the msg.sender address when that condition may not hold. This required me to make some changes to the Box contract whereby the constructor was also applying the same restriction to the deployment of a newly created Box contract.

The less than great news:
I'm now met with the following error for the testGovernanceUpdatesBox:

[178017] MyGovernorTest::testGovernanceUpdatesBox()
    β”œβ”€ [12568] MyGovernor::propose([0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1], [0], [0x6057361d0000000000000000000000000000000000000000000000000000000000000309], "Store 777 in Box")
    β”‚   β”œβ”€ [427] GovToken::clock() [staticcall]
    β”‚   β”‚   └─ ← [Return] 1
    β”‚   β”œβ”€ [3078] GovToken::getPastVotes(MyGovernorTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0) [staticcall]
    β”‚   β”‚   └─ ← [Return] 0
    β”‚   └─ ← [Revert] GovernorInsufficientProposerVotes(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496, 0, 10000000000000000000 [1e19])
    └─ ← [Revert] GovernorInsufficientProposerVotes(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496, 0, 10000000000000000000 [1e19])

This error relates to the box contract (as I read the above) to not have enough proposer votes. What this means is that a contract is attempting to propose an action on the DAO. To do so you require either enough tokens to have the voting power to propose something or another user needs to delegate their tokens to you. It appears in this testing suite I'll have to look into the second option provided minting the box contract with the same logic in the setUp didn't work.

Day 120

Two days ago (in the midst of my testing) Bloom (a leveraged loan provided in the Web3 space) was hacked due to a security vulnerability (losses around ~USD$540k, ~USD$486k recovered).

According to Bloom's tweet they intend to reimburse those affected and were due to release a post-mortem as it related to the protocols rebasing yield...but the report appears to be late. So in the interim, I did some digging on the L2 Blast network to better understand the environment that led to the hack. I have to say its been pretty interesting.

I'd created a recent timeline for exploits on this L2 and it doesn't look great:

  • Feb-24: RiskOnBlast, suffered a rug pull, resulting in a loss of around 500 ETH;
  • Mar-24: Super Sushi Samurai, suffered a token exploit before its gaming launch, resulting in an exploit of USD$4.6 million due to a smart contract bug;
  • Mar-24: Munchables, an NFT game platform, suffered a $62 million exploit; and
  • May-24: Now Bloom has been exploited for far less, but nevertheless exploited.

I also found it a bit odd that the L2 was requesting funding in late 2023, locked investor funds until Feb-24 and then shortly after these funds were released, there is an Airdrop for this L2. I'd be keen to get others' thoughts on this, maybe I don't know enough here?

Hi Jarrod, not sure if you have come across a company called Paradigm started by Fred Ehrsam (co-founded Coinbase) and Matt Huang but I'm sure you will find the articles written by samczsun very interesting.

Like Satoshi, Samczsun likes to keep his identity from view and is Head of Security at Paradigm. He 'helps secure the cryptocurrency ecosystem by responsibly disclosing vulnerabilities and publishing educational resources' according to his profile. He is definitely one of the leaders in this area right now.

As a starting point you could read Two Rights Might Make A Wrong about a USD $350 million vulnerability, but be warned: I might have lead you directly to a rabbit hole here! :smile:

1 Like

Hi Neal, thank you so much for the recommendation!

I've heard of Paradigm but never really delved into any of their writings / products. After reading the post you linked, I can safely say I'm squarely down the rabbit hole (reading all of Samczsun's posts with multiple tabs on subjects open).

My reading recommendation if you are interested is a short post from an independent Web3 researcher by the username cmichel and his journey to making his million on Code4Arena. It's really interesting seeing the hours pumped into the platform vs its rewards (noting this was a post made 2 years ago!). It's also a refreshing perspective provided a lot of players in the industry claim to get millions in minutes through trading bots...

1 Like

Day 121

This post is a non-technical one so others can skip it if they like but I want to make it to showcase all aspects of the journey to becoming a smart contract auditor / researcher. I'll keep some of the details deliberately vague so it isn't obvious which firm I am speaking to.

I have been applying for Web3 security-related jobs (with no real luck!) until I woke from a message on Saturday from a firm's HR asking to discuss further details about my application. I replied saying I was super excited to discuss and would love to chat when they were available (note: I did not receive a response until Monday). I couldn't believe I had my first real interview for a Web3 job and I was so eager to impress that I went back to the job description to refresh on the skills listed in the job. I could barely focus on the job description (lame to admit but like I said, I was excited) noting the relevant frameworks and Rust as a requirement.

I'm not kidding when I say I worked for ~10 hours each day going over Rust, reviewing my security vulnerabilities and preparing a couple of key takeaways for points that may be raised. I receive a message today, after full working day, to see the same recruiter has asked the standard questions regarding experience, salary expectations and, timing for a call. I answer each in turn and in response I am told they were looking for someone with 3+ years experience in Web3...despite the job description failing to mention this.

My stomach dropped and I was absolutely devastated. Now, some might be saying...who cares / why bother posting about this? Well the key takeaways are these:

  1. Don't be so eager to impress you actually prepare for an interview you don't even have locked down yet (this one was a rookie mistake on my behalf);
  2. Exercises like this might look like I've completely wasted a weekend. However, I'd argue that I now know about a brand new set of frameworks I previously didn't know existed in the auditing community and I upskilled big time on Rust; and
  3. Communication in this field is everything: make sure you mention time zones, quote currency as USD$ or AUD$ and do not message others on a weekend if you aren't willing to do the same!

If other veterans have stories similar to this, do share! I'm certainly taking today as a breather before I go on to participate in the latest CodeHawks audit competition!!