-
Notifications
You must be signed in to change notification settings - Fork 91
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
No deposit fee #117
No deposit fee #117
Conversation
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.
Small nits.
Please add a non zero check at L168 and L169 so it stops spamming the multi sig with transfer(0). (it will also decrease gas cost)
deploy/StableJoeStaking.js
Outdated
@@ -12,6 +17,7 @@ module.exports = async function ({ getNamedAccounts, deployments }) { | |||
rewardToken = "0x9Ad6C38BE94206cA50bb0d90783181662f0Cfa10"; | |||
feeCollector = "0x2fbB61a10B96254900C03F1644E9e1d2f5E76DD2"; | |||
depositFeePercent = 0; | |||
smolJoes = "0x0000000000000000000000000000000000000000"; // placeholder for now |
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.
This will revert on initialize, use any ERC20 contract as they also define balanceOf
function or use a proper mock NFT contract
contracts/StableJoeStaking.sol
Outdated
uint256 _fee = _amount.mul(depositFeePercent).div(DEPOSIT_FEE_PERCENT_PRECISION); | ||
uint256 _fee; | ||
// Only EOAs holding Smol Joes are exempt from paying the deposit fee | ||
if (smolJoes.balanceOf(_msgSender()) == 0 || _msgSender() != tx.origin) { |
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.
This will revert on smolJoes
being address 0, (this will be the case during upgrade)
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.
I check that it's not address(0) when I initialize it though?
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.
I believe is talking about the case between when the contract is upgraded and the initializeSmalJoes
function is called (which may be a very small amount of time). You could do this in a backwards compatible way by changing this to
if (address(smolJoes) != address(0) && smolJoes.balanceOf(_msgSender()) == 0 || _msgSender() != tx.origin)
this works due to the short circuiting behaviour of &&
and ||
*/ | ||
function initialize( | ||
IERC20Upgradeable _rewardToken, | ||
IERC20Upgradeable _joe, | ||
address _feeCollector, | ||
uint256 _depositFeePercent | ||
uint256 _depositFeePercent, | ||
IERC721Upgradeable _smolJoes |
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.
Add a setter for smolJoe's address as we can't use initialize during an upgrade as the contract has already been initialized.
feeCollector, | ||
depositFeePercent, | ||
smolJoes, | ||
], | ||
}, | ||
}, | ||
}, |
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.
For safety reasons, don't forget to initialize the implementation even if it's useless for transparent proxies.
What do you mean by this? |
I mean to add a: if (amount > 0) token.safeTransferFrom(msg.sender, to, amount); at L168 and 169 for the 2 different transfers |
contracts/StableJoeStaking.sol
Outdated
uint256 _depositFeePercent, | ||
IERC721Upgradeable _smolJoes | ||
uint256 _depositFeePercent | ||
) external initializer { | ||
__Ownable_init(); | ||
require(address(_rewardToken) != address(0), "StableJoeStaking: reward token can't be address(0)"); | ||
require(address(_joe) != address(0), "StableJoeStaking: joe can't be address(0)"); | ||
require(address(_smolJoes) != address(0), "StableJoeStaking: smol joes can't be address(0)"); | ||
require(_feeCollector != address(0), "StableJoeStaking: fee collector can't be address(0)"); | ||
require(_depositFeePercent <= 5e17, "StableJoeStaking: max deposit fee can't be greater than 50%"); | ||
|
||
joe = _joe; | ||
smolJoes = _smolJoes; |
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.
initialize
should still set smolJoes' address.
One easy way could be to make initializeSmolJoes
public and use it here as well
contracts/StableJoeStaking.sol
Outdated
* @notice Set the Smol Joes address | ||
* @param _newSmolJoes The new Smol Joes contract address | ||
* @notice Initialize the Smol Joes address | ||
* @param _smolJoes The Smol Joes contract address |
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.
Add a comment explaining that this function was added to be able to set smolJoe during an upgrade, as the contract was already initialized
deploy/StableJoeStaking.js
Outdated
methodName: "initialize", | ||
args: [ | ||
rewardToken, | ||
joeAddress, | ||
feeCollector, | ||
depositFeePercent, | ||
smolJoes, | ||
], | ||
methodName: "initializeSmolJoes", | ||
args: [smolJoes], |
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.
This should still call initialize
, the initializeSmolJoes
is only for an upgrade, and we need to call it by the multisig
deploy/StableJoeStaking.js
Outdated
from: deployer, | ||
proxy: { | ||
owner: proxyOwner, | ||
proxyContract: "OpenZeppelinTransparentProxy", | ||
execute: { | ||
init: { | ||
methodName: "initialize", | ||
args: [rewardToken, joeAddress, feeCollector, depositFeePercent], | ||
methodName: "initializeSmolJoes", |
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.
You shouldn't change this block (init
) as it won't get called on upgrade anyway (plus it could have unintended consequences for the unit tests if they use these deployment scripts for fixture generation). If you want to use hardhat-deploy to set the address you can add an onUpgrade
block
init: {...unchanged...},
onUpgrade: {
methodName: "initializeSmolJoes",
args: [smolJoes],
}
To do this however you will need to make the initializeSmolJoes
function idempotent (not reverting if smolJoes
variable is non 0) as it will be called every time the function gets upgraded.
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.
That's very cool, I didn't know. Thanks for sharing that!
The new Initializable
(https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/utils/Initializable.sol) from OZ makes a lot of sense for an upgrade as you could use the re-initializer.
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.
When I run this locally and try to upgrade the implementation after making a modification to StableJoeStaking
, it reverts without any reason:
Compilation finished successfully
Creating Typechain artifacts in directory types for target ethers-v5
Successfully generated Typechain artifacts!
reusing "DefaultProxyAdmin" at 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
deploying "StableJoeStaking_Implementation" (tx: 0x44ad6abb7bc903b949f19176000973c787c83e1b16f92ebbca42e6a9cec86934)...: deployed at 0x0165878A594ca255338adfa4d48449f69242Eb8F with 3619599 gas
An unexpected error occurred:
Error: ERROR processing /Users/cryptofish/workspace/joe-core/deploy/StableJoeStaking.js:
ProviderError: Error: Transaction reverted without a reason string
The deploy code looks like this:
const sJoe = await deploy("StableJoeStaking", {
from: deployer,
proxy: {
owner: deployer, // change from proxyOwner so there are no ownership issues on upgrading
proxyContract: "OpenZeppelinTransparentProxy",
execute: {
init: {
methodName: "initialize",
args: [
rewardToken,
joeAddress,
feeCollector,
depositFeePercent,
smolJoes,
],
},
onUpgrade: {
methodName: "initializeSmolJoes",
args: [smolJoes],
},
},
},
log: true,
});
I'm a little unclear on how onUpgrade
is supposed to work. I've tried asking round but no luck so if you have any suggestions that would be mega welcome!
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.
So it looks like this is failing because of the onlyOwner
modifier on initializeSmolJoes
. This is because hardhat-deploy will use upgradeToAndCall
function on the ProxyAdmin
contract which performs the upgrade and then calls initializeSmolJoes
. As the ProxyAdmin
address isn't the owner of the StableJoeStaking
contract this fails. I confirmed this by calling transferOwnership
with the ProxyAdmin
address on StableJoeStaking
and then performing the upgrade, in this case the initializeSmolJoes
function didn't revert as msg.sender
was the owner (The proxy admin address). This makes sense as the upgradeToAndCall
function should be atomic (the upgrade and function call should succeed or the tx should rollback).
AFAICT there is two options here:
- Change the ownership of
StableJoeStaking
to theProxyAdmin
contract. - Not use atomic upgrade and call the
initializeSmolJoes
function after the contract has been upgraded, this option is probably fine as the smolJoes address isn't needed for the contract to function.
One possible solution could involve this onlyAdmin
modifier but haven't looked into it enough to say for sure.
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.
Thanks for spotting that. Regarding the two options - option 1 is a pain since several other functions are onlyOwner
so you'll need to transfer ownership of StableJoeStaking
back to the multisig.
Can't put initializeSmolJoes
under the initializer
modifier either. If only we could use reinitializer
in the latest OZ Initializable
, but it's v8 so not compatible.
Anyways, gone with option 2 here and turned initializeSmolJoes
to setSmolJoes
.
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.
Option 1 would be a big mistake: Transparent Proxies don't allow the owner of the proxy to call any function of the implementation. So if the ownership is transferred, you couldn't call any onlyOwner
function, nor transfer the ownership again.
contracts/StableJoeStaking.sol
Outdated
* @notice Initialize the Smol Joes address | ||
* @param _smolJoes The Smol Joes contract address | ||
*/ | ||
function initializeSmolJoes(IERC721Upgradeable _smolJoes) external onlyOwner { |
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.
Is there a reason you only allow setting this value once? From a purely pragmatic point of view if there is some mistake in the smolJoes
address the contract will be permanently broken and you won't be able to call it again with the correct address.
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.
That's not wrong, but as it's a proxy nothing is permanent in any case.
contracts/StableJoeStaking.sol
Outdated
@@ -134,7 +137,7 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable { | |||
|
|||
uint256 _fee; | |||
// Only EOAs holding Smol Joes are exempt from paying the deposit fee | |||
if (smolJoes.balanceOf(_msgSender()) == 0 || _msgSender() != tx.origin) { | |||
if ((address(smolJoes) != 0 && smolJoes.balanceOf(_msgSender()) == 0) || _msgSender() != tx.origin) { |
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.
Wrong parenthesis, if smolJoe is not set EOAs wouldn't pay any fees, change to:
if (address(smolJoes) != 0 && (smolJoes.balanceOf(_msgSender()) == 0 || _msgSender() != tx.origin)) {
contracts/StableJoeStaking.sol
Outdated
require(address(_smolJoes) != address(0), "StableJoeStaking: smol joes can't be address(0)"); | ||
require(address(smolJoes) == address(0), "StableJoeStaking: smol joes already initialized"); | ||
function setSmolJoes(IERC721Upgradeable _smolJoes) external onlyOwner { | ||
require(address(_smolJoes) != address(0), "StableJoeStaking: smol joes can't be address(0)3"); |
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.
Typo address(0)3
console.log("Setting Smol Joes..."); | ||
const sJoeProxy = await ethers.getContract("StableJoeStaking"); | ||
await sJoeProxy.setSmolJoes(smolJoes); |
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.
If it's newly deployed then this is not necessary, as it would set it with initialize
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.
On the upgrade though, it will be "newly deployed" but it won't call initialize.
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.
Oh indeed, my bad I thought it was setting it on the implementation again
contracts/StableJoeStaking.sol
Outdated
@@ -66,6 +67,9 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable { | |||
/// @dev Info of each user that stakes JOE | |||
mapping(address => UserInfo) private userInfo; | |||
|
|||
/// @dev Smol Joes contract address | |||
IERC721Upgradeable smolJoes; |
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.
NIT: Mark this variable explicitly as public (default if not specified) to match the style of other variables.
uint256 _fee = _amount.mul(depositFeePercent).div(DEPOSIT_FEE_PERCENT_PRECISION); | ||
uint256 _fee; | ||
// Only EOAs holding Smol Joes are exempt from paying the deposit fee | ||
if (address(smolJoes) != 0 && (smolJoes.balanceOf(_msgSender()) == 0 || _msgSender() != tx.origin)) { |
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.
Tests are failing as you are comparing address with int. 0 needs to be cast to address address(0)
(I also think I made the same error when proposing the code snippet)
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.
I think there is a potential for a problem here if you deploy this update and don't set the smolJoe
variable immediately, as &&
short circuits nobody would pay any fee on deposit until smolJoe
address was set. It looks like this will be covered by the deployment script but still worth being aware.
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.
I think the following fixes the above problem.
if (!(address(smolJoes) != address(0) && smolJoes.balanceOf(_msgSender()) > 0 && _msgSender() == tx.origin)) {
_fee = _amount.mul(depositFeePercent).div(DEPOSIT_FEE_PERCENT_PRECISION);
}
it("should allow EOAs with Smol Joes to be exempt from paying deposit fee", async function () { | ||
await this.stableJoeStaking | ||
.connect(this.dylan) | ||
.deposit(ethers.utils.parseEther("100")); |
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.
There doesn't look to be a bug there but for fuller test coverage you should also deposit via the vault before transferring the ERC721 to it.
In this PR I add feature to sJOE that exempts EOAs holding a Smol Joe from paying the deposit fee.