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

Add BAL Token Holder contract #1149

Merged
merged 9 commits into from
Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions pkg/standalone-utils/contracts/BALTokenHolder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.7.0;

import "@balancer-labs/v2-solidity-utils/contracts/helpers/Authentication.sol";
import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/SafeERC20.sol";

import "@balancer-labs/v2-vault/contracts/interfaces/IVault.sol";
import "@balancer-labs/v2-liquidity-mining/contracts/interfaces/IBalancerToken.sol";

import "./interfaces/IBALTokenHolder.sol";

/**
* @dev This contract simply holds the BAL token and delegates to Balancer Governance the permission to withdraw it. It
* is intended to serve as the recipient of automated BAL minting via the liquidity mining gauges, allowing for the
* final recipient of the funds to be configurable without having to alter the gauges themselves.
*
* There is also a separate auxiliary function to sweep any non-BAL tokens sent here by mistake.
*/
contract BALTokenHolder is IBALTokenHolder, Authentication {
using SafeERC20 for IERC20;

IBalancerToken private immutable _balancerToken;
IVault private immutable _vault;

string private _name;

constructor(
IBalancerToken balancerToken,
IVault vault,
string memory name
) Authentication(bytes32(uint256(address(this)))) {
// BALTokenHolder is often deployed from a factory for convenience, but it uses its own address instead of
// the factory's as a disambiguator to make sure the action IDs of all instances are unique, reducing likelihood
// of errors.

_balancerToken = balancerToken;
_vault = vault;
_name = name;
}

function getBalancerToken() external view returns (IBalancerToken) {
return _balancerToken;
}

function getVault() public view returns (IVault) {
return _vault;
}

function getName() external view returns (string memory) {
return _name;
}

function getAuthorizer() public view returns (IAuthorizer) {
return getVault().getAuthorizer();
}

function _canPerform(bytes32 actionId, address account) internal view override returns (bool) {
return getAuthorizer().canPerform(actionId, account, address(this));
}

function withdrawFunds(address recipient, uint256 amount) external override authenticate {
IERC20(_balancerToken).safeTransfer(recipient, amount);
}

function sweepTokens(
IERC20 token,
address recipient,
uint256 amount
) external override authenticate {
require(token != _balancerToken, "Cannot sweep BAL");
IERC20(token).safeTransfer(recipient, amount);
}
}
59 changes: 59 additions & 0 deletions pkg/standalone-utils/contracts/BALTokenHolderFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.7.0;

import "@balancer-labs/v2-solidity-utils/contracts/helpers/Authentication.sol";
import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/SafeERC20.sol";

import "@balancer-labs/v2-vault/contracts/interfaces/IVault.sol";
import "@balancer-labs/v2-liquidity-mining/contracts/interfaces/IBalancerToken.sol";

import "./BALTokenHolder.sol";
import "./interfaces/IBALTokenHolderFactory.sol";

contract BALTokenHolderFactory is IBALTokenHolderFactory {
IBalancerToken private immutable _balancerToken;
IVault private immutable _vault;

mapping(address => bool) private _factoryCreatedHolders;

event BALTokenHolderCreated(BALTokenHolder balTokenHolder, string name);

constructor(IBalancerToken balancerToken, IVault vault) {
_balancerToken = balancerToken;
_vault = vault;
}

function getBalancerToken() public view override returns (IBalancerToken) {
return _balancerToken;
}

function getVault() public view override returns (IVault) {
return _vault;
}

function isHolderFromFactory(address holder) external view override returns (bool) {
return _factoryCreatedHolders[holder];
}

function create(string memory name) external override returns (IBALTokenHolder) {
BALTokenHolder holder = new BALTokenHolder(getBalancerToken(), getVault(), name);

_factoryCreatedHolders[address(holder)] = true;
emit BALTokenHolderCreated(holder, name);

return holder;
}
}
28 changes: 28 additions & 0 deletions pkg/standalone-utils/contracts/interfaces/IBALTokenHolder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.7.0;

import "@balancer-labs/v2-solidity-utils/contracts/helpers/IAuthentication.sol";
import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/IERC20.sol";

interface IBALTokenHolder is IAuthentication {
function withdrawFunds(address recipient, uint256 amount) external;

function sweepTokens(
IERC20 token,
address recipient,
uint256 amount
) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.7.0;

import "@balancer-labs/v2-vault/contracts/interfaces/IVault.sol";
import "@balancer-labs/v2-liquidity-mining/contracts/interfaces/IBalancerToken.sol";
import "./IBALTokenHolder.sol";

interface IBALTokenHolderFactory {
function getBalancerToken() external view returns (IBalancerToken);

function getVault() external view returns (IVault);

function isHolderFromFactory(address holder) external view returns (bool);

function create(string memory name) external returns (IBALTokenHolder);
}
1 change: 1 addition & 0 deletions pkg/standalone-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"test:watch": "nodemon --ext js,ts --watch test --watch lib --exec 'clear && yarn test --no-compile'"
},
"dependencies": {
"@balancer-labs/v2-liquidity-mining": "workspace:*",
"@balancer-labs/v2-pool-utils": "workspace:*",
"@balancer-labs/v2-solidity-utils": "workspace:*",
"@balancer-labs/v2-vault": "workspace:*"
Expand Down
115 changes: 115 additions & 0 deletions pkg/standalone-utils/test/BALTokenHolder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { ethers } from 'hardhat';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address';

import { deploy, deployedAt } from '@balancer-labs/v2-helpers/src/contract';
import { actionId } from '@balancer-labs/v2-helpers/src/models/misc/actions';

import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault';
import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList';
import { expectBalanceChange } from '@balancer-labs/v2-helpers/src/test/tokenBalance';
import { Contract } from 'ethers';
import { expect } from 'chai';
import Token from '@balancer-labs/v2-helpers/src/models/tokens/Token';

describe('BALTokenHolder', function () {
let tokens: TokenList;
let BAL: Token, DAI: Token;
let vault: Vault;
let holder: Contract;
let admin: SignerWithAddress, authorized: SignerWithAddress, other: SignerWithAddress;

const holderName = 'DAO Treasury';

before('get signers', async () => {
[, admin, authorized, other] = await ethers.getSigners();
});

sharedBeforeEach(async () => {
// Deploy Balancer Vault
vault = await Vault.create({ admin });

// Deploy BAL token
tokens = await TokenList.create([{ symbol: 'BAL' }, { symbol: 'DAI' }]);
BAL = await tokens.findBySymbol('BAL');
DAI = await tokens.findBySymbol('DAI');

holder = await deploy('BALTokenHolder', { args: [BAL.address, vault.address, holderName] });

// Deposit all tokens in the holder
await tokens.mint({ to: holder });
});

it('returns the BAL address', async () => {
expect(await holder.getBalancerToken()).to.equal(BAL.address);
});

it('returns the address of the vault', async () => {
expect(await holder.getVault()).to.equal(vault.address);
});

it('returns its name', async () => {
expect(await holder.getName()).to.equal(holderName);
});

describe('withdrawFunds', () => {
context('when the caller is authorized', () => {
sharedBeforeEach(async () => {
const authorizer = await deployedAt('v2-vault/Authorizer', await vault.instance.getAuthorizer());
const withdrawActionId = await actionId(holder, 'withdrawFunds');
await authorizer.connect(admin).grantPermissions([withdrawActionId], authorized.address, [holder.address]);
});

it('sends funds to the recipient', async () => {
await expectBalanceChange(() => holder.connect(authorized).withdrawFunds(other.address, 100), tokens, {
account: other.address,
changes: { BAL: 100 },
});
});
});

context('when the caller is not authorized', () => {
it('reverts', async () => {
await expect(holder.connect(other).withdrawFunds(other.address, 100)).to.be.revertedWith('SENDER_NOT_ALLOWED');
});
});
});

describe('sweepTokens', () => {
context('when the caller is authorized', () => {
sharedBeforeEach(async () => {
const authorizer = await deployedAt('v2-vault/Authorizer', await vault.instance.getAuthorizer());
const sweepActionId = await actionId(holder, 'sweepTokens');
await authorizer.connect(admin).grantPermissions([sweepActionId], authorized.address, [holder.address]);
});

context('when the token is not BAL', () => {
it('sends funds to the recipient', async () => {
await expectBalanceChange(
() => holder.connect(authorized).sweepTokens(DAI.address, other.address, 100),
tokens,
{
account: other.address,
changes: { DAI: 100 },
}
);
});
});

context('when the token is BAL', () => {
it('reverts', async () => {
await expect(holder.connect(authorized).sweepTokens(BAL.address, other.address, 100)).to.be.revertedWith(
'Cannot sweep BAL'
);
});
});
});

context('when the caller is not authorized', () => {
it('reverts', async () => {
await expect(holder.connect(other).sweepTokens(DAI.address, other.address, 100)).to.be.revertedWith(
'SENDER_NOT_ALLOWED'
);
});
});
});
});
Loading