Квесты Base
Введение
Base запустили обучающую кампанию Base Camp. Необходимо проходить обучение по Solidity, создавать смарт-контракты и получать NFT. За эти NFT можно забрать роли в Guild.
Есть шанс, что эти NFT и роли могут стать мультипликатором для дропа.
Не надо переживать, если вы никогда не пробовали программировать или деплоить смарт-контракты. Мы все сделали сами и вам осталось только разобраться в базовых моментах и задеплоить свои контракты, используя код из гайда.
Вся активность происходит в тестовой сети Base Sepolia, так что никаких затрат и опасности тут нет.
Всего надо выполнить 13 тасков. Ссылки на все таски тут👇
https://docs.base.org/base-camp/docs/exercise-contracts
Подготовка и необходимые ресурсы
👉🏽 https://chainlist.org/chain/84532
👉🏽 https://www.alchemy.com/faucets/base-sepolia
Или забриджить из сети ETH Sepolia через мост. Достаточно 0.2 ETH
👉🏽 https://sepolia-bridge.base.org/deposit
3. Заходим на страницу с бэйджами. Здесь будем отслеживать прогресс. По мере получения NFT, они будут отражаться синим цветом.
https://docs.base.org/base-camp/progress/
4. Заходим в дискорд Base, если еще не присоединились.
👉🏽 https://discord.com/invite/buildonbase
5. После получения всех NFT заходим в гильдию и забираем роли. В дискорде у вас будут отображаться новые роли
👉🏽 https://guild.xyz/base/base-camp
Remix
Remix - онлайн сервис, через который все желающие могут деплоить контракты. Ничего устанавливать не надо. Просто заходите на сайт и подготовим все что нужно по шагам.
Создаем свое рабочее пространство
Template - Basic Workspace name - любое (в примере будет “BASE”) OK
Видим нашу папку и удаляем все кроме 1_Storage.sol. На фото все, что выделено красным - удаляем:
Чтобы создать новый файл нажимаем кнопку как на фото и пишем имя файла и жмем Enter
Пишите имя файла с названием квеста, чтобы не путаться потом
Для каждого таска надо создавать новый файл
- Нажимаем на имя файла 1_Storage.sol и справа увидим код контракта. В этом окне можно писать и менять код.
- Именно сюда необходимо будет вставлять код, который мы собрали в гайде
Заходим на вкладку Solidity compiler и выставляем настройки как на фото.
Это сделаем 1 раз и больше не трогаем
Заходим на вкладку Deploy & Run Transactions. На этой вкладке мы будем деплоить все наши контракты.
Каждый раз, после вставки кода в контракт, идем на эту вкладку, здесь нужно проверять, чтобы в полях было указано как на фото:
1. Вкладка Deploy & Run Transactions
2. ENVIRONMENT - Выбираем среду для разворачивания контракта. Ставим Injected Provider - это наш Метамаск. Он сразу откроется и попросит подключиться. Подключаем его.
3. ACCOUNT - Проверяем, чтоб отображался наш адрес Метамаска
4. CONTRACT - В этом окне выбирается контракт, который будем деплоить. Сейчас это 1_Storage
5. DEPLOY - кнопка, которую нажимаем, чтобы задеплоить контракт
‼️ Очень важно следить, что в кошелке у вас именно сеть BASE SEPOLIA. Потому что, Remix’у все равно в какой сети деплоить и если у вас выбрана сеть ETHEREUM, то будет деплой в ней и заплатите баксов 40 комиссии!
- Если все сделали правильно, то ниже появится такая вкладка нашего контракта, где мы можем скопировать адрес нашего контракта. Именно этот адрес мы будем вставлять на сайте Base для проверки и получения NFT
Как работать с гайдом дальше?
‼️ Теперь у нас есть все, чтобы разворачивать контракты и забрать NFT и роли:
- Создаете новый файл
- Вставляете текст контракта из гайда
- Деплоите контракт
- Копируете адрес вашего контракта
- Вставляете адрес контракта по указанной ссылке и подписываете тестовую транзакцию
- ЗДЕСЬ проверяете, что получили бэйдж
Далее для ускорения процесса для каждого бэйджа будет указано только 2 пункта: текст контракта, ссылка на верификацию контракта.
В некоторых задания нужны будут дополнительные действия, все будет указано.
Ёще один очень важный момент! Если все будут тупо копировать и деплоить один и тот же контракт, то байткод контрактов будет одинаковый и это может быть палевом для проекта!
Поэтому в каждом контракте есть одна строчка, в которой вам надо написать любые символы, чтобы байт код каждого юзера отличался. Выглядит это так:
string private salt = "ваша строка тут";
То, что выделено красным меняете на любую абракадабру (Латинские буквы и цифры) в каждом контракте и все. Remix все равно не даст вам деплоить с русским буквами, так что вы не забудете это.
Деплоим контракты
1. Deployment Exercise
// SPDX-License-Identifier: MIT
string private salt = "ваша строка тут";
function adder(uint _a, uint _b) external pure returns (uint sum, bool error) {
if (result >= _a && result >= _b) {
function subtractor(uint _a, uint _b) external pure returns (uint difference, bool error) {
2. Control Structures Exercise
// SPDX-License-Identifier: MIT
error AfterHours(uint256 _time);
string private salt = "ваша строка тут";
function fizzBuzz(uint256 _number) public pure returns (string memory) {
if (_number % 3 == 0 && _number % 5 == 0) {
} else if (_number % 3 == 0) {
} else if (_number % 5 == 0) {
function doNotDisturb(uint256 _time) public pure returns (string memory) {
} else if (_time >= 2200 || _time < 800) {
} else if (_time >= 1200 && _time <= 1259) {
} else if (_time >= 800 && _time <= 1199) {
} else if (_time >= 1300 && _time <= 1799) {
- Link
- https://docs.base.org/base-camp/docs/control-structures/control-structures-exercise/3. Storage Exercise
// SPDX-License-Identifier: MIT
error TooManyShares(uint16 totalShares);
function viewSalary() public view returns (uint24) {
function viewShares() public view returns (uint16) {
string memory salt = "ваша строка тут";
function grantShares(uint16 _newShares) external {
uint16 totalShares = shares + _newShares;
require(_newShares < 5000, "Too many shares");
revert TooManyShares(totalShares);
* Do not modify this function. It is used to enable the unit test for this pin
* to check whether or not you have configured your storage variables to make
* If you wish to cheat, simply modify this function to always return `0`
* I'm not your boss ¯\\_(ツ)_/¯
* Fair warning though, if you do cheat, it will be on the blockchain having been
* deployed by your wallet....FOREVER!
function checkForPacking(uint _slot) public view returns (uint r) {
* Warning: Anyone can use this function at any time!
function debugResetShares() public {
В этом контракте надо найти и изменить строчку
string memory salt = "ваша строка тут";
То что выделено красным меняем на любые символы-цифры. 3. Идем на вкладку Deploy(1) и заполняем поля как на скриншоте (2), затем жмем transact (3).
Подписываем транзакцию и контракт задеплоен. Дальше по обычной схеме.
https://docs.base.org/base-camp/docs/storage/storage-exercise/
// SPDX-License-Identifier: MIT
**string private salt = "ваша строка тут";**
uint[] public numbers = [1,2,3,4,5,6,7,8,9,10];
function getNumbers() public view returns (uint[] memory) {
function resetNumbers() public {
numbers = [1,2,3,4,5,6,7,8,9,10];
function appendToNumbers(uint[] calldata _toAppend) public {
uint numbersLength = numbers.length;
uint _toAppendLength = _toAppend.length;
uint newLength = numbersLength + _toAppendLength;
uint[] memory newNumbers = new uint[](newLength);
for (uint i = 0; i < numbersLength; i++) {
for (uint j = 0; j < _toAppendLength; j++) {
newNumbers[numbersLength + j] = _toAppend[j];
function saveTimestamp(uint _unixTimestamp) public {
timestamps.push(_unixTimestamp);
function afterY2K() public view returns (uint[] memory, address[] memory) {
uint[] memory recentTimestamps = new uint[](timestamps.length);
address[] memory correspondingSenders = new address[](timestamps.length);
uint y2kTimestamp = 946702800;
for (uint i = 0; i < timestamps.length; i++) {
if (timestamps[i] > y2kTimestamp) {
recentTimestamps[count] = timestamps[i];
correspondingSenders[count] = senders[i];
// Resize arrays to fit only the actual values
uint[] memory trimmedRecentTimestamps = new uint[](count);
address[] memory trimmedCorrespondingSenders = new address[](count);
for (uint j = 0; j < count; j++) {
trimmedRecentTimestamps[j] = recentTimestamps[j];
trimmedCorrespondingSenders[j] = correspondingSenders[j];
return (trimmedRecentTimestamps, trimmedCorrespondingSenders);
function resetSenders() public {
function resetTimestamps() public {
- Link
- https://docs.base.org/base-camp/docs/arrays/arrays-exercise/5. Mappings Exercise
// SPDX-License-Identifier: MIT
string private salt = "ваша строка тут";
mapping(string => bool) public approvedRecords;
mapping(address => mapping(string => bool)) public userFavorites;
approvedRecords["Thriller"] = true;
approvedRecords["Back in Black"] = true;
approvedRecords["The Bodyguard"] = true;
approvedRecords["The Dark Side of the Moon"] = true;
approvedRecords["Their Greatest Hits (1971-1975)"] = true;
approvedRecords["Hotel California"] = true;
approvedRecords["Come On Over"] = true;
approvedRecords["Rumours"] = true;
approvedRecords["Saturday Night Fever"] = true;
function getApprovedRecords() public view returns (string[] memory) {
string[] memory records = new string[](9);
for (uint i = 0; i < 9; i++) {
records[i] = getApprovedRecordAtIndex(i);
function getApprovedRecordAtIndex(uint index) private view returns (string memory) {
require(index < 9, "Index out of range");
if (index == 0) return "Thriller";
if (index == 1) return "Back in Black";
if (index == 2) return "The Bodyguard";
if (index == 3) return "The Dark Side of the Moon";
if (index == 4) return "Their Greatest Hits (1971-1975)";
if (index == 5) return "Hotel California";
if (index == 6) return "Come On Over";
if (index == 7) return "Rumours";
if (index == 8) return "Saturday Night Fever";
function addRecord(string memory _albumName) public {
require(approvedRecords[_albumName], string(abi.encodePacked(_albumName, " is not an approved album")));
userFavorites[msg.sender][_albumName] = true;
function getUserFavorites(address _user) public view returns (string[] memory) {
string[] memory favorites = new string[](9);
for (uint i = 0; i < 9; i++) {
if (userFavorites[_user][getApprovedRecordAtIndex(i)]) {
favorites[index] = getApprovedRecordAtIndex(i);
string[] memory trimmedFavorites = new string[](index);
for (uint j = 0; j < index; j++) {
trimmedFavorites[j] = favorites[j];
function resetUserFavorites() public {
for (uint i = 0; i < 9; i++) {
userFavorites[msg.sender][getApprovedRecordAtIndex(i)] = false;
6. Structs ExerciseContract:
// SPDX-License-Identifier: MIT
string private salt = "ваша строка тут";
mapping(address => Car[]) public garage;
event CarAdded(address indexed owner, string make, string model, string color, uint numberOfDoors);
event CarUpdated(address indexed owner, uint indexed index, string make, string model, string color, uint numberOfDoors);
event GarageReset(address indexed owner);
function addCar(string memory _make, string memory _model, string memory _color, uint _numberOfDoors) public {
Car memory newCar = Car(_make, _model, _color, _numberOfDoors);
garage[msg.sender].push(newCar);
emit CarAdded(msg.sender, _make, _model, _color, _numberOfDoors);
function getMyCars() public view returns (Car[] memory) {
function getUserCars(address _user) public view returns (Car[] memory) {
function updateCar(uint _index, string memory _make, string memory _model, string memory _color, uint _numberOfDoors) public {
require(_index < garage[msg.sender].length, "BadCarIndex");
garage[msg.sender][_index] = Car(_make, _model, _color, _numberOfDoors);
emit CarUpdated(msg.sender, _index, _make, _model, _color, _numberOfDoors);
function resetMyGarage() public {
- Link
- https://docs.base.org/base-camp/docs/structs/structs-exercise/
7. Inheritance Exercise
Сначала создаем контракт но не деплоим.
// SPDX-License-Identifier: MIT
string private salt = "ваша строка тут";
constructor(uint _idNumber, uint _managerId) {
function getAnnualCost() public view virtual returns (uint);
contract Salaried is Employee {
constructor(uint _idNumber, uint _managerId, uint _annualSalary) Employee(_idNumber, _managerId) {
function getAnnualCost() public view override returns (uint) {
constructor(uint _idNumber, uint _managerId, uint _hourlyRate) Employee(_idNumber, _managerId) {
function getAnnualCost() public view override returns (uint) {
function addReport(uint _idNumber) public {
function resetReports() public {
contract Salesperson is Hourly {
constructor(uint _idNumber, uint _managerId, uint _hourlyRate) Hourly(_idNumber, _managerId, _hourlyRate) {}
contract EngineeringManager is Salaried, Manager {
constructor(uint _idNumber, uint _managerId, uint _annualSalary) Salaried(_idNumber, _managerId, _annualSalary) {}
Заходим на вкладку деплоя, выбираем в выпадающем списке Salesperson и заполняем 3 поля, как на фото:
Жмем Transact, подписываем транзу
Копируем адрес полученного контракта себе в блокнот
Теперь выбираем EngineeringManager и заполняем 3 поля, как на фото:
Жмем Transact, подписываем транзу
Копируем адрес контракта в блокнот
Конец близко, но нужен еще 1 контракт, последний в этом таске квесте 😁
Создаем контракт (не забываем вставлять свою строку)
// SPDX-License-Identifier: MIT
contract InheritanceSubmission {
string private salt = "ваша строка тут";
address public engineeringManager;
constructor(address _salesPerson, address _engineeringManager) {
engineeringManager = _engineeringManager;
Заходим на вкладку деплоя, и там вписываем сохраненные адреса контрактов с предыдущих шагов, соответствующие названиям.
Жмем Transact, подписываем транзу
Готово, можно субмитить контракт. Копируем адрес этого последнего контракта и идем, как обычно по ссылке
Создаем контракт именно с таким именем 👇
Вставляем этот код 👇 и ничего в нем не меняем
// SPDX-License-Identifier: MIT
function shruggie(string memory _input) internal pure returns (string memory) {
return string.concat(_input, unicode" 🤷");
Затем создаем второй с любым именем в этой же папке и вставляем этот код 👇 и меняем нашу уникальную сроку
// SPDX-License-Identifier: MIT
import "./SillyStringUtils.sol";
**string private salt = "ваша строка тут";**
SillyStringUtils.Haiku public haiku;
function saveHaiku(string calldata line1, string calldata line2, string calldata line3) public payable {
haiku = SillyStringUtils.Haiku({
function getHaiku() public view returns(SillyStringUtils.Haiku memory){
function shruggieHaiku() public view returns(SillyStringUtils.Haiku memory){
SillyStringUtils.Haiku memory _haiku = SillyStringUtils.Haiku({
line3: SillyStringUtils.shruggie(haiku.line3)
Дальше как обычно деплоим и вставляем адрес контракта на сайте для теста
// SPDX-License-Identifier: MIT
contract ErrorTriageExercise {
string private salt = "ваша строка тут";
// Correctly calculates the absolute difference between neighboring arguments
) public pure returns (uint[] memory) {
uint[] memory results = new uint[](3);
results[0] = _a > _b ? _a - _b : _b - _a;
results[1] = _b > _c ? _b - _c : _c - _b;
results[2] = _c > _d ? _c - _d : _d - _c;
// Safely applies a modifier to the base value
) public pure returns (uint) {
require(_base >= uint(-_modifier), "Result would underflow");
return _base - uint(-_modifier);
return _base + uint(_modifier);
// Correctly pops the last element from the array and returns it
function popWithReturn() public returns (uint) {
require(arr.length > 0, "Array is empty");
arr.pop(); // This correctly reduces the array length by 1
// The utility functions below are working as expected
function addToArr(uint _num) public {
function getArr() public view returns (uint[] memory) {
- Link
- https://docs.base.org/base-camp/docs/error-triage/error-triage-exercise/10. New Exercise
- Создаем контракт с таким именем
// SPDX-License-Identifier: MIT
import "@openzeppelin/contracts/access/Ownable.sol";
contract AddressBook is Ownable(msg.sender) {
string private salt = "ваша строка тут";
mapping(uint => uint) private idToIndex;
error ContactNotFound(uint id);
function addContact(string calldata firstName, string calldata lastName, uint[] calldata phoneNumbers) external onlyOwner {
contacts.push(Contact(nextId, firstName, lastName, phoneNumbers));
idToIndex[nextId] = contacts.length - 1;
function deleteContact(uint id) external onlyOwner {
if (index >= contacts.length || contacts[index].id != id) revert ContactNotFound(id);
contacts[index] = contacts[contacts.length - 1];
idToIndex[contacts[index].id] = index;
function getContact(uint id) external view returns (Contact memory) {
if (index >= contacts.length || contacts[index].id != id) revert ContactNotFound(id);
function getAllContacts() external view returns (Contact[] memory) {
Сразу создаем второй контракт, вставляем код и меняем нашу уникальную сроку
// SPDX-License-Identifier: MIT
string private salt = "ваша строка тут";
function deploy() external returns (AddressBook) {
AddressBook newAddressBook = new AddressBook();
newAddressBook.transferOwnership(msg.sender);
Деплоим именно второй контракт, первый деплоить не нужно. Все, можем проходить тест и забирать NFT
// SPDX-License-Identifier: MIT
string private salt = "ваша строка тут";
mapping(address => uint256) public balances;
mapping(address => bool) private claimed;
error UnsafeTransfer(address _to);
totalSupply = 100000000; // Set the total supply of tokens
// Public function to claim tokens
if (totalClaimed >= totalSupply) revert AllTokensClaimed(); // Check if all tokens have been claimed
if (claimed[msg.sender]) revert TokensClaimed(); // Check if the caller has already claimed tokens
// Update balances and claimed status
// Public function for safe token transfer
function safeTransfer(address _to, uint256 _amount) public {
// Check for unsafe transfer conditions, including if the target address has a non-zero ether balance
if (_to == address(0) || _to.balance == 0) revert UnsafeTransfer(_to);
// Ensure the sender has enough balance to transfer
require(balances[msg.sender] >= _amount, "Insufficient balance");
balances[msg.sender] -= _amount;
- Link
https://docs.base.org/base-camp/docs/minimal-tokens/minimal-tokens-exercise/12. ERC-20 Tokens Exercise
// SPDX-License-Identifier: UNLICENSED
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
contract WeightedVoting is ERC20 {
string private salt = "ваша строка тут";
using EnumerableSet for EnumerableSet.AddressSet;
EnumerableSet.AddressSet voters;
mapping(address => bool) public tokensClaimed;
uint256 public maxSupply = 1000000;
uint256 public claimAmount = 100;
string salt = "Random String";
constructor(string memory _name, string memory _symbol)
if (totalSupply() + claimAmount > maxSupply) {
if (tokensClaimed[msg.sender]) {
_mint(msg.sender, claimAmount);
tokensClaimed[msg.sender] = true;
function createIssue(string calldata _issueDesc, uint256 _quorum)
if (balanceOf(msg.sender) == 0) {
if (_quorum > totalSupply()) {
Issue storage _issue = issues.push();
_issue.issueDesc = _issueDesc;
function getIssue(uint256 _issueId)
returns (SerializedIssue memory)
Issue storage _issue = issues[_issueId];
voters: _issue.voters.values(),
totalVotes: _issue.totalVotes,
votesAgainst: _issue.votesAgainst,
votesAbstain: _issue.votesAbstain,
function vote(uint256 _issueId, Vote _vote) public {
Issue storage _issue = issues[_issueId];
if (_issue.voters.contains(msg.sender)) {
uint256 nTokens = balanceOf(msg.sender);
_issue.votesAgainst += nTokens;
} else if (_vote == Vote.FOR) {
_issue.votesAbstain += nTokens;
_issue.voters.add(msg.sender);
if (_issue.totalVotes >= _issue.quorum) {
if (_issue.votesFor > _issue.votesAgainst) {
Чтобы задеплоить этот контракт на странице Deploy (1) жмем галочку (2) и в окне (3) и (4) пишем любые символы, затем жмем transact (5). Готово!
// SPDX-License-Identifier: MIT
import "<https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol>";
// function ownerOf(uint256 _id) external view returns (address);
function counter() external view returns (uint256);
function shareHaiku(uint256 _id, address _to) external;
function getMySharedHaikus() external view returns (Haiku[] memory);
contract HaikuNFT is ERC721, ISubmission {
mapping(address => mapping(uint256 => bool)) public sharedHaikus;
constructor() ERC721("HaikuNFT", "HAIKU") {
string salt = "**ваша строка тут**";
function counter() external view override returns (uint256) {
// Check if the haiku is unique
string[3] memory haikusStrings = [_line1, _line2, _line3];
for (uint256 li = 0; li < haikusStrings.length; li++) {
string memory newLine = haikusStrings[li];
// string memory newHaikuString = string(
// abi.encodePacked(haikusStrings[li])
for (uint256 i = 0; i < haikus.length; i++) {
Haiku memory existingHaiku = haikus[i];
string[3] memory existingHaikuStrings = [
for (uint256 eHsi = 0; eHsi < 3; eHsi++) {
string memory existingHaikuString = existingHaikuStrings[
keccak256(abi.encodePacked(existingHaikuString)) ==
keccak256(abi.encodePacked(newLine))
_safeMint(msg.sender, haikuCounter);
haikus.push(Haiku(msg.sender, _line1, _line2, _line3));
function shareHaiku(uint256 _id, address _to) external override {
require(_id > 0 && _id <= haikuCounter, "Invalid haiku ID");
Haiku memory haikuToShare = haikus[_id - 1];
require(haikuToShare.author == msg.sender, "NotYourHaiku");
sharedHaikus[_to][_id] = true;
for (uint256 i = 0; i < haikus.length; i++) {
if (sharedHaikus[msg.sender][i + 1]) {
Haiku[] memory result = new Haiku[](sharedHaikuCount);
for (uint256 i = 0; i < haikus.length; i++) {
if (sharedHaikus[msg.sender][i + 1]) {
result[currentIndex] = haikus[i];
Поздравляю, вы получили 13 NFT + 5 ролей в Дискорде, и стали чуть-чуть программистом на Solidity 🤓