bnpl
Version:
The smart contracts for bnpl
587 lines (487 loc) • 20.3 kB
text/typescript
import _ from 'lodash'
import { solidity } from "ethereum-waffle"
import chai from "chai"
import { BigNumber, Signer, Wallet, constants, utils } from "ethers"
import { ethers, deployments } from "hardhat"
import {
TestERC721,
Conduit,
BNPL,
TestERC20,
ERC4907,
} from '../build/typechain'
import { increaseTimestamp } from './testUtils'
import { orderType } from '../eip-712-types/order'
import { OrderComponents } from '../utils/types'
import { calculateOrderHash } from '../utils/encoding'
chai.use(solidity)
const { expect } = chai
const { get } = deployments
describe("BoostPool", () => {
let deployer: Wallet
let alice: Wallet
let bob: Wallet
let cat: Wallet
let attacker: Wallet
let deployerAddress: string
let aliceAddress: string
let bobAddress: string
let catAddress: string
let conduit: Conduit
let bnpl: BNPL
let erc721: TestERC721
let erc20: TestERC20
let erc4907: ERC4907
let chainId: string
let conduitKey: string
const ERC721_TokenId = 100
const ERC4907_TokenId = 0
const setupTest = deployments.createFixture(
async ({ deployments, ethers, getChainId }) => {
await deployments.fixture()
const signers = await ethers.getSigners();
[ deployer, alice, bob, cat, attacker ] = signers
deployerAddress = await deployer.getAddress()
aliceAddress = await alice.getAddress()
bobAddress = await bob.getAddress()
catAddress = await cat.getAddress()
conduit = (await ethers.getContractAt(
"Conduit",
(await get("ConduitControllerCreateConduit")).address
)) as Conduit
bnpl = (await ethers.getContractAt(
"BNPL",
(await get("BNPL")).address
)) as BNPL
erc721 = (await ethers.getContractAt(
"TestERC721",
(await get("TestERC721")).address
)) as TestERC721
erc20 = (await ethers.getContractAt(
"TestERC20",
(await get("TestERC20")).address
)) as TestERC20
erc4907 = (await ethers.getContractAt(
"ERC4907",
(await get("ERC4907")).address
)) as ERC4907
chainId = await getChainId()
await erc721.mint(aliceAddress, ERC721_TokenId)
await erc721.connect(alice).setApprovalForAll(conduit.address, true)
await erc20.mint(aliceAddress, utils.parseEther('10000'))
await erc20.mint(deployerAddress, utils.parseEther('10000'))
await erc20.approve(conduit.address, constants.MaxUint256)
conduitKey = `${deployerAddress}000000000000000000000000`
}
)
const getAndVerifyOrderHash = async (orderComponents: OrderComponents) => {
const orderHash = await bnpl.getOrderHash(orderComponents)
const derivedOrderHash = calculateOrderHash(orderComponents)
expect(orderHash).to.equal(derivedOrderHash)
return orderHash
}
const signOrder = async (
orderComponents: OrderComponents,
signer: Wallet
) => {
const domainData = {
name: "BNPL",
version: "1.0",
chainId,
verifyingContract: bnpl.address,
}
const signature = await signer._signTypedData(
domainData,
orderType,
orderComponents
);
const orderHash = await getAndVerifyOrderHash(orderComponents);
const { domainSeparator } = await bnpl.information();
const digest = utils.keccak256(
`0x1901${domainSeparator.slice(2)}${orderHash.slice(2)}`
);
const recoveredAddress = utils.recoverAddress(digest, signature);
expect(recoveredAddress).to.equal(signer.address);
return signature;
}
const getETHOrderComponents = () => {
const total = utils.parseEther('1.0')
const royalty = total.mul(5).div(100)
const fee = total.mul(1).div(100)
const withdrawFee = total.mul(5).div(1000)
return {
offerer: aliceAddress,
token: erc721.address,
identifier: BigNumber.from(ERC721_TokenId),
currency: constants.AddressZero,
artist: catAddress,
platform: bobAddress,
startTime: 0,
endTime: constants.MaxUint256,
duration: 3600,
periods: 3,
amount: total,
ratio: '7000',
royalty: royalty,
fee: fee,
withdrawFee: withdrawFee,
salt: constants.HashZero,
conduitKey,
counter: BigNumber.from(0)
}
}
const getERC20OrderComponents = () => {
const total = utils.parseEther('1.0')
const royalty = total.mul(5).div(100)
const fee = total.mul(1).div(100)
const withdrawFee = total.mul(5).div(1000)
return {
offerer: aliceAddress,
token: erc721.address,
identifier: BigNumber.from(ERC721_TokenId),
currency: erc20.address,
artist: catAddress,
platform: bobAddress,
startTime: 0,
endTime: constants.MaxUint256,
duration: 3600,
periods: 3,
amount: total,
ratio: '7000',
royalty: royalty,
fee: fee,
withdrawFee: withdrawFee,
salt: constants.HashZero,
conduitKey,
counter: BigNumber.from(0)
}
}
beforeEach(async () => {
await setupTest()
})
describe("fulfillOrder", () => {
it('signature', async () => {
const orderComponents = getETHOrderComponents()
await signOrder(orderComponents, alice)
})
it('eth fulfillOrder', async () => {
const orderComponents = getETHOrderComponents()
const signature = await signOrder(orderComponents, alice)
const aliceBalanceBefore = await alice.getBalance()
const bobBalanceBefore = await bob.getBalance()
const catBalanceBefore = await cat.getBalance()
const orderHash = calculateOrderHash(orderComponents)
let status = await bnpl.getOrderStatus(orderHash)
expect(status.isValidated).eq(false)
expect(status.isCancelled).eq(false)
expect(status.isFinalized).eq(false)
expect(status.isBroken).eq(false)
expect(status.fulfiller).eq(constants.AddressZero)
expect(status.startedAt).eq(0)
expect(status.shadowId).eq(0)
expect(status.paidTimes).eq(0)
await bnpl.fulfillOrder({
parameters: orderComponents,
signature
}, conduitKey, { value: utils.parseEther('1.0') })
const toPlatform = orderComponents.fee.add(orderComponents.withdrawFee)
const toOfferer = orderComponents.amount
.div(orderComponents.periods)
.mul(orderComponents.ratio)
.div(10000)
.sub(orderComponents.withdrawFee)
.sub(orderComponents.royalty.div(3))
expect(await bob.getBalance()).eq(toPlatform.add(bobBalanceBefore))
expect(await cat.getBalance()).eq(catBalanceBefore)
expect(await alice.getBalance()).eq(toOfferer.add(aliceBalanceBefore))
status = await bnpl.getOrderStatus(orderHash)
expect(status.isValidated).eq(true)
expect(status.isCancelled).eq(false)
expect(status.isFinalized).eq(false)
expect(status.isBroken).eq(false)
expect(status.fulfiller).eq(deployerAddress)
expect(status.startedAt).gt(0)
expect(status.shadowId).eq(ERC4907_TokenId)
expect(status.paidTimes).eq(1)
expect(await erc4907.userOf(ERC4907_TokenId)).eq(deployerAddress)
})
})
describe("repayOrder", () => {
it('repay', async () => {
const orderComponents = getETHOrderComponents()
const orderHash = calculateOrderHash(orderComponents)
const signature = await signOrder(orderComponents, alice)
const aliceBalanceBefore = await alice.getBalance()
const bobBalanceBefore = await bob.getBalance()
const catBalanceBefore = await cat.getBalance()
await bnpl.fulfillOrder({
parameters: orderComponents,
signature
}, conduitKey, { value: utils.parseEther('1.0') })
const toPlatform = orderComponents.fee.add(orderComponents.withdrawFee)
const toOfferer = orderComponents.amount
.div(orderComponents.periods)
.mul(orderComponents.ratio)
.div(10000)
.sub(orderComponents.withdrawFee)
.sub(orderComponents.royalty.div(3))
expect(await bob.getBalance()).eq(toPlatform.add(bobBalanceBefore))
expect(await cat.getBalance()).eq(catBalanceBefore)
expect(await alice.getBalance()).eq(toOfferer.add(aliceBalanceBefore))
await bnpl.repayOrder(
orderComponents,
conduitKey,
1,
{ value: utils.parseEther('1.0') }
)
const toPlatform2 = orderComponents.withdrawFee
expect(await bob.getBalance()).eq(toPlatform.add(toPlatform2).add(bobBalanceBefore))
expect(await cat.getBalance()).eq(catBalanceBefore)
expect(await alice.getBalance()).eq(toOfferer.mul(2).add(aliceBalanceBefore))
let status = await bnpl.getOrderStatus(orderHash)
expect(status.isValidated).eq(true)
expect(status.isCancelled).eq(false)
expect(status.isFinalized).eq(false)
expect(status.isBroken).eq(false)
expect(status.fulfiller).eq(deployerAddress)
expect(status.startedAt).gt(0)
expect(status.shadowId).eq(ERC4907_TokenId)
expect(status.paidTimes).eq(2)
expect(await erc4907.userOf(ERC4907_TokenId)).eq(deployerAddress)
expect(await erc721.ownerOf(ERC721_TokenId)).eq(bnpl.address)
await bnpl.repayOrder(
orderComponents,
conduitKey,
1,
{ value: utils.parseEther('1.0') }
)
expect(await bob.getBalance()).eq(toPlatform.add(toPlatform2.mul(2)).add(bobBalanceBefore))
expect(await cat.getBalance()).eq(orderComponents.royalty.add(catBalanceBefore))
expect(await alice.getBalance()).eq(orderComponents.amount.sub(orderComponents.withdrawFee.mul(3)).sub(orderComponents.royalty).add(aliceBalanceBefore))
status = await bnpl.getOrderStatus(orderHash)
expect(status.isValidated).eq(true)
expect(status.isCancelled).eq(false)
expect(status.isFinalized).eq(true)
expect(status.isBroken).eq(false)
expect(status.fulfiller).eq(deployerAddress)
expect(status.startedAt).gt(0)
expect(status.shadowId).eq(ERC4907_TokenId)
expect(status.paidTimes).eq(3)
expect(await erc4907.userOf(ERC4907_TokenId)).eq(constants.AddressZero)
expect(await erc721.ownerOf(ERC721_TokenId)).eq(deployerAddress)
})
it('repay ERC20', async () => {
const orderComponents = getERC20OrderComponents()
const orderHash = calculateOrderHash(orderComponents)
const signature = await signOrder(orderComponents, alice)
const aliceBalanceBefore = await erc20.balanceOf(aliceAddress)
const bobBalanceBefore = await erc20.balanceOf(bobAddress)
const catBalanceBefore = await erc20.balanceOf(catAddress)
await bnpl.fulfillOrder({
parameters: orderComponents,
signature
}, conduitKey)
const toPlatform = orderComponents.fee.add(orderComponents.withdrawFee)
const toOfferer = orderComponents.amount
.div(orderComponents.periods)
.mul(orderComponents.ratio)
.div(10000)
.sub(orderComponents.withdrawFee)
.sub(orderComponents.royalty.div(3))
expect(await erc20.balanceOf(bobAddress)).eq(toPlatform.add(bobBalanceBefore))
expect(await erc20.balanceOf(catAddress)).eq(catBalanceBefore)
expect(await erc20.balanceOf(aliceAddress)).eq(toOfferer.add(aliceBalanceBefore))
await bnpl.repayOrder(
orderComponents,
conduitKey,
1
)
const toPlatform2 = orderComponents.withdrawFee
expect(await erc20.balanceOf(bobAddress)).eq(toPlatform.add(toPlatform2).add(bobBalanceBefore))
expect(await erc20.balanceOf(catAddress)).eq(catBalanceBefore)
expect(await erc20.balanceOf(aliceAddress)).eq(toOfferer.mul(2).add(aliceBalanceBefore))
let status = await bnpl.getOrderStatus(orderHash)
expect(status.isValidated).eq(true)
expect(status.isCancelled).eq(false)
expect(status.isFinalized).eq(false)
expect(status.isBroken).eq(false)
expect(status.fulfiller).eq(deployerAddress)
expect(status.startedAt).gt(0)
expect(status.shadowId).eq(ERC4907_TokenId)
expect(status.paidTimes).eq(2)
expect(await erc4907.userOf(ERC4907_TokenId)).eq(deployerAddress)
expect(await erc721.ownerOf(ERC721_TokenId)).eq(bnpl.address)
const tx = await bnpl.repayOrder(
orderComponents,
conduitKey,
1
)
const rx = await tx.wait()
console.log(rx.gasUsed.toString())
expect(await erc20.balanceOf(bobAddress)).eq(toPlatform.add(toPlatform2.mul(2)).add(bobBalanceBefore))
expect(await erc20.balanceOf(catAddress)).eq(orderComponents.royalty.add(catBalanceBefore))
expect(await erc20.balanceOf(aliceAddress)).eq(orderComponents.amount.sub(orderComponents.withdrawFee.mul(3)).sub(orderComponents.royalty).add(aliceBalanceBefore))
status = await bnpl.getOrderStatus(orderHash)
expect(status.isValidated).eq(true)
expect(status.isCancelled).eq(false)
expect(status.isFinalized).eq(true)
expect(status.isBroken).eq(false)
expect(status.fulfiller).eq(deployerAddress)
expect(status.startedAt).gt(0)
expect(status.shadowId).eq(ERC4907_TokenId)
expect(status.paidTimes).eq(3)
expect(await erc4907.userOf(ERC4907_TokenId)).eq(constants.AddressZero)
expect(await erc721.ownerOf(ERC721_TokenId)).eq(deployerAddress)
})
it('repay all', async () => {
const orderComponents = getETHOrderComponents()
const orderHash = calculateOrderHash(orderComponents)
const signature = await signOrder(orderComponents, alice)
const aliceBalanceBefore = await alice.getBalance()
const bobBalanceBefore = await bob.getBalance()
const catBalanceBefore = await cat.getBalance()
await bnpl.fulfillOrder({
parameters: orderComponents,
signature
}, conduitKey, { value: utils.parseEther('1.0') })
await bnpl.repayOrder(
orderComponents,
conduitKey,
2,
{ value: utils.parseEther('1.0') }
)
expect(await bob.getBalance()).eq(orderComponents.withdrawFee.mul(2).add(orderComponents.fee).add(bobBalanceBefore))
expect(await cat.getBalance()).eq(orderComponents.royalty.add(catBalanceBefore))
expect(await alice.getBalance()).eq(orderComponents.amount.sub(orderComponents.withdrawFee.mul(2)).sub(orderComponents.royalty).add(aliceBalanceBefore))
let status = await bnpl.getOrderStatus(orderHash)
expect(status.isValidated).eq(true)
expect(status.isCancelled).eq(false)
expect(status.isFinalized).eq(true)
expect(status.fulfiller).eq(deployerAddress)
expect(status.startedAt).gt(0)
expect(status.shadowId).eq(ERC4907_TokenId)
expect(status.paidTimes).eq(3)
expect(await erc721.ownerOf(100)).eq(deployerAddress)
expect(await erc4907.userOf(ERC4907_TokenId)).eq(constants.AddressZero)
})
it('repay all ERC20', async () => {
const orderComponents = getERC20OrderComponents()
const orderHash = calculateOrderHash(orderComponents)
const signature = await signOrder(orderComponents, alice)
const aliceBalanceBefore = await erc20.balanceOf(aliceAddress)
const bobBalanceBefore = await erc20.balanceOf(bobAddress)
const catBalanceBefore = await erc20.balanceOf(catAddress)
await bnpl.fulfillOrder({
parameters: orderComponents,
signature
}, conduitKey)
await bnpl.repayOrder(
orderComponents,
conduitKey,
2
)
expect(await erc20.balanceOf(bobAddress)).eq(orderComponents.withdrawFee.mul(2).add(orderComponents.fee).add(bobBalanceBefore))
expect(await erc20.balanceOf(catAddress)).eq(orderComponents.royalty.add(catBalanceBefore))
expect(await erc20.balanceOf(aliceAddress)).eq(orderComponents.amount.sub(orderComponents.withdrawFee.mul(2)).sub(orderComponents.royalty).add(aliceBalanceBefore))
let status = await bnpl.getOrderStatus(orderHash)
expect(status.isValidated).eq(true)
expect(status.isCancelled).eq(false)
expect(status.isFinalized).eq(true)
expect(status.isBroken).eq(false)
expect(status.fulfiller).eq(deployerAddress)
expect(status.startedAt).gt(0)
expect(status.shadowId).eq(ERC4907_TokenId)
expect(status.paidTimes).eq(3)
expect(await erc721.ownerOf(100)).eq(deployerAddress)
expect(await erc4907.userOf(ERC4907_TokenId)).eq(constants.AddressZero)
})
})
describe("breakOrder", () => {
it('break', async () => {
const orderComponents = getETHOrderComponents()
const orderHash = calculateOrderHash(orderComponents)
const signature = await signOrder(orderComponents, alice)
const aliceBalanceBefore = await alice.getBalance()
const bobBalanceBefore = await bob.getBalance()
const catBalanceBefore = await cat.getBalance()
await bnpl.fulfillOrder({
parameters: orderComponents,
signature
}, conduitKey, { value: utils.parseEther('1.0') })
await increaseTimestamp(3601)
await bnpl.breakOrder(orderComponents)
const toOfferer = orderComponents.amount
.div(orderComponents.periods)
.mul(orderComponents.ratio)
.div(10000)
.sub(orderComponents.withdrawFee)
const toPlatform = orderComponents.withdrawFee
.add(orderComponents.fee)
.add(orderComponents.amount.div(orderComponents.periods)
.sub(orderComponents.amount
.div(orderComponents.periods)
.mul(orderComponents.ratio)
.div(10000)
)
)
expect(await bob.getBalance()).eq(toPlatform.add(bobBalanceBefore))
expect(await cat.getBalance()).eq(catBalanceBefore)
expect(await alice.getBalance()).eq(toOfferer.add(aliceBalanceBefore))
const status = await bnpl.getOrderStatus(orderHash)
expect(status.isValidated).eq(true)
expect(status.isCancelled).eq(false)
expect(status.isFinalized).eq(true)
expect(status.isBroken).eq(true)
expect(status.fulfiller).eq(deployerAddress)
expect(status.startedAt).gt(0)
expect(status.shadowId).eq(ERC4907_TokenId)
expect(status.paidTimes).eq(1)
expect(await erc4907.userOf(ERC4907_TokenId)).eq(constants.AddressZero)
expect(await erc721.ownerOf(ERC721_TokenId)).eq(aliceAddress)
})
it('break ERC20', async () => {
const orderComponents = getERC20OrderComponents()
const orderHash = calculateOrderHash(orderComponents)
const signature = await signOrder(orderComponents, alice)
const aliceBalanceBefore = await erc20.balanceOf(aliceAddress)
const bobBalanceBefore = await erc20.balanceOf(bobAddress)
const catBalanceBefore = await erc20.balanceOf(catAddress)
await bnpl.fulfillOrder({
parameters: orderComponents,
signature
}, conduitKey)
await increaseTimestamp(3601)
await bnpl.breakOrder(orderComponents)
const toOfferer = orderComponents.amount
.div(orderComponents.periods)
.mul(orderComponents.ratio)
.div(10000)
.sub(orderComponents.withdrawFee)
const toPlatform = orderComponents.withdrawFee
.add(orderComponents.fee)
.add(orderComponents.amount.div(orderComponents.periods)
.sub(orderComponents.amount
.div(orderComponents.periods)
.mul(orderComponents.ratio)
.div(10000)
)
)
expect(await erc20.balanceOf(bobAddress)).eq(toPlatform.add(bobBalanceBefore))
expect(await erc20.balanceOf(catAddress)).eq(catBalanceBefore)
expect(await erc20.balanceOf(aliceAddress)).eq(toOfferer.add(aliceBalanceBefore))
const status = await bnpl.getOrderStatus(orderHash)
expect(status.isValidated).eq(true)
expect(status.isCancelled).eq(false)
expect(status.isFinalized).eq(true)
expect(status.isBroken).eq(true)
expect(status.fulfiller).eq(deployerAddress)
expect(status.startedAt).gt(0)
expect(status.shadowId).eq(ERC4907_TokenId)
expect(status.paidTimes).eq(1)
expect(await erc4907.userOf(ERC4907_TokenId)).eq(constants.AddressZero)
expect(await erc721.ownerOf(ERC721_TokenId)).eq(aliceAddress)
})
})
describe("cancelOrder", () => {
})
})