diff --git a/core/.solhint.json b/core/.solhint.json index b53dba35b..f54af0b9a 100644 --- a/core/.solhint.json +++ b/core/.solhint.json @@ -1,7 +1,4 @@ { "extends": "thesis", - "plugins": [], - "rules": { - "func-visibility": ["off", { "ignoreConstructors": true }] - } + "plugins": [] } diff --git a/core/contracts/Acre.sol b/core/contracts/Acre.sol index fda7ab237..bb8f357e9 100644 --- a/core/contracts/Acre.sol +++ b/core/contracts/Acre.sol @@ -3,14 +3,21 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; +/// @title Acre +/// @notice Implementation of the ERR-4626 tokenized vault standard. ERC-4626 is +/// a standard to optimize and unify the technical parameters of +/// yield-bearing vaults. This contract allows the minting and burning +/// of shares, represented as standard ERC20 token, in exchange for tBTC +/// tokens. +/// @dev ERC-4626 standard extends the ERC-20 token. contract Acre is ERC4626 { event Staked(bytes32 indexed referral, uint256 assets, uint256 shares); constructor( - IERC20 _token - ) ERC4626(_token) ERC20("Acre Staked Bitcoin", "stBTC") {} + IERC20 tbtc + ) ERC4626(tbtc) ERC20("Acre Staked Bitcoin", "stBTC") {} - /// @notice Stakes a given amount of underlying token and mints shares to a + /// @notice Stakes a given amount of tBTC token and mints shares to a /// receiver. /// @dev This function calls `deposit` function from `ERC4626` contract. /// @param assets Approved amount for the transfer and stake. @@ -21,8 +28,9 @@ contract Acre is ERC4626 { uint256 assets, address receiver, bytes32 referral - ) public returns (uint256 shares) { - shares = deposit(assets, receiver); + ) public returns (uint256) { + // TODO: revisit the type of referral. + uint256 shares = deposit(assets, receiver); emit Staked(referral, assets, shares); diff --git a/core/contracts/test/TestToken.sol b/core/contracts/test/TestToken.sol index 5a83eabd8..44e5e14dc 100644 --- a/core/contracts/test/TestToken.sol +++ b/core/contracts/test/TestToken.sol @@ -2,30 +2,11 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@thesis/solidity-contracts/contracts/token/IApproveAndCall.sol"; -import "@thesis/solidity-contracts/contracts/token/IReceiveApproval.sol"; -contract TestToken is ERC20, IApproveAndCall { +contract TestERC20 is ERC20 { constructor() ERC20("Test Token", "TEST") {} function mint(address account, uint256 value) external { _mint(account, value); } - - function approveAndCall( - address spender, - uint256 amount, - bytes memory extraData - ) external returns (bool) { - if (approve(spender, amount)) { - IReceiveApproval(spender).receiveApproval( - msg.sender, - amount, - address(this), - extraData - ); - return true; - } - return false; - } } diff --git a/core/package.json b/core/package.json index 783907808..603cb8643 100644 --- a/core/package.json +++ b/core/package.json @@ -57,7 +57,6 @@ "typescript": "^5.3.2" }, "dependencies": { - "@openzeppelin/contracts": "^5.0.0", - "@thesis/solidity-contracts": "github:thesis/solidity-contracts#c315b9d" + "@openzeppelin/contracts": "^5.0.0" } } diff --git a/core/test/Acre.test.ts b/core/test/Acre.test.ts index 1a74af049..369f80704 100644 --- a/core/test/Acre.test.ts +++ b/core/test/Acre.test.ts @@ -6,105 +6,198 @@ import { import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { ethers } from "hardhat" import { expect } from "chai" -import { WeiPerEther } from "ethers" -import type { TestToken, Acre } from "../typechain" +import { ContractTransactionResponse, ZeroAddress } from "ethers" +import type { TestERC20, Acre } from "../typechain" +import { to1e18 } from "./utils" async function acreFixture() { - const [_, staker] = await ethers.getSigners() - const Token = await ethers.getContractFactory("TestToken") + const [staker, staker2] = await ethers.getSigners() + const Token = await ethers.getContractFactory("TestERC20") const tbtc = await Token.deploy() - const amountToMint = WeiPerEther * 100000n + const amountToMint = to1e18(100000) tbtc.mint(staker, amountToMint) + tbtc.mint(staker2, amountToMint) const Acre = await ethers.getContractFactory("Acre") const acre = await Acre.deploy(await tbtc.getAddress()) - return { acre, tbtc, staker } + return { acre, tbtc, staker, staker2 } } describe("Acre", () => { let acre: Acre - let tbtc: TestToken + let tbtc: TestERC20 let staker: HardhatEthersSigner + let staker2: HardhatEthersSigner before(async () => { - ;({ acre, tbtc, staker } = await loadFixture(acreFixture)) + ;({ acre, tbtc, staker, staker2 } = await loadFixture(acreFixture)) }) describe("Staking", () => { const referral = ethers.encodeBytes32String("referral") let snapshot: SnapshotRestorer - context("when staking via Acre contract", () => { - const amountToStake = 1000n * WeiPerEther - + context("when staking", () => { beforeEach(async () => { snapshot = await takeSnapshot() - - await tbtc - .connect(staker) - .approve(await acre.getAddress(), amountToStake) }) afterEach(async () => { await snapshot.restore() }) - it("should stake tokens and receive shares", async () => { - const stakerAddress = staker.address - const balanceOfBeforeStake = await tbtc.balanceOf(stakerAddress) - - const tx = await acre - .connect(staker) - .stake(amountToStake, stakerAddress, referral) - - const stakedTokens = amountToStake + context("with a referral", () => { + const amountToStake = to1e18(1000) // In this test case there is only one staker and // the token vault has not earned anythig yet so received shares are // equal to staked tokens amount. - const receivedShares = amountToStake - - expect(tx).to.emit(acre, "Deposit").withArgs( - // Caller. - stakerAddress, - // Receiver. - stakerAddress, - // Staked tokens. - stakedTokens, - // Received shares. - receivedShares, - ) - - expect(tx) - .to.emit(acre, "Staked") - .withArgs(referral, stakedTokens, receivedShares) - - expect(await acre.balanceOf(stakerAddress)).to.be.eq(amountToStake) - expect(await tbtc.balanceOf(stakerAddress)).to.be.eq( - balanceOfBeforeStake - amountToStake, - ) + const expectedReceivedShares = amountToStake + + let tx: ContractTransactionResponse + + beforeEach(async () => { + await tbtc + .connect(staker) + .approve(await acre.getAddress(), amountToStake) + + tx = await acre + .connect(staker) + .stake(amountToStake, staker.address, referral) + }) + + it("should emit Deposit event", () => { + expect(tx).to.emit(acre, "Deposit").withArgs( + // Caller. + staker.address, + // Receiver. + staker.address, + // Staked tokens. + amountToStake, + // Received shares. + expectedReceivedShares, + ) + }) + + it("should emit Staked event", () => { + expect(tx) + .to.emit(acre, "Staked") + .withArgs(referral, amountToStake, expectedReceivedShares) + }) + + it("should mint stBTC tokens", async () => { + await expect(tx).to.changeTokenBalances( + acre, + [staker.address], + [amountToStake], + ) + }) + + it("should transfer tBTC tokens", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [staker.address, acre], + [-amountToStake, amountToStake], + ) + }) }) - it("should not revert if the referral is zero value", async () => { + context("without referral", () => { const emptyReferral = ethers.encodeBytes32String("") + let tx: ContractTransactionResponse + + beforeEach(async () => { + await tbtc.connect(staker).approve(await acre.getAddress(), 1) - await expect( - acre + tx = await acre .connect(staker) - .stake(amountToStake, staker.address, emptyReferral), - ).to.be.not.reverted + .stake(1, staker.address, emptyReferral) + }) + + it("should not revert", async () => { + await expect(tx).to.be.not.reverted + }) }) - it("should revert if a staker wants to stake more tokens than approved", async () => { - await expect( - acre + context( + "when amount to stake is greater than the approved amount", + () => { + const amountToStake = to1e18(10) + beforeEach(async () => { + await tbtc + .connect(staker) + .approve(await acre.getAddress(), amountToStake) + }) + + it("should revert", async () => { + await expect( + acre + .connect(staker) + .stake(amountToStake + 1n, staker.address, referral), + ).to.be.reverted + }) + }, + ) + + context("when amount to stake is 1", () => { + const amountToStake = 1 + + beforeEach(async () => { + await tbtc .connect(staker) - .stake(amountToStake + 1n, staker.address, referral), - ).to.be.reverted + .approve(await acre.getAddress(), amountToStake) + }) + + it("should not revert", async () => { + await expect( + acre.connect(staker).stake(amountToStake, staker.address, referral), + ).to.not.be.reverted + }) + }) + + context("when the receiver is zero address", () => { + const amountToStake = to1e18(10) + + beforeEach(async () => { + await tbtc + .connect(staker) + .approve(await acre.getAddress(), amountToStake) + }) + + it("should revert", async () => { + await expect( + acre.connect(staker).stake(amountToStake, ZeroAddress, referral), + ).to.be.revertedWithCustomError(acre, "ERC20InvalidReceiver") + }) }) + + context( + "when a staker approved and staked tokens and wants to stake more but w/o another apporval", + () => { + const amountToStake = to1e18(10) + + beforeEach(async () => { + await tbtc + .connect(staker) + .approve(await acre.getAddress(), amountToStake) + + await acre + .connect(staker) + .stake(amountToStake, staker.address, referral) + }) + + it("should revert", async () => { + await expect( + acre + .connect(staker) + .stake(amountToStake, staker.address, referral), + ).to.be.revertedWithCustomError(acre, "ERC20InsufficientAllowance") + }) + }, + ) }) context("when there are two stakers, A and B ", () => { @@ -119,25 +212,24 @@ describe("Acre", () => { let afterSimulatingYieldSnapshot: SnapshotRestorer before(async () => { - const [staker1, staker2] = await ethers.getSigners() - const staker1AmountToStake = WeiPerEther * 10000n - const staker2AmountToStake = WeiPerEther * 5000n + const stakerAmountToStake = to1e18(75) + const staker2AmountToStake = to1e18(25) // Infinite approval for staking contract. await tbtc - .connect(staker1) + .connect(staker) .approve(await acre.getAddress(), ethers.MaxUint256) await tbtc .connect(staker2) .approve(await acre.getAddress(), ethers.MaxUint256) // Mint tokens. - await tbtc.connect(staker1).mint(staker1.address, staker1AmountToStake) + await tbtc.connect(staker).mint(staker.address, stakerAmountToStake) await tbtc.connect(staker2).mint(staker2.address, staker2AmountToStake) stakerA = { - signer: staker1, - address: staker1.address, - amountToStake: staker1AmountToStake, + signer: staker, + address: staker.address, + amountToStake: stakerAmountToStake, } stakerB = { signer: staker2, @@ -154,7 +246,7 @@ describe("Acre", () => { }) context("when staker A stakes tokens", () => { - it("should stake tokens", async () => { + it("should stake tokens correctly", async () => { await expect( acre .connect(stakerA.signer) @@ -205,13 +297,22 @@ describe("Acre", () => { let stakerASharesBefore: bigint let stakerBSharesBefore: bigint let vaultYield: bigint + let expectedTotalAssets: bigint + let expectedTotalSupply: bigint before(async () => { await afterStakesSnapshot.restore() stakerASharesBefore = await acre.balanceOf(stakerA.address) stakerBSharesBefore = await acre.balanceOf(stakerB.address) - vaultYield = WeiPerEther * 10000n + vaultYield = to1e18(100) + // Staker A shares = 75 + // Staker B shares = 25 + // Total assets = 75(staker A) + 25(staker B) + 100(yield) + expectedTotalAssets = + stakerA.amountToStake + stakerB.amountToStake + vaultYield + // Total shares = 75 + 25 = 100 + expectedTotalSupply = stakerA.amountToStake + stakerB.amountToStake // Simulating yield returned from strategies. The vault now contains // more tokens than deposited which causes the exchange rate to @@ -242,42 +343,93 @@ describe("Acre", () => { const shares = await acre.balanceOf(stakerA.address) const availableAssetsToRedeem = await acre.previewRedeem(shares) + // Expected amount w/o rounding: 75 * 200 / 100 = 150 + // Expected amount w/ support for rounding: 149999999999999999999 in + // tBTC token precision. + const expectedAssetsToRedeem = + (shares * (expectedTotalAssets + 1n)) / (expectedTotalSupply + 1n) + expect(availableAssetsToRedeem).to.be.greaterThan( stakerA.amountToStake, ) + expect(availableAssetsToRedeem).to.be.eq(expectedAssetsToRedeem) }) it("the staker B should be able to redeem more tokens than before", async () => { const shares = await acre.balanceOf(stakerB.address) const availableAssetsToRedeem = await acre.previewRedeem(shares) + // Expected amount w/o rounding: 25 * 200 / 100 = 50 + // Expected amount w/ support for rounding: 49999999999999999999 in + // tBTC token precision. + const expectedAssetsToRedeem = + (shares * (expectedTotalAssets + 1n)) / (expectedTotalSupply + 1n) + expect(availableAssetsToRedeem).to.be.greaterThan( stakerB.amountToStake, ) + expect(availableAssetsToRedeem).to.be.eq(expectedAssetsToRedeem) }) }) context("when staker A stakes more tokens", () => { + const newAmountToStake = to1e18(20) let sharesBefore: bigint let availableToRedeemBefore: bigint + let totalAssetsBefore: bigint + let totalSupplyBefore: bigint + let expectedSharesToMint: bigint + let expectedTotalSupply: bigint + let expectedTotalAssets: bigint before(async () => { + // Current state: + // Total assets = 75(staker A) + 25(staker B) + 100(yield) + // Total shares = 75 + 25 = 100 await afterSimulatingYieldSnapshot.restore() sharesBefore = await acre.balanceOf(stakerA.address) availableToRedeemBefore = await acre.previewRedeem(sharesBefore) + totalAssetsBefore = await acre.totalAssets() + totalSupplyBefore = await acre.totalSupply() + + tbtc.mint(stakerA.address, newAmountToStake) + + expectedSharesToMint = + (newAmountToStake * (totalSupplyBefore + 1n)) / + (totalAssetsBefore + 1n) - tbtc.mint(stakerA.address, stakerA.amountToStake) + await acre.stake(newAmountToStake, stakerA.address, referral) - await acre.stake(stakerA.amountToStake, stakerA.address, referral) + // State after stake: + // Total assets = 75(staker A) + 25(staker B) + 100(yield) + 20(staker + // A) = 220 + // Total shares = 75 + 25 + 10 = 110 + expectedTotalAssets = totalAssetsBefore + newAmountToStake + expectedTotalSupply = totalSupplyBefore + expectedSharesToMint }) it("should receive more shares", async () => { const shares = await acre.balanceOf(stakerA.address) - const availableToRedeem = await acre.previewRedeem(shares) expect(shares).to.be.greaterThan(sharesBefore) + expect(shares).to.be.eq(sharesBefore + expectedSharesToMint) + }) + + it("should be able to redeem more tokens than before", async () => { + const shares = await acre.balanceOf(stakerA.address) + const availableToRedeem = await acre.previewRedeem(shares) + + // Expected amount w/o rounding: 85 * 220 / 110 = 170 + // Expected amount w/ support for rounding: 169999999999999999999 in + // tBTC token precision. + const expectedTotalAssetsAvailableToRedeem = + (shares * (expectedTotalAssets + 1n)) / (expectedTotalSupply + 1n) + expect(availableToRedeem).to.be.greaterThan(availableToRedeemBefore) + expect(availableToRedeem).to.be.eq( + expectedTotalAssetsAvailableToRedeem, + ) }) }) }) diff --git a/core/test/utils/index.ts b/core/test/utils/index.ts new file mode 100644 index 000000000..6654cca0e --- /dev/null +++ b/core/test/utils/index.ts @@ -0,0 +1 @@ +export * from "./number" diff --git a/core/test/utils/number.ts b/core/test/utils/number.ts new file mode 100644 index 000000000..9784b499c --- /dev/null +++ b/core/test/utils/number.ts @@ -0,0 +1,10 @@ +export function to1ePrecision( + n: string | number | bigint, + precision: number, +): bigint { + return BigInt(n) * 10n ** BigInt(precision) +} + +export function to1e18(n: string | number | bigint): bigint { + return to1ePrecision(n, 18) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d7a91c33..257a3a017 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,6 @@ importers: '@openzeppelin/contracts': specifier: ^5.0.0 version: 5.0.0 - '@thesis/solidity-contracts': - specifier: github:thesis/solidity-contracts#c315b9d - version: github.com/thesis/solidity-contracts/c315b9d devDependencies: '@nomicfoundation/hardhat-chai-matchers': specifier: ^2.0.2 @@ -95,7 +92,7 @@ importers: version: 4.0.0 solhint-config-thesis: specifier: github:thesis/solhint-config - version: github.com/thesis/solhint-config/159587cd6103f21aaa7fbadc17a06717a51b5b42(solhint@4.0.0) + version: github.com/thesis/solhint-config/266de12d96d58f01171e20858b855ec80520de8d(solhint@4.0.0) solidity-coverage: specifier: ^0.8.5 version: 0.8.5(hardhat@2.19.1) @@ -172,12 +169,21 @@ importers: gatsby: specifier: ^5.12.11 version: 5.12.11(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2) + gatsby-plugin-manifest: + specifier: ^5.12.3 + version: 5.12.3(gatsby@5.12.11)(graphql@16.8.1) + gatsby-plugin-react-helmet: + specifier: ^6.12.0 + version: 6.12.0(gatsby@5.12.11)(react-helmet@6.1.0) react: specifier: ^18.2.0 version: 18.2.0 react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-helmet: + specifier: ^6.1.0 + version: 6.1.0(react@18.2.0) devDependencies: '@thesis-co/eslint-config': specifier: ^0.6.1 @@ -191,6 +197,9 @@ importers: '@types/react-dom': specifier: ^18.2.17 version: 18.2.17 + '@types/react-helmet': + specifier: ^6.1.9 + version: 6.1.9 '@typescript-eslint/eslint-plugin': specifier: ^6.12.0 version: 6.12.0(@typescript-eslint/parser@6.12.0)(eslint@8.54.0)(typescript@5.3.2) @@ -4416,10 +4425,6 @@ packages: - supports-color dev: true - /@openzeppelin/contracts@4.9.3: - resolution: {integrity: sha512-He3LieZ1pP2TNt5JbkPA4PNT9WC3gOTOlDcFGJW4Le4QKqwmiNJCRt44APfxMxvq7OugU/cqYuPcSBzOw38DAg==} - dev: false - /@openzeppelin/contracts@5.0.0: resolution: {integrity: sha512-bv2sdS6LKqVVMLI5+zqnNrNU/CA+6z6CmwFXm/MzmOPBRSO5reEJN7z0Gbzvs0/bv/MZZXNklubpwy3v2+azsw==} dev: false @@ -5605,6 +5610,12 @@ packages: '@types/react': 18.2.38 dev: true + /@types/react-helmet@6.1.9: + resolution: {integrity: sha512-nuOeTefP4yPTWHvjGksCBKb/4hsgJxSX7aSTjTIDFXJIkZ6Wo2Y4/cmE1FO9OlYBrHjKOer/0zLwY7s4qiQBtw==} + dependencies: + '@types/react': 18.2.38 + dev: true + /@types/react@18.2.38: resolution: {integrity: sha512-cBBXHzuPtQK6wNthuVMV6IjHAFkdl/FOPFIlkd81/Cd1+IqkHu/A+w4g43kaQQoYHik/ruaQBDL72HyCy1vuMw==} dependencies: @@ -9613,6 +9624,22 @@ packages: '@parcel/transformer-js': 2.8.3(@parcel/core@2.8.3) '@parcel/transformer-json': 2.8.3(@parcel/core@2.8.3) + /gatsby-plugin-manifest@5.12.3(gatsby@5.12.11)(graphql@16.8.1): + resolution: {integrity: sha512-qpH0pSIIt7ggO7OnP127eKn6fhD1DKTzg9Aw8vaMCO8MMOQ5qfOn3ZrRCgH6DuaU1admZU18gFKlCKH+QHoGfQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + gatsby: ^5.0.0-next + dependencies: + '@babel/runtime': 7.23.4 + gatsby: 5.12.11(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2) + gatsby-core-utils: 4.12.1 + gatsby-plugin-utils: 4.12.3(gatsby@5.12.11)(graphql@16.8.1) + semver: 7.5.4 + sharp: 0.32.6 + transitivePeerDependencies: + - graphql + dev: false + /gatsby-plugin-page-creator@5.12.3(gatsby@5.12.11)(graphql@16.8.1): resolution: {integrity: sha512-li9jKy70h4vXNxxRrXP2DpgEx05m5E7EDOLCjAWNsm7e9EO1szixXQ0ev6Ie1SBKT6vAHAoIonet6+oFattf9w==} engines: {node: '>=18.0.0'} @@ -9647,6 +9674,18 @@ packages: lodash.uniq: 4.5.0 dev: true + /gatsby-plugin-react-helmet@6.12.0(gatsby@5.12.11)(react-helmet@6.1.0): + resolution: {integrity: sha512-agcBCT9H8nlpkAU3D1fUeJbjh7IMPjGO/njoa7avIYLGsQ2nyGlHwcrEmS2zBHxYKaxPkztvr47OpCdnuEIvEw==} + engines: {node: '>=18.0.0'} + peerDependencies: + gatsby: ^5.0.0-next + react-helmet: ^5.1.3 || ^6.0.0 + dependencies: + '@babel/runtime': 7.23.4 + gatsby: 5.12.11(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2) + react-helmet: 6.1.0(react@18.2.0) + dev: false + /gatsby-plugin-typescript@5.12.1(gatsby@5.12.11): resolution: {integrity: sha512-NIigc9TnhjLam/WAQxvVLKpRgjOXzDDgetOt2F2qtO+1KjMuUgLxHdd613Z0JoSPGpi5ug0KG8U99gh9zge7jA==} engines: {node: '>=18.0.0'} @@ -13015,6 +13054,18 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.38)(react@18.2.0) dev: false + /react-helmet@6.1.0(react@18.2.0): + resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==} + peerDependencies: + react: '>=16.3.0' + dependencies: + object-assign: 4.1.1 + prop-types: 15.8.1 + react: 18.2.0 + react-fast-compare: 3.2.2 + react-side-effect: 2.1.2(react@18.2.0) + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -13070,6 +13121,14 @@ packages: react: 18.2.0 webpack: 5.89.0 + /react-side-effect@2.1.2(react@18.2.0): + resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==} + peerDependencies: + react: ^16.3.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /react-style-singleton@2.2.1(@types/react@18.2.38)(react@18.2.0): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -15351,9 +15410,9 @@ packages: prettier: 3.1.0 dev: true - github.com/thesis/solhint-config/159587cd6103f21aaa7fbadc17a06717a51b5b42(solhint@4.0.0): - resolution: {tarball: https://codeload.github.com/thesis/solhint-config/tar.gz/159587cd6103f21aaa7fbadc17a06717a51b5b42} - id: github.com/thesis/solhint-config/159587cd6103f21aaa7fbadc17a06717a51b5b42 + github.com/thesis/solhint-config/266de12d96d58f01171e20858b855ec80520de8d(solhint@4.0.0): + resolution: {tarball: https://codeload.github.com/thesis/solhint-config/tar.gz/266de12d96d58f01171e20858b855ec80520de8d} + id: github.com/thesis/solhint-config/266de12d96d58f01171e20858b855ec80520de8d name: solhint-config-thesis version: 0.1.0 engines: {node: '>=0.10.0'} @@ -15362,11 +15421,3 @@ packages: dependencies: solhint: 4.0.0 dev: true - - github.com/thesis/solidity-contracts/c315b9d: - resolution: {tarball: https://codeload.github.com/thesis/solidity-contracts/tar.gz/c315b9d} - name: '@thesis-co/solidity-contracts' - version: 0.0.1-pre - dependencies: - '@openzeppelin/contracts': 4.9.3 - dev: false diff --git a/website/gatsby-browser.js b/website/gatsby-browser.js new file mode 100644 index 000000000..e942f19b3 --- /dev/null +++ b/website/gatsby-browser.js @@ -0,0 +1 @@ +import "./src/styles/global-styles.css" diff --git a/website/gatsby-config.ts b/website/gatsby-config.ts index ecb1bd0a3..01a0d10f1 100644 --- a/website/gatsby-config.ts +++ b/website/gatsby-config.ts @@ -2,12 +2,27 @@ import type { GatsbyConfig } from "gatsby" const config: GatsbyConfig = { siteMetadata: { - // TODO: Update the needed data when the final domain name will be ready. - siteUrl: "https://www.yourdomain.tld", + siteUrl: "https://acre.fi/", title: "Acre", }, graphqlTypegen: true, - plugins: ["gatsby-plugin-pnpm"], + plugins: [ + "gatsby-plugin-pnpm", + "gatsby-plugin-react-helmet", + { + resolve: "gatsby-plugin-manifest", + options: { + name: "Acre", + short_name: "Acre", + start_url: "/", + background_color: "#F3E5C1", + theme_color: "#F3E5C1", + display: "standalone", + icon: "src/images/favicon.png", + crossOrigin: "use-credentials", + }, + }, + ], } export default config diff --git a/website/package.json b/website/package.json index 531edab47..23f6fdb53 100644 --- a/website/package.json +++ b/website/package.json @@ -21,14 +21,18 @@ }, "dependencies": { "gatsby": "^5.12.11", + "gatsby-plugin-manifest": "^5.12.3", + "gatsby-plugin-react-helmet": "^6.12.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-helmet": "^6.1.0" }, "devDependencies": { "@thesis-co/eslint-config": "^0.6.1", "@types/node": "^20.9.4", "@types/react": "^18.2.38", "@types/react-dom": "^18.2.17", + "@types/react-helmet": "^6.1.9", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", "eslint": "^8.54.0", diff --git a/website/src/components/SEO/index.tsx b/website/src/components/SEO/index.tsx new file mode 100644 index 000000000..98065d036 --- /dev/null +++ b/website/src/components/SEO/index.tsx @@ -0,0 +1,14 @@ +import React from "react" +import { Helmet } from "react-helmet" + +function SEO() { + return ( + + + Acre + + + ) +} + +export default SEO diff --git a/website/src/images/favicon.png b/website/src/images/favicon.png new file mode 100644 index 000000000..108f98dc9 Binary files /dev/null and b/website/src/images/favicon.png differ diff --git a/website/src/images/placeholder-mobile.svg b/website/src/images/placeholder-mobile.svg new file mode 100644 index 000000000..b9189ab6c --- /dev/null +++ b/website/src/images/placeholder-mobile.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/src/images/placeholder.svg b/website/src/images/placeholder.svg new file mode 100644 index 000000000..04bae5360 --- /dev/null +++ b/website/src/images/placeholder.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/src/pages/404.tsx b/website/src/pages/404.tsx index a537ff411..9a959c1dc 100644 --- a/website/src/pages/404.tsx +++ b/website/src/pages/404.tsx @@ -1,10 +1,14 @@ import * as React from "react" +import SEO from "../components/SEO" function NotFoundPage() { return ( -
-

Page not found

-
+ <> + +
+

Page not found

+
+ ) } diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index 0003e8d76..6b594ada3 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -1,10 +1,16 @@ import * as React from "react" +import SEO from "../components/SEO" function IndexPage() { return ( -
-

Landing page

-
+ <> + +
+
+
+
+
+ ) } diff --git a/website/src/styles/global-styles.css b/website/src/styles/global-styles.css new file mode 100644 index 000000000..bdd150f7e --- /dev/null +++ b/website/src/styles/global-styles.css @@ -0,0 +1,33 @@ +body { + width: 100vw; + height: 100vh; + margin: 0; + padding: 0; + overflow: hidden; +} + +.placeholder { + width: 100vw; + height: 100vh; + padding: 10px; + box-sizing: border-box; + background-color: #F3E5C1; +} + +.placeholder_svg { + width: 100%; + height: 100%; + background: url('../images/placeholder.svg'); + background-repeat: no-repeat; + background-position: center; + background-size: cover; +} + +@media (max-width: 854px) { + .placeholder_svg { + background: url('../images/placeholder-mobile.svg'); + background-repeat: no-repeat; + background-position: center; + background-size: cover; + } +}