Skip to content

Commit 11caa6d

Browse files
committed
security audit nft staking
1 parent 73278be commit 11caa6d

16 files changed

+3212
-96
lines changed

.env.template

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ TREASURY_ADDRESS=
1717

1818
// address of main pool factory
1919
FACTORY_ADDRESS=
20+
21+
// mnemonic phrase for temporary deployer
22+
MNEMONIC_PHRASE=
23+
24+
// polygonscan api ley
25+
POLYGONSCAN_KEY=

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ see [gysr.io](https://www.gysr.io/)
99
## Setup
1010

1111
Both **Node.js** and **npm** are required for package management and testing. See instructions
12-
for installation [here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). This
13-
codebase has been tested with `Node.js: v10.16.0` and `npm: 6.9.0`.
12+
for installation [here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
1413

1514
This project uses [OpenZeppelin](https://docs.openzeppelin.com/)
1615
and [Truffle](https://www.trufflesuite.com/docs/truffle)

contracts/ERC721StakingModule.sol

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
ERC721StakingModule
3+
4+
https://github.com/gysr-io/core
5+
6+
SPDX-License-Identifier: MIT
7+
*/
8+
9+
pragma solidity 0.8.4;
10+
11+
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
12+
13+
import "./interfaces/IStakingModule.sol";
14+
15+
/**
16+
* @title ERC721 staking module
17+
*
18+
* @notice this staking module allows users to deposit one or more ERC721
19+
* tokens in exchange for shares credited to their address. When the user
20+
* unstakes, these shares will be burned and a reward will be distributed.
21+
*/
22+
contract ERC721StakingModule is IStakingModule {
23+
// constant
24+
uint256 public constant SHARES_PER_TOKEN = 10**18;
25+
26+
// members
27+
IERC721 private immutable _token;
28+
address private immutable _factory;
29+
30+
mapping(address => uint256) public counts;
31+
mapping(uint256 => address) public owners;
32+
mapping(address => mapping(uint256 => uint256)) public tokenByOwner;
33+
mapping(uint256 => uint256) public tokenIndex;
34+
35+
/**
36+
* @param token_ the token that will be rewarded
37+
*/
38+
constructor(address token_, address factory_) {
39+
require(IERC165(token_).supportsInterface(0x80ac58cd), "smn1");
40+
_token = IERC721(token_);
41+
_factory = factory_;
42+
}
43+
44+
/**
45+
* @inheritdoc IStakingModule
46+
*/
47+
function tokens()
48+
external
49+
view
50+
override
51+
returns (address[] memory tokens_)
52+
{
53+
tokens_ = new address[](1);
54+
tokens_[0] = address(_token);
55+
}
56+
57+
/**
58+
* @inheritdoc IStakingModule
59+
*/
60+
function balances(address user)
61+
external
62+
view
63+
override
64+
returns (uint256[] memory balances_)
65+
{
66+
balances_ = new uint256[](1);
67+
balances_[0] = counts[user];
68+
}
69+
70+
/**
71+
* @inheritdoc IStakingModule
72+
*/
73+
function factory() external view override returns (address) {
74+
return _factory;
75+
}
76+
77+
/**
78+
* @inheritdoc IStakingModule
79+
*/
80+
function totals()
81+
external
82+
view
83+
override
84+
returns (uint256[] memory totals_)
85+
{
86+
totals_ = new uint256[](1);
87+
totals_[0] = _token.balanceOf(address(this));
88+
}
89+
90+
/**
91+
* @inheritdoc IStakingModule
92+
*/
93+
function stake(
94+
address user,
95+
uint256 amount,
96+
bytes calldata data
97+
) external override onlyOwner returns (address, uint256) {
98+
// validate
99+
require(amount > 0, "smn2");
100+
require(amount <= _token.balanceOf(user), "smn3");
101+
require(data.length == 32 * amount, "smn4");
102+
103+
uint256 sz = counts[user];
104+
105+
// stake
106+
for (uint256 i = 0; i < amount; i++) {
107+
// get token id
108+
uint256 id;
109+
uint256 pos = 132 + 32 * i;
110+
assembly {
111+
id := calldataload(pos)
112+
}
113+
114+
// ownership mappings
115+
owners[id] = user;
116+
uint256 len = sz + i;
117+
tokenByOwner[user][len] = id;
118+
tokenIndex[id] = len;
119+
120+
// transfer to module
121+
_token.transferFrom(user, address(this), id);
122+
}
123+
124+
// update position
125+
counts[user] = sz + amount;
126+
127+
// emit
128+
uint256 s = amount * SHARES_PER_TOKEN;
129+
emit Staked(user, address(_token), amount, s);
130+
131+
return (user, s);
132+
}
133+
134+
/**
135+
* @inheritdoc IStakingModule
136+
*/
137+
function unstake(
138+
address user,
139+
uint256 amount,
140+
bytes calldata data
141+
) external override onlyOwner returns (address, uint256) {
142+
// validate
143+
require(amount > 0, "smn5");
144+
uint256 sz = counts[user];
145+
require(amount <= sz, "smn6");
146+
require(data.length == 32 * amount, "smn7");
147+
148+
// unstake
149+
for (uint256 i = 0; i < amount; i++) {
150+
// get token id
151+
uint256 id;
152+
uint256 pos = 132 + 32 * i;
153+
assembly {
154+
id := calldataload(pos)
155+
}
156+
157+
// ownership
158+
require(owners[id] == user, "smn8");
159+
delete owners[id];
160+
161+
// clean up ownership mappings
162+
uint256 lastIndex = sz - 1 - i;
163+
if (amount != sz) {
164+
// reindex on partial unstake
165+
uint256 index = tokenIndex[id];
166+
if (index != lastIndex) {
167+
uint256 lastId = tokenByOwner[user][lastIndex];
168+
tokenByOwner[user][index] = lastId;
169+
tokenIndex[lastId] = index;
170+
}
171+
}
172+
delete tokenByOwner[user][lastIndex];
173+
delete tokenIndex[id];
174+
175+
// transfer to user
176+
_token.safeTransferFrom(address(this), user, id);
177+
}
178+
179+
// update position
180+
counts[user] = sz - amount;
181+
182+
// emit
183+
uint256 s = amount * SHARES_PER_TOKEN;
184+
emit Unstaked(user, address(_token), amount, s);
185+
186+
return (user, s);
187+
}
188+
189+
/**
190+
* @inheritdoc IStakingModule
191+
*/
192+
function claim(
193+
address user,
194+
uint256 amount,
195+
bytes calldata
196+
) external override onlyOwner returns (address, uint256) {
197+
// validate
198+
require(amount > 0, "smn9");
199+
require(amount <= counts[user], "smn10");
200+
201+
uint256 s = amount * SHARES_PER_TOKEN;
202+
emit Claimed(user, address(_token), amount, s);
203+
return (user, s);
204+
}
205+
206+
/**
207+
* @inheritdoc IStakingModule
208+
*/
209+
function update(address) external override {}
210+
211+
/**
212+
* @inheritdoc IStakingModule
213+
*/
214+
function clean() external override {}
215+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
ERC721StakingModuleFactory
3+
4+
https://github.com/gysr-io/core
5+
6+
SPDX-License-Identifier: MIT
7+
*/
8+
9+
pragma solidity 0.8.4;
10+
11+
import "./interfaces/IModuleFactory.sol";
12+
import "./ERC721StakingModule.sol";
13+
14+
/**
15+
* @title ERC721 staking module factory
16+
*
17+
* @notice this factory contract handles deployment for the
18+
* ERC721StakingModule contract
19+
*
20+
* @dev it is called by the parent PoolFactory and is responsible
21+
* for parsing constructor arguments before creating a new contract
22+
*/
23+
contract ERC721StakingModuleFactory is IModuleFactory {
24+
/**
25+
* @inheritdoc IModuleFactory
26+
*/
27+
function createModule(bytes calldata data)
28+
external
29+
override
30+
returns (address)
31+
{
32+
// validate
33+
require(data.length == 32, "smnf1");
34+
35+
// parse staking token
36+
address token;
37+
assembly {
38+
token := calldataload(68)
39+
}
40+
41+
// create module
42+
ERC721StakingModule module =
43+
new ERC721StakingModule(token, address(this));
44+
module.transferOwnership(msg.sender);
45+
46+
// output
47+
emit ModuleCreated(msg.sender, address(module));
48+
return address(module);
49+
}
50+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
ERC721StakingModuleInfo
3+
4+
https://github.com/gysr-io/core
5+
6+
SPDX-License-Identifier: MIT
7+
*/
8+
9+
pragma solidity 0.8.4;
10+
11+
import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
12+
13+
import "../interfaces/IStakingModule.sol";
14+
import "../ERC721StakingModule.sol";
15+
16+
/**
17+
* @title ERC721 staking module info library
18+
*
19+
* @notice this library provides read-only convenience functions to query
20+
* additional information about the ERC721StakingModule contract.
21+
*/
22+
library ERC721StakingModuleInfo {
23+
/**
24+
* @notice convenience function to get token metadata in a single call
25+
* @param module address of staking module
26+
* @return address
27+
* @return name
28+
* @return symbol
29+
* @return decimals
30+
*/
31+
function token(address module)
32+
public
33+
view
34+
returns (
35+
address,
36+
string memory,
37+
string memory,
38+
uint8
39+
)
40+
{
41+
IStakingModule m = IStakingModule(module);
42+
IERC721Metadata tkn = IERC721Metadata(m.tokens()[0]);
43+
if (!tkn.supportsInterface(0x5b5e139f)) {
44+
return (address(tkn), "", "", 0);
45+
}
46+
return (address(tkn), tkn.name(), tkn.symbol(), 0);
47+
}
48+
49+
/**
50+
* @notice quote the share value for an amount of tokens
51+
* @param module address of staking module
52+
* @param addr account address of interest
53+
* @param amount number of tokens. if zero, return entire share balance
54+
* @return number of shares
55+
*/
56+
function shares(
57+
address module,
58+
address addr,
59+
uint256 amount
60+
) public view returns (uint256) {
61+
ERC721StakingModule m = ERC721StakingModule(module);
62+
63+
// return all user shares
64+
if (amount == 0) {
65+
return m.counts(addr) * m.SHARES_PER_TOKEN();
66+
}
67+
68+
require(amount <= m.counts(addr), "smni1");
69+
return amount * m.SHARES_PER_TOKEN();
70+
}
71+
72+
/**
73+
* @notice get shares per token
74+
* @param module address of staking module
75+
* @return current shares per token
76+
*/
77+
function sharesPerToken(address module) public view returns (uint256) {
78+
ERC721StakingModule m = ERC721StakingModule(module);
79+
return m.SHARES_PER_TOKEN() * 1e18;
80+
}
81+
82+
/**
83+
* @notice get staked token ids for user
84+
* @param module address of staking module
85+
* @param addr account address of interest
86+
* @param amount number of tokens to enumerate
87+
* @param start token index to start at
88+
* @return ids array of token ids
89+
*/
90+
function tokenIds(
91+
address module,
92+
address addr,
93+
uint256 amount,
94+
uint256 start
95+
) public view returns (uint256[] memory ids) {
96+
ERC721StakingModule m = ERC721StakingModule(module);
97+
uint256 sz = m.counts(addr);
98+
require(start + amount <= sz, "smni2");
99+
100+
if (amount == 0) {
101+
amount = sz - start;
102+
}
103+
104+
ids = new uint256[](amount);
105+
106+
for (uint256 i = 0; i < amount; i++) {
107+
ids[i] = m.tokenByOwner(addr, i + start);
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)