NOTE: At the time of the internal disclosure I was a FT contributor to DXdao, I did not work on the smart contracts in question, nor in the UIs which lacked the functionality reflection. My scope of work revolves mostly around treasury management and governance operations.

It’s Friday, 24th of June. Not only is it the last day of the week - but tomorrow it’s my birthday - hurah!

I need to submit some important proposals at DXdao, but our UIs are down. Alchemy seems to have some subgraph sync issues and DXvote doesn’t load at all. Ugh - I don’t want to delay this to the weekend – did I mention already tomorrow is my birthday?!

Ok, fuck this. I’ll submit it via CLI. Load the smart contracts locally, upload the content to IPFS locally, then build the transaction with the IPFS CID and other params, then finally submit it.

What smart contracts do I need? Well, 95% of DXdao proposals are done through the ContributionReward. The scheme has direct access to DXdao’s Treasury and is used for anything that involves moving money around, paying contributors, sponsorships, you name it.

All I want to do is submit a proposal, so I check out the proposeContributionReward function.

    /**
    * @dev Submit a proposal for a reward for a contribution:
    * @param _avatar Avatar of the organization that the contribution was made for
    * @param _descriptionHash A hash of the proposal's description
    * @param _reputationChange - Amount of reputation change requested .Can be negative.
    * @param _rewards rewards array:
    *         rewards[0] - Amount of tokens requested per period
    *         rewards[1] - Amount of ETH requested per period
    *         rewards[2] - Amount of external tokens requested per period
    *         rewards[3] - Period length - if set to zero it allows immediate redeeming after execution.
    *         rewards[4] - Number of periods
    * @param _externalToken Address of external token, if reward is requested there
    * @param _beneficiary Who gets the rewards
    */
    function proposeContributionReward(
        Avatar _avatar,
        string memory _descriptionHash,
        int256 _reputationChange,
        uint[5] memory _rewards,
        IERC20 _externalToken,
        address payable _beneficiary
    )

I’m so illiterate at Solidity that I start off reading the comments (rofl - who reads comments in code). One thing that stands out immediately is that the rewards in the _rewards array are set “per period”? What does that even mean?

Let’s look up where periods are - oh wait it’s right there the last int in the _rewards array. I’m confused. All proposals at DXdao are one-off proposals - or at least I (and everyone else) thought so. The UIs don’t even display periods of a proposal, when you submit a proposal through a UI interface it defaults proposals to 1 - well let me try on override it and see what happens… suspense …nothing. The proposal submitted fine, the UIs (now working again ofc… after I did the whole local setup) show it as a completely normal proposal - although it has multiple periods (i.e. should be redeemable multiple times).

Alright, this is weird. But I guess it probably doesn’t allow you to redeem it multiple time - that would be crazy! Enter Tenderly - one of my fav tools. Let’s overwrite a couple of states to make the proposal appear as successfully passed and do a Tenderly Simulation.

Holy smokes.

Periods are multipliers?! Let’s dig in…

function redeemReputation(bytes32 _proposalId, Avatar _avatar) public returns(int256 reputation) {

        uint256 periodsToPay = getPeriodsToPay(_proposalId, address(_avatar), 0);

        //set proposal reward to zero to prevent reentrancy attack.
        proposal.reputationChange = 0;
        reputation = int(periodsToPay) * _proposal.reputationChange;

Holy shit int(periodsToPay) * _proposal.reputationChange; periods is just a hidden multiplier?!?

Also while the comments of the proposeContributionReward explicitly states Native Token, ETH, and External Tokens are paid out per period. This code snippet above is REP - DXdao’s voting token. REP is a non-transferable ERC20 tokens (aka not an ERC20 token lol - but essentially an ERC20 token with the transfer function removed). With 51% REP you own the DAO.

Could this be exploited? Well…

I guess anyone submitting a proposal for payment could use a multiplier to drain funds - but it’d be obvious who did it.

Is there an anon way to exploit the funds?

Here it becomes interesting - DXdao generously rewards REP to active community members, the simplest way for example is being active in the Discord channel and receiving a “REP boost”. These are small rewards payed out to active community members a lot of whom are anon. The smallest REP boost is 186 REP - is that enough to take full control of the DAO?

Yes.

The tenderly simulation above mints 176 REP with 100k periods - which would account for over 90% REP. Allowing the exploiter to instantly withdraw all funds in the DAO, as if it were an EOA they control.

How much money was in the DAO?

At the time of discovery, there were over $80M in the DAO - although a large part of those funds were native tokens. ETH + Stables alone made up $37.8M though.

What happened?

I reported the attack vector to the DXdao governance team immediately, on the same day an update was pushed to DXvote (the UI we control) to reflect proposals that use multiple periods and display a warning on such proposals.

Some more deets and forum discussion: link