Skip to content
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

Builder #157

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions src/core/Builder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

//import {console} from "forge-std/Test.sol";
import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import {Controllable} from "./base/Controllable.sol";
import {IControllable} from "../interfaces/IControllable.sol";
import {IBuilder} from "../interfaces/IBuilder.sol";
import {IPriceReader} from "../interfaces/IPriceReader.sol";
import {IPlatform} from "../interfaces/IPlatform.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/// @title Decentralized platform builder
/// @author Alien Deployer (https://github.com/a17)
contract Builder is Controllable, ERC721Upgradeable, ReentrancyGuardUpgradeable, IBuilder {
using SafeERC20 for IERC20;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTANTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @notice Version of Builder contract implementation
string public constant VERSION = "1.0.0";

/// @notice Contract uses 2 decimals to represent dollar amount
uint48 public constant USD_DENOMINATOR = 1_00;

/// @notice Minimum USD amount for the first investment by the user
uint48 public constant MIN_AMOUNT_FOR_MINTING = 10_00;

/// @notice Minimum USD amount for investment by existing user
uint48 public constant MIN_AMOUNT_FOR_ADDING = 1_00;

// keccak256(abi.encode(uint256(keccak256("erc7201:stability.Builder")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant BUILDER_STORAGE_LOCATION =
0xb43e9b39f8b177b6f23ef5d977f7f3bce46e298c7c0f146b78fa5d5641b83f00;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @custom:storage-location erc7201:stability.Builder
struct BuilderStorage {
uint newTokenId;
mapping(address owner => uint tokenId) ownedToken;
mapping(uint tokenId => TokenData) tokenData;
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INITIALIZATION */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

function initialize(address platform_) external initializer {
__Controllable_init(platform_);
__ReentrancyGuard_init();
__ERC721_init("Stability Builder", "BUILDER");
BuilderStorage storage $ = _getStorage();
$.newTokenId = 1;
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* WRITE FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @inheritdoc IBuilder
function invest(address asset, uint amount) external nonReentrant returns (uint48 amountUSD) {
IPriceReader priceReader = IPriceReader(IPlatform(platform()).priceReader());
(uint price, bool trusted) = priceReader.getPrice(asset);
if (!trusted) {
revert NotTrustedPriceFor(asset);

Check warning on line 73 in src/core/Builder.sol

View check run for this annotation

Codecov / codecov/patch

src/core/Builder.sol#L73

Added line #L73 was not covered by tests
}
amountUSD = uint48(amount * price / 10 ** IERC20Metadata(asset).decimals() * USD_DENOMINATOR / 1e18);
if (amountUSD == 0) {
revert ZeroAmountToInvest();

Check warning on line 77 in src/core/Builder.sol

View check run for this annotation

Codecov / codecov/patch

src/core/Builder.sol#L77

Added line #L77 was not covered by tests
}

BuilderStorage storage $ = _getStorage();
uint tokenId = $.ownedToken[msg.sender];

if (tokenId == 0) {
if (amountUSD < MIN_AMOUNT_FOR_MINTING) {
revert TooLittleAmountToInvest(amountUSD, MIN_AMOUNT_FOR_MINTING);

Check warning on line 85 in src/core/Builder.sol

View check run for this annotation

Codecov / codecov/patch

src/core/Builder.sol#L85

Added line #L85 was not covered by tests
}
tokenId = $.newTokenId;
$.newTokenId = tokenId + 1;
$.ownedToken[msg.sender] = tokenId;
_mint(msg.sender, tokenId);
} else {
if (amountUSD < MIN_AMOUNT_FOR_ADDING) {
revert TooLittleAmountToInvest(amountUSD, MIN_AMOUNT_FOR_ADDING);

Check warning on line 93 in src/core/Builder.sol

View check run for this annotation

Codecov / codecov/patch

src/core/Builder.sol#L93

Added line #L93 was not covered by tests
}
}

TokenData storage tokenData = $.tokenData[tokenId];
tokenData.invested += amountUSD;

IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);

emit Invested(msg.sender, tokenId, asset, amount, amountUSD);
}
Comment on lines +69 to +103

Check warning

Code scanning / Slither

Divide before multiply Medium


/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* VIEW FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @inheritdoc Controllable
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(ERC721Upgradeable, Controllable)
returns (bool)
{
return interfaceId == type(IControllable).interfaceId || super.supportsInterface(interfaceId);
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL LOGIC */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

function _getStorage() private pure returns (BuilderStorage storage $) {
//slither-disable-next-line assembly
assembly {
$.slot := BUILDER_STORAGE_LOCATION
}
}
}
23 changes: 23 additions & 0 deletions src/interfaces/IBuilder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

interface IBuilder {
event Invested(address indexed user, uint indexed tokenId, address indexed asset, uint amount, uint48 amountUSD);

error NotTrustedPriceFor(address asset);
error ZeroAmountToInvest();
error TooLittleAmountToInvest(uint amountUSD, uint minimum);

struct TokenData {
uint8 role;
uint48 invested;
uint48 bountyPending;
uint48 bountyGot;
}

/// @notice Invest money in development
/// @param asset Allowed asset
/// @param amount Amount of asset
/// @return amountUSD Invested USD amount with 2 decimals
function invest(address asset, uint amount) external returns (uint48 amountUSD);
}
37 changes: 37 additions & 0 deletions test/core/Builder.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Test, console, Vm} from "forge-std/Test.sol";
import {FullMockSetup} from "../base/FullMockSetup.sol";
import {Builder} from "../../src/core/Builder.sol";
import {Proxy} from "../../src/core/proxy/Proxy.sol";
import {IControllable} from "../../src/interfaces/IControllable.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";

contract BuilderTest is Test, FullMockSetup {
Builder public builder;

function setUp() public {
Proxy proxy = new Proxy();
address implementation = address(new Builder());
proxy.initProxy(implementation);
builder = Builder(address(proxy));
builder.initialize(address(platform));

tokenA.mint(25e18);
tokenA.approve(address(builder), 25e18);
}

function testInvest() public {
builder.invest(address(tokenA), 11e18);
}

function testERC165() public {
assertEq(builder.supportsInterface(type(IERC165).interfaceId), true);
assertEq(builder.supportsInterface(type(IERC721).interfaceId), true);
assertEq(builder.supportsInterface(type(IERC721Metadata).interfaceId), true);
assertEq(builder.supportsInterface(type(IControllable).interfaceId), true);
}
}
Loading