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 2 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
70 changes: 70 additions & 0 deletions pkg/standalone-utils/contracts/BALTokenHolder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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-gauges/contracts/interfaces/IBalancerToken.sol";

import "./interfaces/IBALTokenHolder.sol";

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 conveniency, but it uses its own address instead that of
// the factory as a disambiguator to make sure the action IDs of all instances are unique, reducing likelihood
// of errors.
nventuro marked this conversation as resolved.
Show resolved Hide resolved

_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);
}
}
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-gauges/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;
}
}
21 changes: 21 additions & 0 deletions pkg/standalone-utils/contracts/interfaces/IBALTokenHolder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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";

interface IBALTokenHolder is IAuthentication {
function withdrawFunds(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-gauges/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-gauges": "workspace:*",
"@balancer-labs/v2-pool-utils": "workspace:*",
"@balancer-labs/v2-solidity-utils": "workspace:*",
"@balancer-labs/v2-vault": "workspace:*"
Expand Down
73 changes: 73 additions & 0 deletions pkg/standalone-utils/test/BALTokenHolder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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;
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' }]);
BAL = await tokens.findBySymbol('BAL');

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

// Deposit BAL in the holder
await BAL.mint(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);
});

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');
});
});
});
81 changes: 81 additions & 0 deletions pkg/standalone-utils/test/BALTokenHolderFactory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 { Contract } from 'ethers';
import { expect } from 'chai';
import * as expectEvent from '@balancer-labs/v2-helpers/src/test/expectEvent';
import Token from '@balancer-labs/v2-helpers/src/models/tokens/Token';

describe('BALTokenHolderFactory', function () {
let tokens: TokenList;
let BAL: Token;
let vault: Vault;
let factory: Contract;

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

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

factory = await deploy('BALTokenHolderFactory', { args: [BAL.address, vault.address] });
});

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

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

async function deployHolder(name: string): Promise<Contract> {
const receipt = await (await factory.create(name)).wait();
const {
args: { balTokenHolder: holder },
} = expectEvent.inReceipt(receipt, 'BALTokenHolderCreated', { name });

return await deployedAt('BALTokenHolder', holder);
}

describe('creation', () => {
it('emits an event', async () => {
const receipt = await (await factory.create('holder')).wait();
expectEvent.inReceipt(receipt, 'BALTokenHolderCreated', { name: 'holder' });
});

it('creates a holder with the same BAL and vualt addresses', async () => {
nventuro marked this conversation as resolved.
Show resolved Hide resolved
const holder = await deployHolder('holder');

expect(await holder.getBalancerToken()).to.equal(BAL.address);
expect(await holder.getVault()).to.equal(vault.address);
});

it('creates a holder with name', async () => {
const holder = await deployHolder('holder');
expect(await holder.getName()).to.equal('holder');
});

it('creates holders with unique action IDs', async () => {
const first = await deployHolder('first');
const second = await deployHolder('second');

expect(await actionId(first, 'withdrawFunds')).to.not.equal(await actionId(second, 'withdrawFunds'));
});
});

describe('is holder from factory', () => {
it('returns true for holders created by the factory', async () => {
const holder = await deployHolder('holder');
expect(await factory.isHolderFromFactory(holder.address)).to.equal(true);
});

it('returns false for other addresses', async () => {
expect(await factory.isHolderFromFactory(factory.address)).to.equal(false);
});
});
});
3 changes: 2 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ __metadata:
languageName: unknown
linkType: soft

"@balancer-labs/v2-gauges@workspace:pkg/gauges":
"@balancer-labs/v2-gauges@workspace:*, @balancer-labs/v2-gauges@workspace:pkg/gauges":
version: 0.0.0-use.local
resolution: "@balancer-labs/v2-gauges@workspace:pkg/gauges"
dependencies:
Expand Down Expand Up @@ -590,6 +590,7 @@ __metadata:
dependencies:
"@balancer-labs/balancer-js": "workspace:*"
"@balancer-labs/v2-common": "workspace:*"
"@balancer-labs/v2-gauges": "workspace:*"
"@balancer-labs/v2-helpers": "workspace:*"
"@balancer-labs/v2-pool-utils": "workspace:*"
"@balancer-labs/v2-solidity-utils": "workspace:*"
Expand Down