-
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
Changes from 7 commits
ee4426b
80e9db9
e68cd09
b9048ef
1d5a73b
8df35d7
b005318
a468913
138c840
fd88471
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 |
---|---|---|
|
@@ -6,6 +6,7 @@ import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; | |
import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; | ||
|
||
/** | ||
|
@@ -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; | ||
|
||
/// @notice Emitted when a user deposits JOE | ||
event Deposit(address indexed user, uint256 amount, uint256 fee); | ||
|
||
|
@@ -87,6 +91,9 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable { | |
/// @notice Emitted when owner removes a token from the reward tokens list | ||
event RewardTokenRemoved(address token); | ||
|
||
/// @notice Emitted when Smol Joes address is changed | ||
event SmolJoesInitialized(IERC721Upgradeable newSmolJoes, IERC721Upgradeable oldSmolJoes); | ||
|
||
/** | ||
* @notice Initialize a new StableJoeStaking contract | ||
* @dev This contract needs to receive an ERC20 `_rewardToken` in order to distribute them | ||
|
@@ -125,7 +132,11 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable { | |
function deposit(uint256 _amount) external { | ||
UserInfo storage user = userInfo[_msgSender()]; | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. This will revert on 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 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 commentThe 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 |
||
_fee = _amount.mul(depositFeePercent).div(DEPOSIT_FEE_PERCENT_PRECISION); | ||
} | ||
uint256 _amountMinusFee = _amount.sub(_fee); | ||
|
||
uint256 _previousAmount = user.amount; | ||
|
@@ -153,8 +164,8 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable { | |
} | ||
|
||
internalJoeBalance = internalJoeBalance.add(_amountMinusFee); | ||
joe.safeTransferFrom(_msgSender(), feeCollector, _fee); | ||
joe.safeTransferFrom(_msgSender(), address(this), _amountMinusFee); | ||
if (_fee > 0) joe.safeTransferFrom(_msgSender(), feeCollector, _fee); | ||
if (_amountMinusFee > 0) joe.safeTransferFrom(_msgSender(), address(this), _amountMinusFee); | ||
emit Deposit(_msgSender(), _amountMinusFee, _fee); | ||
} | ||
|
||
|
@@ -224,6 +235,17 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable { | |
emit DepositFeeChanged(_depositFeePercent, oldFee); | ||
} | ||
|
||
/** | ||
* @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 commentThe 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 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. That's not wrong, but as it's a proxy nothing is permanent in any case. |
||
require(address(_smolJoes) != address(0), "StableJoeStaking: smol joes can't be address(0)"); | ||
require(address(smolJoes) == address(0), "StableJoeStaking: smol joes already initialized"); | ||
smolJoes = _smolJoes; | ||
emit SmolJoesInitialized(_smolJoes, smolJoes); | ||
} | ||
|
||
/** | ||
* @notice View function to see pending reward token on frontend | ||
* @param _user The address of the user | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity 0.6.12; | ||
|
||
import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; | ||
|
||
contract ERC721Mock is ERC721 { | ||
uint256 tokenOwners; | ||
|
||
constructor(string memory name, string memory symbol) public ERC721(name, symbol) {} | ||
|
||
function mint(address _account) external { | ||
_mint(_account, tokenOwners); | ||
tokenOwners += 1; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity 0.7.6; | ||
|
||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | ||
import "../StableJoeStaking.sol"; | ||
|
||
contract StableJoeVaultMock { | ||
IERC20 public immutable joe; | ||
StableJoeStaking public immutable sJoe; | ||
|
||
constructor(IERC20 _joe, StableJoeStaking _sJoe) public { | ||
joe = _joe; | ||
sJoe = _sJoe; | ||
} | ||
|
||
function deposit(uint256 _amount) external { | ||
joe.transferFrom(msg.sender, address(this), _amount); | ||
joe.approve(address(sJoe), _amount); | ||
sJoe.deposit(_amount); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,12 @@ module.exports = async function ({ getNamedAccounts, deployments }) { | |
const { deploy, catchUnknownSigner } = deployments; | ||
const { deployer } = await getNamedAccounts(); | ||
|
||
let rewardToken, joeAddress, feeCollector, depositFeePercent, proxyOwner; | ||
let rewardToken, | ||
joeAddress, | ||
feeCollector, | ||
depositFeePercent, | ||
smolJoes, | ||
proxyOwner; | ||
|
||
const chainId = await getChainId(); | ||
if (chainId == 4) { | ||
|
@@ -12,33 +17,45 @@ module.exports = async function ({ getNamedAccounts, deployments }) { | |
rewardToken = "0x9Ad6C38BE94206cA50bb0d90783181662f0Cfa10"; | ||
feeCollector = "0x2fbB61a10B96254900C03F1644E9e1d2f5E76DD2"; | ||
depositFeePercent = 0; | ||
proxyOwner = deployer.address; | ||
smolJoes = "0xce347E069B68C53A9ED5e7DA5952529cAF8ACCd4"; // use an ERC20 as placeholder for now | ||
proxyOwner = deployer; | ||
} else if (chainId == 43114 || chainId == 31337) { | ||
// avalanche mainnet or hardhat network addresses | ||
joeAddress = "0x6e84a6216eA6dACC71eE8E6b0a5B7322EEbC0fDd"; | ||
// USDC.e | ||
rewardToken = "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E"; | ||
feeCollector = "0x2fbB61a10B96254900C03F1644E9e1d2f5E76DD2"; | ||
depositFeePercent = 0; | ||
smolJoes = "0xC70DF87e1d98f6A531c8E324C9BCEC6FC82B5E8d"; | ||
proxyOwner = "0x2fbB61a10B96254900C03F1644E9e1d2f5E76DD2"; | ||
} | ||
|
||
await catchUnknownSigner( | ||
deploy("StableJoeStaking", { | ||
await catchUnknownSigner(async () => { | ||
const sJoe = await deploy("StableJoeStaking", { | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. You shouldn't change this block (
To do this however you will need to make the 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. That's very cool, I didn't know. Thanks for sharing that! 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. When I run this locally and try to upgrade the implementation after making a modification to
The deploy code looks like this:
I'm a little unclear on how 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. So it looks like this is failing because of the AFAICT there is two options here:
One possible solution could involve this 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. Thanks for spotting that. Regarding the two options - option 1 is a pain since several other functions are Can't put Anyways, gone with option 2 here and turned 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. 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 |
||
args: [smolJoes], | ||
}, | ||
}, | ||
}, | ||
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. For safety reasons, don't forget to initialize the implementation even if it's useless for transparent proxies. |
||
log: true, | ||
}) | ||
); | ||
}); | ||
if (sJoe.newlyDeployed) { | ||
console.log("Initializing implementation for safe measure..."); | ||
const impl = await ethers.getContract("StableJoeStaking_Implementation"); | ||
await impl.initialize( | ||
rewardToken, | ||
joeAddress, | ||
feeCollector, | ||
depositFeePercent | ||
); | ||
} | ||
}); | ||
}; | ||
|
||
module.exports.tags = ["StableJoeStaking"]; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,39 +9,54 @@ describe("Stable Joe Staking", function () { | |
this.StableJoeStakingCF = await ethers.getContractFactory( | ||
"StableJoeStaking" | ||
); | ||
this.StableJoeVaultCF = await ethers.getContractFactory( | ||
"StableJoeVaultMock" | ||
); | ||
this.JoeTokenCF = await ethers.getContractFactory("JoeToken"); | ||
this.SmolJoesCF = await ethers.getContractFactory("ERC721Mock"); | ||
|
||
this.signers = await ethers.getSigners(); | ||
this.dev = this.signers[0]; | ||
this.alice = this.signers[1]; | ||
this.bob = this.signers[2]; | ||
this.carol = this.signers[3]; | ||
this.dylan = this.signers[4]; | ||
this.joeMaker = this.signers[4]; | ||
this.penaltyCollector = this.signers[5]; | ||
}); | ||
|
||
beforeEach(async function () { | ||
this.rewardToken = await this.JoeTokenCF.deploy(); | ||
this.joe = await this.JoeTokenCF.deploy(); | ||
this.smolJoes = await this.SmolJoesCF.deploy("Smol Joes", "SMOL JOES"); | ||
|
||
await this.joe.mint(this.alice.address, ethers.utils.parseEther("1000")); | ||
await this.joe.mint(this.bob.address, ethers.utils.parseEther("1000")); | ||
await this.joe.mint(this.carol.address, ethers.utils.parseEther("1000")); | ||
await this.joe.mint(this.dylan.address, ethers.utils.parseEther("1000")); | ||
await this.rewardToken.mint( | ||
this.joeMaker.address, | ||
ethers.utils.parseEther("1000000") | ||
); // 1_000_000 tokens | ||
|
||
await this.smolJoes.mint(this.dylan.address); | ||
|
||
this.stableJoeStaking = await hre.upgrades.deployProxy( | ||
this.StableJoeStakingCF, | ||
[ | ||
this.rewardToken.address, | ||
this.joe.address, | ||
this.penaltyCollector.address, | ||
ethers.utils.parseEther("0.03"), | ||
this.smolJoes.address, | ||
] | ||
); | ||
|
||
this.stableJoeVault = await this.StableJoeVaultCF.deploy( | ||
this.joe.address, | ||
this.stableJoeStaking.address | ||
); | ||
|
||
await this.joe | ||
.connect(this.alice) | ||
.approve( | ||
|
@@ -60,6 +75,16 @@ describe("Stable Joe Staking", function () { | |
this.stableJoeStaking.address, | ||
ethers.utils.parseEther("100000") | ||
); | ||
await this.joe | ||
.connect(this.dylan) | ||
.approve( | ||
this.stableJoeStaking.address, | ||
ethers.utils.parseEther("100000") | ||
); | ||
|
||
await this.joe | ||
.connect(this.dylan) | ||
.approve(this.stableJoeVault.address, ethers.utils.parseEther("100000")); | ||
}); | ||
|
||
describe("should allow deposits and withdraws", function () { | ||
|
@@ -672,6 +697,29 @@ describe("Stable Joe Staking", function () { | |
); | ||
}); | ||
|
||
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 commentThe 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. |
||
expect( | ||
await this.joe.balanceOf(this.stableJoeStaking.address) | ||
).to.be.equal(ethers.utils.parseEther("100")); | ||
|
||
// Transfer Smol Joe to vault contract and deposit via vault | ||
await this.smolJoes | ||
.connect(this.dylan) | ||
.transferFrom(this.dylan.address, this.stableJoeVault.address, 0); | ||
await this.stableJoeVault | ||
.connect(this.dylan) | ||
.deposit(ethers.utils.parseEther("100")); | ||
expect( | ||
await this.joe.balanceOf(this.stableJoeStaking.address) | ||
).to.be.equal(ethers.utils.parseEther("197")); | ||
expect( | ||
await this.joe.balanceOf(this.penaltyCollector.address) | ||
).to.be.equal(ethers.utils.parseEther("3")); | ||
}); | ||
|
||
it("should allow emergency withdraw", async function () { | ||
await this.stableJoeStaking | ||
.connect(this.alice) | ||
|
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.