Skip to content

Commit

Permalink
Add BAL Token Holder contract (#1149)
Browse files Browse the repository at this point in the history
* Add BALTokenHolder and factory

Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com>
  • Loading branch information
nventuro and TomAFrench authored Mar 23, 2022
1 parent 3be2a59 commit 8c76fc0
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 1 deletion.
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);
}
}
58 changes: 58 additions & 0 deletions pkg/standalone-utils/contracts/BALTokenHolderFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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-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

0 comments on commit 8c76fc0

Please sign in to comment.