-
Notifications
You must be signed in to change notification settings - Fork 385
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add BAL Token Holder contract (#1149)
* Add BALTokenHolder and factory Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com>
- Loading branch information
1 parent
3be2a59
commit 8c76fc0
Showing
8 changed files
with
400 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
28
pkg/standalone-utils/contracts/interfaces/IBALTokenHolder.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
29 changes: 29 additions & 0 deletions
29
pkg/standalone-utils/contracts/interfaces/IBALTokenHolderFactory.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.