Skip to content

Commit 52aa14d

Browse files
feat: add HodlChallege
1 parent 5100b59 commit 52aa14d

File tree

6 files changed

+230
-1
lines changed

6 files changed

+230
-1
lines changed

.env.example

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALCHEMY_API_KEY=
2+
BLOCK_NUMBER=

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ yarn compile
3939
4. NftSaleChallenge.sol
4040
5. BankChallenge.sol
4141
6. WrappedERC20Challenge.sol
42+
7. HodleChallenge.sol (required to fork mainnet state)
4243

4344
To run a test type:
4445

@@ -47,6 +48,20 @@ To run a test type:
4748
yarn test test/<*Challenge>.test.ts
4849
```
4950

51+
Some of challenges are required to be run on forked network. You need access to an archive node like the free ones from [Alchemy](https://alchemyapi.io/). Create `.env` file and paste the api key.
52+
53+
Type:
54+
55+
```bash
56+
cp .env.example .env
57+
```
58+
59+
Then set the environment variable.
60+
61+
```bash
62+
ALCHEMY_API_KEY=<Your Alchemy api key>
63+
```
64+
5065
### About this challenges
5166

5267
[Slide [ja]](https://docs.google.com/presentation/d/17FKtVC1S29WFnQjq92_SiqGIS6EGuslHsUfuNv-NlXU/edit?usp=sharing)

contracts/miscs/HodlChallenge.sol

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity 0.8.10;
3+
4+
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
import "@openzeppelin/contracts/access/Ownable.sol";
6+
7+
import "./ProxyERC20.sol";
8+
9+
/// @notice Challenge Description
10+
/// HodlVault allows an account to hold a token for a specific period.
11+
/// You can complete this challenge by empaty the following HodlVault for SNX.
12+
/// @notice This challenge is required to setup forking mainnet state.
13+
/// To run this challenge, You need access to an archive node like the free ones from Alchemy :https://alchemyapi.io/.
14+
15+
contract HodlVault is ERC20 {
16+
IERC20 public immutable token;
17+
18+
address public governance;
19+
20+
uint256 public unlocktimestamp;
21+
22+
uint256 public minDelayTime = 5 * 3600 * 24 * 365; // default 5 years
23+
24+
constructor(IERC20 _token, address _governance) ERC20("Hodl", "HODL") {
25+
token = _token;
26+
governance = _governance;
27+
}
28+
29+
/// @notice deposit tokens and lock up for a certain period.
30+
function hold(uint256 amount) external {
31+
require(amount > 0, "zero");
32+
unlocktimestamp = block.timestamp + minDelayTime;
33+
34+
token.transferFrom(msg.sender, address(this), amount);
35+
_mint(msg.sender, amount);
36+
}
37+
38+
/// @notice withdraw locked tokens if tokens can be unlocked
39+
function withdraw(uint256 amount) external {
40+
require(amount > 0, "zero");
41+
require(block.timestamp > unlocktimestamp, "lock");
42+
43+
_burn(msg.sender, amount);
44+
token.transfer(msg.sender, amount);
45+
}
46+
47+
/// @notice rescue tokens accidentally send
48+
function sweep(IERC20 _token) external {
49+
// You can not rescue locked token.
50+
require(token != _token, "!token");
51+
_token.transfer(governance, _token.balanceOf(address(this)));
52+
}
53+
54+
function updateGovernance(address _governance) external {
55+
require(governance == msg.sender, "!gov");
56+
require(_governance != address(0), "zero");
57+
58+
governance = _governance;
59+
}
60+
61+
function updateMinDelayTime(uint256 _minDelayTime) external {
62+
require(governance == msg.sender, "!gov");
63+
64+
minDelayTime = _minDelayTime;
65+
}
66+
67+
function holdMethodIsCalled() external view returns (bool) {
68+
return unlocktimestamp > 0;
69+
}
70+
}
71+
72+
contract HodlChallenge {
73+
uint256 public constant SNAPSHOT_BLOCK_NUMBER = 14850000;
74+
75+
HodlVault public vault;
76+
77+
/// @notice SNX is an DeFi governance token.
78+
/// This address is Mainnet Synthetix token. it implements ProxyERC20 interface
79+
/// The source code can be found in the the following link :
80+
/// https://etherscan.io/address/0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f#code
81+
/// @dev
82+
/// Hardhat mainnet forking is available.
83+
/// await network.provider.request({
84+
/// method: 'hardhat_reset',
85+
/// params: [
86+
/// {
87+
/// forking: {
88+
/// jsonRpcUrl: <jsonRpcUrl> || hre.config.networks.hardhat.forking.url,
89+
/// blockNumber: <blockNumber>,
90+
/// },
91+
/// },
92+
/// ],
93+
/// })
94+
/// To get some SNX hardhat_impersonateAccount is useful.
95+
/// impersonate a SNX whale address such as 0xF977814e90dA44bFA03b6295A0616a897441aceC (Binance 8)
96+
/// await network.provider.request({
97+
/// method: 'hardhat_impersonateAccount',
98+
/// params: [address],
99+
/// })
100+
/// const signer = await ethers.getSigner(address)
101+
IERC20 public constant SNX =
102+
ProxyERC20(0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F);
103+
104+
constructor() {
105+
// deploy HodlVault for SNX
106+
vault = new HodlVault(SNX, address(this));
107+
}
108+
109+
function isSolved() public view returns (bool) {
110+
require(vault.holdMethodIsCalled(), "rule: lock SNX at least once");
111+
return SNX.balanceOf(address(vault)) == 0;
112+
}
113+
}

contracts/miscs/ProxyERC20.sol

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity ^0.8.10;
3+
4+
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
5+
6+
/// @title Synthetix-like proxy interface
7+
/// ref: SNX https://etherscan.io/address/0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f#code
8+
9+
interface Proxyable is IERC20Metadata {
10+
function proxy() external view returns (Proxy);
11+
12+
function setProxy(address payable _proxy) external;
13+
14+
function setMessageSender(address sender) external;
15+
}
16+
17+
/**
18+
* @notice
19+
* A proxy contract that, if it does not recognise the function
20+
* being called on it, passes all value and call data to an
21+
* underlying target contract.
22+
*
23+
* This proxy has the capacity to toggle between DELEGATECALL
24+
* and CALL style proxy functionality.
25+
*
26+
* The former executes in the proxy's context, and so will preserve
27+
* msg.sender and store data at the proxy address. The latter will not.
28+
* Therefore, any contract the proxy wraps in the CALL style must
29+
* implement the Proxyable interface, in order that it can pass msg.sender
30+
* into the underlying contract as the state parameter, messageSender.
31+
**/
32+
interface Proxy {
33+
function target() external view returns (Proxyable);
34+
35+
function useDELEGATECALL() external view returns (bool);
36+
37+
function setTarget(Proxyable _target) external;
38+
39+
function setUseDELEGATECALL(bool value) external;
40+
}
41+
42+
/**
43+
* @notice Synthetix-like proxy Implementaion with explicit ERC20 standard
44+
**/
45+
interface ProxyERC20 is Proxy, IERC20Metadata {} // prettier-ignore

contracts/tokens/NftSaleChallenge.sol

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ pragma solidity 0.8.10;
44
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
55

66
/// @notice Challenge Description
7-
/// You can mint awesome nfts. but the nft contract limits the number of nft you can buy at a time. Mint at least 100 nfts within one transaction.
7+
/// You can mint awesome nfts. The nft contract seems to limits the number of nft accounts can buy at a time.
8+
/// Mint at least 100 nfts within one transaction.
89
interface INft {
910
function mint(uint256 numberOfNfts) external payable;
1011

test/HodlChallenge.test.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { ethers } from 'hardhat'
2+
import { expect } from 'chai'
3+
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
4+
import { ProxyERC20, HodlVault } from '../typechain-types'
5+
import { getImpersonatedSigner, resetFork } from './helpers/utils'
6+
7+
/// @note provider url is required to run this challenge
8+
const PROVIDER_URL = `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_API_KEY}`
9+
10+
const BLOCK_NUMBER = 14850000
11+
const SNX = '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F'
12+
const SNX_WHALE = '0xF977814e90dA44bFA03b6295A0616a897441aceC' // binance8
13+
14+
describe('HodlChallenge', async function () {
15+
let player: SignerWithAddress
16+
let snx: ProxyERC20
17+
let challenge
18+
before(async function () {
19+
await resetFork(BLOCK_NUMBER, PROVIDER_URL) // fork state at BLOCK_NUMBER
20+
player = (await getImpersonatedSigner(SNX_WHALE)) as any
21+
22+
await ethers.provider.send('hardhat_setBalance', [
23+
player.address,
24+
'0xffffffffffffffffffffff',
25+
])
26+
27+
snx = (await ethers.getContractAt('ProxyERC20', SNX)) as ProxyERC20
28+
challenge = await (
29+
await ethers.getContractFactory('HodlChallenge')
30+
).deploy()
31+
32+
expect(await snx.name()).to.be.not.equal('') // check
33+
})
34+
after(async function () {
35+
await resetFork(BLOCK_NUMBER, PROVIDER_URL)
36+
})
37+
it('Attack', async function () {
38+
const vault = (await ethers.getContractFactory('HodlVault')).attach(
39+
await challenge.vault()
40+
) as HodlVault
41+
42+
// deposit SNX at least once
43+
const amotToDeposit = await snx.balanceOf(SNX_WHALE)
44+
await snx.connect(player).approve(vault.address, amotToDeposit)
45+
await vault.connect(player).hold(amotToDeposit)
46+
expect(await vault.holdMethodIsCalled()).to.be.true // should be true
47+
48+
// TODO: Your solution below
49+
50+
// check
51+
expect(await challenge.isSolved()).to.be.true
52+
})
53+
})

0 commit comments

Comments
 (0)