Skip to content

Commit

Permalink
doc: added README for land-sale package
Browse files Browse the repository at this point in the history
  • Loading branch information
capedcrusader21 committed Nov 20, 2024
1 parent 154fc72 commit e9909d3
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {DeployFunction} from 'hardhat-deploy/types';
import {HardhatRuntimeEnvironment} from 'hardhat/types';

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const {deployments, getNamedAccounts} = hre;
const {deploy} = deployments;
const {deployer, sandAdmin, backendAuthWallet} = await getNamedAccounts();
await deploy('PolygonAuthValidator', {
contract: 'AuthValidator',
from: deployer,
args: [sandAdmin, backendAuthWallet],
log: true,
skipIfAlreadyDeployed: true,
});
};
export default func;
func.tags = ['PolygonAuthValidator', 'PolygonAuthValidator_deploy'];
139 changes: 139 additions & 0 deletions packages/deploy/deploy/1000_landsale/02_deploy_estate_sale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {Deployment} from 'hardhat-deploy/dist/types';
import {DeployFunction} from 'hardhat-deploy/types';
import {HardhatRuntimeEnvironment} from 'hardhat/types';
import {
getDeadline,
getLandSales,
LandSale,
setAsLandMinter,
writeProofs,
} from '../../../core/data/landSales/getLandSales';
import {skipUnlessTest} from '../../../core/utils/network';

type SaleDeployment = {
name: string;
// top level skip function for the whole sale data
skip?: (env: HardhatRuntimeEnvironment) => Promise<boolean>;
// object map of skip function for each individual sector
skipSector?: {
[sector: number]: (env: HardhatRuntimeEnvironment) => Promise<boolean>;
};
};

const sales: SaleDeployment[] = [
{name: 'EstateSaleWithAuth_0', skip: skipUnlessTest},
{name: 'LandPreSale_19', skip: async () => true},
{name: 'LandPreSale_20', skip: async () => true},
{name: 'LandPreSale_21', skip: async () => true},
{name: 'LandPreSale_22', skip: async () => true},
{name: 'LandPreSale_23', skip: async () => true},
{name: 'LandPreSale_24', skip: async () => true},
{name: 'LandPreSale_25', skip: async () => true},
{name: 'LandPreSale_26', skip: async () => true},
{name: 'LandPreSale_27', skip: async () => true},
{name: 'LandPreSale_28', skip: async () => true},
{name: 'LandPreSale_29', skip: async () => true},
{name: 'LandPreSale_30', skip: async () => true},
{name: 'LandPreSale_31', skip: async () => true},
{name: 'LandPreSale_32', skip: async () => true},
{name: 'LandPreSale_33', skip: async () => true},
{name: 'LandPreSale_34', skip: async () => true},
{name: 'LandPreSale_35', skip: async () => true},
{name: 'LandPreSale_36', skip: async () => true},
{name: 'LandPreSale_37', skip: async () => true},
];

const func: DeployFunction = async function (hre) {
const {deployments, getNamedAccounts} = hre;
const {deploy} = deployments;
const {
deployer,
landSaleBeneficiary,
backendReferralWallet,
landSaleFeeRecipient,
landSaleAdmin,
assetAdmin,
} = await getNamedAccounts();
const sandContract = await deployments.get('PolygonSand');
const landContract = await deployments.get('PolygonLand');
let assetContract: Deployment;
const deployedAsset = await deployments.getOrNull('Asset'); // L2 Asset, json files available on Polygon and Mumbai
if (!deployedAsset) {
// mock asset used for test networks and forking
// TODO: change to MockAsset from packages/asset when outside core
assetContract = await deploy('MockERC1155Asset', {
from: assetAdmin,
args: ['http://nft-test/nft-1155-{id}'],
log: true,
skipIfAlreadyDeployed: true,
});
} else {
assetContract = deployedAsset;
}
const authValidatorContract = await deployments.get('PolygonAuthValidator');

async function deployLandSale(name: string, landSale: LandSale) {
const {lands, merkleRootHash, sector} = landSale;
const landSaleName = `${name}_${sector}`;
const deadline = getDeadline(hre, sector);
const deployName = `PolygonLandPreSale_${sector}`;
let landSaleDeployment = await deployments.getOrNull(deployName);
if (!landSaleDeployment) {
landSaleDeployment = await deploy(deployName, {
from: deployer,
linkedData: lands,
contract: 'EstateSaleWithAuth',
args: [
landContract.address,
sandContract.address,
sandContract.address,
landSaleAdmin,
landSaleBeneficiary,
merkleRootHash,
deadline,
backendReferralWallet,
2000,
'0x0000000000000000000000000000000000000000',
assetContract.address,
landSaleFeeRecipient,
authValidatorContract.address,
],
log: true,
});
writeProofs(hre, landSaleName, landSale);
}
await setAsLandMinter(hre, landSaleDeployment.address, 'PolygonLand');
}

for (const sale of sales) {
if (sale.skip) {
const skip = await sale.skip(hre);
if (skip) continue;
}
const landSales = await getLandSales(
sale.name,
hre.network.name,
hre.network.live
);
const skipSector = sale.skipSector || {};
const sectors = Object.keys(skipSector).map((k) => parseInt(k));
for (const landSale of landSales) {
if (sectors.includes(landSale.sector)) {
const skip = await skipSector[landSale.sector](hre);
if (skip) {
console.log(`Skipping sector ${landSale.sector}`);
continue;
}
}
await deployLandSale(sale.name, landSale);
}
}
};

export default func;
func.tags = ['PolygonEstateSaleWithAuth', 'PolygonEstateSaleWithAuth_deploy'];
func.dependencies = [
'PolygonSand_deploy',
'PolygonLand_deploy',
'PolygonAuthValidator_deploy',
];
8 changes: 7 additions & 1 deletion packages/land-sale/.solcover.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@ module.exports = {
grep: '@skip-on-coverage', // Find everything with this tag
invert: true, // Run the grep's inverse set.
},
skipFiles: ['/mock'],
skipFiles: [
'/mocks',
'/AuthValidator.sol',
'common/BaseWithStorage',
'common/Libraries/SafeMathWithRequire.sol',
'ReferralValidator/ReferralValidator.sol',
],
};
102 changes: 102 additions & 0 deletions packages/land-sale/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# land-sale

The land-sale package outlines the process for deploying Land Presale smart
contracts for The Sandbox's metaverse.

## Architecture

This package manages deployments for Land sales. The package
contains 2 main contracts:

| Component | Description |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| [AuthValidator](contracts/AuthValidator.sol) | contract which verifies that only a trusted wallet can authorize land sale actions |
| [EstateSaleWithAuth](contracts/EstateSaleWithAuth.sol) | contract used to create and manage land sales in the metaverse. |

- It leverages two key files:
- **bundles.testnet.json**: Specifies optional asset bundles sold with certain
Lands.
- **sectors.testnet.json**: Defines the sectors being deployed.
- Deploy a new Land Presale contract for each sector using the
`EstateSaleWithAuth` template, as shown in the deploy script.
- **No need for new deploy scripts**: Instead of creating a new deploy script
for each sale, the existing script is updated with the new sectors.
- **Set a Deadline**: A deadline must be specified in
`packages/land-sale/data/deadlines.ts`.
- For mainnet deadline will be of six months and for testnet this deadline is
ignored by the script.

## Getting Started

### Step 1: Prepare for Deployment

- **Receive JSON Files**: The Landsale team will provide `bundles.testnet.json`
and `sectors.testnet.json` files.
- **Organize the Files**: Place the JSON files in a new folder within
`data/land-sale` named `LandPreSale_XX`.
- **NOTE**: Here, `XX` is an incremented folder number that does not correspond
to sector numbers.

### Step 2: Manage Secrets

- For testnet, the secret file is automatically generated in the deploy folder.
- For mainnet, you must manually create a file at `packages/deploy/secrets/estate-sale/.LandPreSale_XX_testnet_secret`, where XX is the sector number, and note that the mainnet secret differs from the testnet secret.

---

## Sample files

### bundles.test.json :

```
{
"bundleId":
[
bundle-Id
]
}
```

### sectors.testnet.json :

```
[
{
"sector": sector_number,
"lands": [
{
"coordinateX": X-coordinate,
"coordinateY": Y-coordinate,
"ownerAddress": "",
"bundleId": bundle-Id
}
]
}
]
```

### deadlines.ts :
- The deadline represents the timestamp (in seconds) until which the land sale contract for each sector will remain active, with the deadline number corresponding to the sector number.
- Place the deadline in `data/deadlines.ts`

```
sector_number: new Date('YYYY-MM-DDT12:00:00.000Z').valueOf() / 1000
```

---

## Running the project locally

- Make sure you navigate to the `deploy` package directory before running any commands
- Create a new `.env` file by copying the structure from `.env.example`
- Ensure you replace any placeholder values with the actual configuration needed for your environment.
- Install dependencies with `yarn`

To execute the deployment, run

- for fork deployment:
`yarn fork:deploy --network NETWORK --tags TAG --no-impersonation`
- for live deployment: `yarn live:deploy --network NETWORK --tags TAG`
- where:
- `NETWORK` is the network name like: `polygon`, `mainet` etc
- `TAG` are the tags used to limit which deployment scripts will be executed
16 changes: 16 additions & 0 deletions packages/land-sale/test/EstateSaleWithAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s
const {deployEstateSaleContract} = await runEstateSaleSetup();
await expect(deployEstateSaleContract()).to.not.be.reverted;
});

describe('Functions', function () {
describe('setReceivingWallet', function () {
it('should allow admin to set new receving wallet', async function () {
Expand All @@ -22,6 +23,7 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s
.to.emit(EstateSaleContractAsAdmin, 'NewReceivingWallet')
.withArgs(newLandSaleBeneficiary.address);
});

it('should not allow non-admin to set new receving wallet', async function () {
const {EstateSaleContract, newLandSaleBeneficiary} =
await runEstateSaleSetup();
Expand All @@ -32,6 +34,7 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s

await expect(tx).to.be.revertedWith('NOT_AUTHORIZED');
});

it('should not allow admin to set new receving wallet to the zero address', async function () {
const {EstateSaleContractAsAdmin} = await runEstateSaleSetup();

Expand All @@ -41,6 +44,7 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s
await expect(tx).to.be.revertedWith('ZERO_ADDRESS');
});
});

describe('buyLandWithSand', function () {
describe('Reverts', function () {
it('should revert if the sale is over', async function () {
Expand All @@ -55,6 +59,7 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s
}),
).to.be.revertedWith('SALE_IS_OVER');
});

it('should revert if the buyer is not the sender', async function () {
const {buyLand, landBuyer2, landBuyer} = await runEstateSaleSetup();
await expect(
Expand All @@ -64,6 +69,7 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s
}),
).to.be.revertedWith('NOT_AUTHORIZED');
});

it('should revert if trying to buy a reserved land', async function () {
const {buyLand, landBuyer, reservedLandIndex} =
await runEstateSaleSetup();
Expand All @@ -74,6 +80,7 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s
}),
).to.be.revertedWith('RESERVED_LAND');
});

it('should revert if the signature is invalid', async function () {
const {buyLand} = await runEstateSaleSetup();
await expect(
Expand All @@ -82,6 +89,7 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s
}),
).to.be.revertedWith('INVALID_AUTH');
});

it('should revert if trying to purchase another land that the one from the proof', async function () {
const {buyLand} = await runEstateSaleSetup();
await expect(
Expand All @@ -90,13 +98,15 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s
}),
).to.be.revertedWith('INVALID_LAND');
});

it("shoudl revert if the buyer doesn't have enough funds", async function () {
const {buyLand, landBuyer} = await runEstateSaleSetup();
await expect(
buyLand({buyer: landBuyer, landIndex: 3}),
).to.be.revertedWith('ERC20: transfer amount exceeds balance');
});
});

describe('Success', function () {
it('should send the 5% land fee to the specified address', async function () {
const {
Expand All @@ -115,6 +125,7 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s

expect(balance).to.equal(fivePercentFee);
});

it('should NOT revert if the buyer is not the sender and IT IS a meta transaction', async function () {
const {buyLand, trustedForwarder, landBuyer} =
await runEstateSaleSetup();
Expand All @@ -125,6 +136,7 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s
}),
).to.not.be.reverted;
});

it('should NOT revert if trying to buy a reserved land from correct account', async function () {
const {buyLand, landBuyer2} = await runEstateSaleSetup();
await expect(
Expand All @@ -134,10 +146,12 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s
}),
).to.not.be.reverted;
});

it("should mint new land to the to's address", async function () {
const {buyLand} = await runEstateSaleSetup();
await expect(buyLand()).to.not.be.reverted;
});

it("should send assets to the to's address for premium lands", async function () {
const {
buyLand,
Expand All @@ -158,6 +172,7 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s
expect(balance[0]).to.equal(1);
});
});

describe('Events', function () {
it("should emit NewReceivingWallet with the new wallet's address", async function () {
const {EstateSaleContractAsAdmin, newLandSaleBeneficiary} =
Expand All @@ -169,6 +184,7 @@ describe('EstateSaleWithAuth (/packages/land-sale/contracts/EstateSaleWithAuth.s
.to.emit(EstateSaleContractAsAdmin, 'NewReceivingWallet')
.withArgs(newLandSaleBeneficiary.address);
});

it('should emit LandQuadPurchased with the buyers and land info', async function () {
const {
buyLand,
Expand Down
Loading

1 comment on commit e9909d3

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage for this commit

90.32%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
packages/land-sale/contracts
   EstateSaleWithAuth.sol80.33%68.18%75%90.32%109, 125, 166, 166, 169, 259–266, 63, 74, 80, 88, 88, 88–89
packages/land-sale/contracts/common/Interfaces
   ERC1155.sol100%100%100%100%
   ERC165.sol100%100%100%100%
   ERC20.sol100%100%100%100%
   ERC20WithMetadata.sol100%100%100%100%
   ERC721.sol100%100%100%100%
   ERC721Events.sol100%100%100%100%
   ILandToken.sol100%100%100%100%

Please sign in to comment.