-
Notifications
You must be signed in to change notification settings - Fork 369
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
Prevent CELO Transfer to Unreleased Treasury #11222
Changes from all commits
5f7e5d1
4084bac
f3fed48
0b473c2
b012f29
3a4f87a
ad8831d
5fa905d
b3023c2
9681185
18ba704
8d862d1
70b12a5
0c74a5e
7379aff
7205bb7
b89cc74
758d097
d7b32fb
a66e578
de3ae19
8e86672
0fae892
0e55b36
973c21e
315aaee
efebfb7
4c9ee98
c6fd893
04af0f7
29aadbb
41fe5e6
05dcde2
ce59f2e
01a3098
f7454fc
ff4fb8f
55d462a
a2522a2
dc9ece5
85303f9
7979ffb
f7cb22c
9403236
b63e487
96eb887
62bfb44
7e5e8ea
40e692b
a536b5f
2c49e64
4e1cf6a
2eab03f
49a49ed
4cbc6d9
080b430
5dc389f
f4b4564
59ea380
cfe3b30
7fe7a29
d2fd5c4
df84c5d
c85d594
032f543
91baa1b
acd1f86
a35fdba
02454f4
cac4a13
580bd9d
3e5c033
2a26cc6
63db861
c516109
19122fe
5aafad1
b182645
ec5ed87
102b224
19c5138
7778f3b
3d78151
539b513
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,15 +5,27 @@ import "@openzeppelin/contracts8/security/ReentrancyGuard.sol"; | |
import "@openzeppelin/contracts8/utils/math/Math.sol"; | ||
|
||
import "./UsingRegistry.sol"; | ||
import "../common/IsL2Check.sol"; | ||
|
||
import "../../contracts/common/Initializable.sol"; | ||
import "./interfaces/ICeloUnreleasedTreasuryInitializer.sol"; | ||
|
||
/** | ||
* @title Contract for unreleased Celo tokens. | ||
* @notice This contract is not allowed to receive transfers of CELO, | ||
* to avoid miscalculating the epoch rewards and to prevent any malicious actor | ||
* from routing stolen fund through the epoch reward distribution. | ||
*/ | ||
contract CeloUnreleasedTreasury is UsingRegistry, ReentrancyGuard, Initializable, IsL2Check { | ||
contract CeloUnreleasedTreasury is | ||
ICeloUnreleasedTreasuryInitializer, | ||
UsingRegistry, | ||
ReentrancyGuard, | ||
Initializable | ||
{ | ||
bool internal hasAlreadyReleased; | ||
|
||
// Remaining epoch rewards to distribute. | ||
uint256 internal remainingBalanceToRelease; | ||
|
||
event Released(address indexed to, uint256 amount); | ||
|
||
modifier onlyEpochManager() { | ||
|
@@ -46,11 +58,31 @@ contract CeloUnreleasedTreasury is UsingRegistry, ReentrancyGuard, Initializable | |
* @param amount The amount to release. | ||
*/ | ||
function release(address to, uint256 amount) external onlyEpochManager { | ||
require(address(this).balance >= amount, "Insufficient balance."); | ||
if (!hasAlreadyReleased) { | ||
remainingBalanceToRelease = address(this).balance; | ||
hasAlreadyReleased = true; | ||
} | ||
|
||
require(remainingBalanceToRelease >= amount, "Insufficient balance."); | ||
Comment on lines
+62
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd use ERC20 balance so that it can be ported to another token if needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I understand what you mean by porting to another token. As in distributing a different ERC20 token instead of CELO? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we ever want to use this contract for another ERC20, it won't work at we use ERC20 interfaces instead of native ones. |
||
remainingBalanceToRelease -= amount; | ||
soloseng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
require(getCeloToken().transfer(to, amount), "CELO transfer failed."); | ||
|
||
emit Released(to, amount); | ||
} | ||
|
||
/** | ||
* @notice Returns the remaining balance this contract has left to release. | ||
* @dev This uses internal accounting of the released balance, | ||
* to avoid recounting CELO that was transferred back to this contract. | ||
*/ | ||
function getRemainingBalanceToRelease() external view returns (uint256) { | ||
if (!hasAlreadyReleased) { | ||
return address(this).balance; | ||
} else { | ||
return remainingBalanceToRelease; | ||
} | ||
} | ||
|
||
/** | ||
* @notice Returns the storage, major, minor, and patch version of the contract. | ||
* @return Storage version of the contract. | ||
|
soloseng marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,7 @@ contract GoldTokenTest is Test, TestConstants, IsL2Check { | |
address receiver; | ||
address sender; | ||
address celoTokenOwner; | ||
address celoTokenDistributionSchedule; | ||
address celoUnreleasedTreasuryAddress; | ||
|
||
event Transfer(address indexed from, address indexed to, uint256 value); | ||
event TransferComment(string comment); | ||
|
@@ -26,16 +26,17 @@ contract GoldTokenTest is Test, TestConstants, IsL2Check { | |
} | ||
|
||
function setUp() public { | ||
celoTokenOwner = actor("celoTokenOwner"); | ||
celoUnreleasedTreasuryAddress = actor("celoUnreleasedTreasury"); | ||
deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); | ||
deployCodeTo("CeloUnreleasedTreasury.sol", abi.encode(false), celoUnreleasedTreasuryAddress); | ||
registry = IRegistry(REGISTRY_ADDRESS); | ||
|
||
celoTokenOwner = actor("celoTokenOwner"); | ||
celoTokenDistributionSchedule = actor("celoTokenDistributionSchedule"); | ||
vm.prank(celoTokenOwner); | ||
celoToken = new GoldToken(true); | ||
vm.prank(celoTokenOwner); | ||
celoToken.setRegistry(REGISTRY_ADDRESS); | ||
registry.setAddressFor("CeloUnreleasedTreasury", celoTokenDistributionSchedule); | ||
registry.setAddressFor("CeloUnreleasedTreasury", celoUnreleasedTreasuryAddress); | ||
receiver = actor("receiver"); | ||
sender = actor("sender"); | ||
vm.deal(receiver, ONE_CELOTOKEN); | ||
|
@@ -126,6 +127,26 @@ contract GoldTokenTest_transfer is GoldTokenTest { | |
vm.expectRevert(); | ||
celoToken.transfer(address(0), ONE_CELOTOKEN); | ||
} | ||
|
||
function test_Succeeds_whenTransferingToCeloUnreleasedTreasury() public { | ||
vm.prank(sender); | ||
uint256 balanceBefore = celoToken.balanceOf(celoUnreleasedTreasuryAddress); | ||
|
||
celoToken.transfer(celoUnreleasedTreasuryAddress, ONE_CELOTOKEN); | ||
uint256 balanceAfter = celoToken.balanceOf(celoUnreleasedTreasuryAddress); | ||
assertGt(balanceAfter, balanceBefore); | ||
Comment on lines
+132
to
+137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't this fail? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if ERC20 token transfer succeed, for consistency shouldn't we make native transfer for as well? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This follows the same pattern as all other core contracts. No fallback implemented but still receive ERC20 transfers. Since it now keeps an internal accounting of the remaining distribution, any transfers are ignored. |
||
} | ||
|
||
function test_FailsWhenNativeTransferingToCeloUnreleasedTreasury() public payable { | ||
(bool success, ) = address(uint160(celoUnreleasedTreasuryAddress)).call.value(ONE_CELOTOKEN)( | ||
"" | ||
); | ||
|
||
assertFalse(success); | ||
|
||
bool sent = address(uint160(celoUnreleasedTreasuryAddress)).send(ONE_CELOTOKEN); | ||
assertFalse(sent); | ||
} | ||
} | ||
|
||
contract GoldTokenTest_transferFrom is GoldTokenTest { | ||
|
@@ -150,6 +171,14 @@ contract GoldTokenTest_transferFrom is GoldTokenTest { | |
celoToken.transferFrom(sender, address(0), ONE_CELOTOKEN); | ||
} | ||
|
||
function test_Succeeds_whenTransferingToCeloUnreleasedTreasury() public { | ||
uint256 balanceBefore = celoToken.balanceOf(celoUnreleasedTreasuryAddress); | ||
vm.prank(receiver); | ||
celoToken.transferFrom(sender, celoUnreleasedTreasuryAddress, ONE_CELOTOKEN); | ||
uint256 balanceAfter = celoToken.balanceOf(celoUnreleasedTreasuryAddress); | ||
assertGt(balanceAfter, balanceBefore); | ||
} | ||
|
||
function test_Reverts_WhenTransferMoreThanSenderHas() public { | ||
uint256 value = sender.balance + ONE_CELOTOKEN * 4; | ||
|
||
|
@@ -198,7 +227,7 @@ contract GoldTokenTest_mint is GoldTokenTest { | |
vm.expectRevert("Only VM can call"); | ||
celoToken.mint(receiver, ONE_CELOTOKEN); | ||
|
||
vm.prank(celoTokenDistributionSchedule); | ||
vm.prank(celoUnreleasedTreasuryAddress); | ||
vm.expectRevert("Only VM can call"); | ||
celoToken.mint(receiver, ONE_CELOTOKEN); | ||
} | ||
|
@@ -220,7 +249,7 @@ contract GoldTokenTest_mint is GoldTokenTest { | |
|
||
function test_Reverts_whenL2() public _whenL2 { | ||
vm.expectRevert("This method is no longer supported in L2."); | ||
vm.prank(celoTokenDistributionSchedule); | ||
vm.prank(celoUnreleasedTreasuryAddress); | ||
celoToken.mint(receiver, ONE_CELOTOKEN); | ||
vm.expectRevert("This method is no longer supported in L2."); | ||
vm.prank(address(0)); | ||
|
@@ -249,23 +278,24 @@ contract CeloTokenMockTest is Test, TestConstants { | |
GoldTokenMock mockCeloToken; | ||
uint256 ONE_CELOTOKEN = 1000000000000000000; | ||
address burnAddress = address(0x000000000000000000000000000000000000dEaD); | ||
address celoUnreleasedTreasury; | ||
address celoUnreleasedTreasuryAddress = actor("CeloUnreleasedTreasury"); | ||
|
||
modifier _whenL2() { | ||
deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); | ||
vm.deal(celoUnreleasedTreasury, L2_INITIAL_STASH_BALANCE); | ||
vm.deal(celoUnreleasedTreasuryAddress, L2_INITIAL_STASH_BALANCE); | ||
_; | ||
} | ||
|
||
function setUp() public { | ||
deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); | ||
deployCodeTo("CeloUnreleasedTreasury.sol", abi.encode(false), celoUnreleasedTreasuryAddress); | ||
registry = IRegistry(REGISTRY_ADDRESS); | ||
|
||
mockCeloToken = new GoldTokenMock(); | ||
mockCeloToken.setRegistry(REGISTRY_ADDRESS); | ||
mockCeloToken.setTotalSupply(L1_MINTED_CELO_SUPPLY); | ||
celoUnreleasedTreasury = actor("CeloUnreleasedTreasury"); | ||
registry.setAddressFor("CeloUnreleasedTreasury", celoUnreleasedTreasury); | ||
vm.deal(celoUnreleasedTreasuryAddress, L2_INITIAL_STASH_BALANCE); | ||
registry.setAddressFor("CeloUnreleasedTreasury", celoUnreleasedTreasuryAddress); | ||
} | ||
} | ||
|
||
|
@@ -304,7 +334,7 @@ contract GoldTokenTest_AllocatedSupply is CeloTokenMockTest { | |
} | ||
|
||
function test_ShouldReturn_WhenWithdrawn_WhenInL2() public _whenL2 { | ||
deal(address(celoUnreleasedTreasury), ONE_CELOTOKEN); | ||
deal(celoUnreleasedTreasuryAddress, ONE_CELOTOKEN); | ||
assertEq(mockCeloToken.allocatedSupply(), mockCeloToken.totalSupply() - ONE_CELOTOKEN); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make the fallback payable function explicitly revert?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could add a fallback function to make it explicitly revert, but that would acheive the same result as it is currently.