Skip to content

Commit f66d358

Browse files
committed
rescue funds
1 parent 5d1d380 commit f66d358

File tree

5 files changed

+176
-6
lines changed

5 files changed

+176
-6
lines changed

contracts/TokenGeyser.sol

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ contract TokenGeyser is IStaking, Ownable {
3535
// amount: Unlocked tokens, total: Total locked tokens
3636
event TokensUnlocked(uint256 amount, uint256 total);
3737

38-
TokenPool private _stakingPool;
38+
TokenPool public stakingPool;
3939
TokenPool private _unlockedPool;
4040
TokenPool private _lockedPool;
4141

@@ -110,7 +110,7 @@ contract TokenGeyser is IStaking, Ownable {
110110
require(bonusPeriodSec_ != 0, 'TokenGeyser: bonus period is zero');
111111
require(initialSharesPerToken > 0, 'TokenGeyser: initialSharesPerToken is zero');
112112

113-
_stakingPool = new TokenPool(stakingToken);
113+
stakingPool = new TokenPool(stakingToken);
114114
_unlockedPool = new TokenPool(distributionToken);
115115
_lockedPool = new TokenPool(distributionToken);
116116
startBonus = startBonus_;
@@ -123,7 +123,7 @@ contract TokenGeyser is IStaking, Ownable {
123123
* @return The token users deposit as stake.
124124
*/
125125
function getStakingToken() public view returns (IERC20) {
126-
return _stakingPool.token();
126+
return stakingPool.token();
127127
}
128128

129129
/**
@@ -186,7 +186,7 @@ contract TokenGeyser is IStaking, Ownable {
186186
// _lastAccountingTimestampSec = now;
187187

188188
// interactions
189-
require(_stakingPool.token().transferFrom(staker, address(_stakingPool), amount),
189+
require(stakingPool.token().transferFrom(staker, address(stakingPool), amount),
190190
'TokenGeyser: transfer into staking pool failed');
191191

192192
emit Staked(beneficiary, amount, totalStakedFor(beneficiary), "");
@@ -266,7 +266,7 @@ contract TokenGeyser is IStaking, Ownable {
266266
// _lastAccountingTimestampSec = now;
267267

268268
// interactions
269-
require(_stakingPool.transfer(msg.sender, amount),
269+
require(stakingPool.transfer(msg.sender, amount),
270270
'TokenGeyser: transfer out of staking pool failed');
271271
require(_unlockedPool.transfer(msg.sender, rewardAmount),
272272
'TokenGeyser: transfer out of unlocked pool failed');
@@ -328,7 +328,7 @@ contract TokenGeyser is IStaking, Ownable {
328328
* @return The total number of deposit tokens staked globally, by all users.
329329
*/
330330
function totalStaked() public view returns (uint256) {
331-
return _stakingPool.balance();
331+
return stakingPool.balance();
332332
}
333333

334334
/**
@@ -500,4 +500,13 @@ contract TokenGeyser is IStaking, Ownable {
500500
schedule.unlockedShares = schedule.unlockedShares.add(sharesToUnlock);
501501
return sharesToUnlock;
502502
}
503+
504+
/**
505+
* @dev Lets the owner rescue funds air-dropped to the staking pool.
506+
*/
507+
function rescueFundsFromStakingPool(address _tokenToRescue, address to, uint256 amount)
508+
public onlyOwner returns (bool) {
509+
510+
return stakingPool.rescueFunds(_tokenToRescue, to, amount);
511+
}
503512
}

contracts/TokenPool.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,10 @@ contract TokenPool is Ownable {
2222
function transfer(address to, uint256 value) external onlyOwner returns (bool) {
2323
return token.transfer(to, value);
2424
}
25+
26+
function rescueFunds(address _tokenToRescue, address to, uint256 amount) external onlyOwner returns (bool) {
27+
require(address(token) != _tokenToRescue, 'TokenPool: Cannot claim token held by the contract');
28+
29+
return IERC20(_tokenToRescue).transfer(to, amount);
30+
}
2531
}

contracts/mocks/MockErc20.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
2+
3+
contract MockERC20 is ERC20 {
4+
constructor(uint256 _totalSupply) public {
5+
_mint(msg.sender, _totalSupply);
6+
}
7+
}

test/staking.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
invokeRebase
1111
} = _require('/test/helper');
1212

13+
const MockERC20 = contract.fromArtifact('MockERC20');
1314
const AmpleforthErc20 = contract.fromArtifact('UFragments');
1415
const TokenGeyser = contract.fromArtifact('TokenGeyser');
1516
const InitialSharesPerToken = 10 ** 6;
@@ -218,3 +219,42 @@ describe('staking', function () {
218219
});
219220
});
220221
});
222+
223+
224+
describe('rescueFundsFromStakingPool', function () {
225+
describe('when tokens gets air-dropped', function() {
226+
it('should allow the owner to claim them', async function() {
227+
const accounts = await chain.getUserAccounts();
228+
owner = web3.utils.toChecksumAddress(accounts[0]);
229+
anotherAccount = web3.utils.toChecksumAddress(accounts[8]);
230+
231+
ampl = await AmpleforthErc20.new();
232+
await ampl.initialize(owner);
233+
await ampl.setMonetaryPolicy(owner);
234+
235+
const startBonus = 50;
236+
const bonusPeriod = 86400;
237+
const dist = await TokenGeyser.new(ampl.address, ampl.address, 10, startBonus, bonusPeriod,
238+
InitialSharesPerToken);
239+
240+
const stakingPool = await dist.stakingPool.call();
241+
242+
await ampl.approve(dist.address, $AMPL(100));
243+
await dist.stake($AMPL(100), []);
244+
245+
const token = await MockERC20.new(1000);
246+
await token.transfer(stakingPool, 1000);
247+
248+
expect(await token.balanceOf.call(anotherAccount)).to.be.bignumber.equal('0');
249+
await dist.rescueFundsFromStakingPool(
250+
token.address, anotherAccount, 1000
251+
);
252+
expect(await token.balanceOf.call(anotherAccount)).to.be.bignumber.equal('1000');
253+
254+
await expectRevert(
255+
dist.rescueFundsFromStakingPool(ampl.address, anotherAccount, $AMPL(10)),
256+
'TokenPool: Cannot claim token held by the contract'
257+
);
258+
})
259+
});
260+
});

test/token_pool.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
const { contract, web3 } = require('@openzeppelin/test-environment');
2+
const { expectRevert, expectEvent } = require('@openzeppelin/test-helpers');
3+
const { expect } = require('chai');
4+
5+
const _require = require('app-root-path').require;
6+
const BlockchainCaller = _require('/util/blockchain_caller');
7+
const chain = new BlockchainCaller(web3);
8+
9+
const MockERC20 = contract.fromArtifact('MockERC20');
10+
const TokenPool = contract.fromArtifact('TokenPool');
11+
12+
let token, otherToken, tokenPool, owner, anotherAccount;
13+
describe('tokenPool', function () {
14+
beforeEach('setup contracts', async function () {
15+
const accounts = await chain.getUserAccounts();
16+
owner = web3.utils.toChecksumAddress(accounts[0]);
17+
anotherAccount = web3.utils.toChecksumAddress(accounts[8]);
18+
19+
token = await MockERC20.new(1000);
20+
otherToken = await MockERC20.new(2000);
21+
22+
tokenPool = await TokenPool.new(token.address);
23+
});
24+
25+
describe('balance', function() {
26+
it('should return the balance of the token pool', async function(){
27+
await token.transfer(tokenPool.address, 123);
28+
expect(await tokenPool.balance.call()).to.be.bignumber.equal('123');
29+
await tokenPool.transfer(owner, 99);
30+
expect(await tokenPool.balance.call()).to.be.bignumber.equal('24');
31+
await tokenPool.transfer(owner, 24);
32+
expect(await tokenPool.balance.call()).to.be.bignumber.equal('0');
33+
});
34+
});
35+
36+
describe('transfer', function() {
37+
it('should let the owner transfer funds out', async function(){
38+
await token.transfer(tokenPool.address, 1000);
39+
40+
expect(await tokenPool.balance.call()).to.be.bignumber.equal('1000');
41+
expect(await token.balanceOf.call(anotherAccount)).to.be.bignumber.equal('0');
42+
43+
await tokenPool.transfer(anotherAccount, 1000);
44+
45+
expect(await tokenPool.balance.call()).to.be.bignumber.equal('0');
46+
expect(await token.balanceOf.call(anotherAccount)).to.be.bignumber.equal('1000');
47+
});
48+
49+
it('should NOT let other users transfer funds out', async function(){
50+
await token.transfer(tokenPool.address, 1000);
51+
await expectRevert(
52+
tokenPool.transfer(anotherAccount, 1000, { from: anotherAccount }),
53+
'Ownable: caller is not the owner'
54+
);
55+
});
56+
});
57+
58+
describe('rescueFunds', function() {
59+
beforeEach(async function(){
60+
await token.transfer(tokenPool.address, 1000);
61+
await otherToken.transfer(tokenPool.address, 2000);
62+
63+
expect(await tokenPool.balance.call()).to.be.bignumber.equal('1000');
64+
expect(await token.balanceOf.call(anotherAccount)).to.be.bignumber.equal('0');
65+
expect(await otherToken.balanceOf.call(tokenPool.address)).to.be.bignumber.equal('2000');
66+
expect(await otherToken.balanceOf.call(anotherAccount)).to.be.bignumber.equal('0');
67+
});
68+
69+
it('should let owner users claim excess funds completely', async function(){
70+
await tokenPool.rescueFunds(otherToken.address, anotherAccount, 2000);
71+
72+
expect(await tokenPool.balance.call()).to.be.bignumber.equal('1000');
73+
expect(await token.balanceOf.call(anotherAccount)).to.be.bignumber.equal('0');
74+
expect(await otherToken.balanceOf.call(tokenPool.address)).to.be.bignumber.equal('0');
75+
expect(await otherToken.balanceOf.call(anotherAccount)).to.be.bignumber.equal('2000');
76+
});
77+
78+
it('should let owner users claim excess funds partially', async function(){
79+
await tokenPool.rescueFunds(otherToken.address, anotherAccount, 777);
80+
81+
expect(await tokenPool.balance.call()).to.be.bignumber.equal('1000');
82+
expect(await token.balanceOf.call(anotherAccount)).to.be.bignumber.equal('0');
83+
expect(await otherToken.balanceOf.call(tokenPool.address)).to.be.bignumber.equal('1223');
84+
expect(await otherToken.balanceOf.call(anotherAccount)).to.be.bignumber.equal('777');
85+
});
86+
87+
it('should NOT let owner claim more than available excess funds', async function(){
88+
await expectRevert(
89+
tokenPool.rescueFunds(otherToken.address, anotherAccount, 2001),
90+
'ERC20: transfer amount exceeds balance'
91+
);
92+
});
93+
94+
it('should NOT let owner users claim held funds', async function(){
95+
await expectRevert(
96+
tokenPool.rescueFunds(token.address, anotherAccount, 1000),
97+
'TokenPool: Cannot claim token held by the contract'
98+
);
99+
});
100+
101+
it('should NOT let other users users claim excess funds', async function(){
102+
await expectRevert(
103+
tokenPool.rescueFunds(otherToken.address, anotherAccount, 2000, { from: anotherAccount }),
104+
'Ownable: caller is not the owner'
105+
);
106+
});
107+
});
108+
});

0 commit comments

Comments
 (0)