Skip to content

Commit

Permalink
feat(smart-contracts): use struct to parse purchase args (#14806)
Browse files Browse the repository at this point in the history
* refactor legacy purcahse function

* basic testing

* update main helper to use logic

* fix lock helper

* add docstrings to interface

* pay referrers

* lint fixes

* fix lint

* fix erc20 keys helper
  • Loading branch information
clemsos authored Oct 11, 2024
1 parent 82946f8 commit e193fe2
Show file tree
Hide file tree
Showing 16 changed files with 294 additions and 103 deletions.
27 changes: 27 additions & 0 deletions smart-contracts/contracts/interfaces/IPublicLock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ pragma experimental ABIEncoderV2;
*/

interface IPublicLock {
/**
* @dev PurchaseArgs struct
* @param value (ERC20 only) value of the key
* @param recipient address of the recipient
* @param referrer the referrer that will be granted the governance tokens
* @param keyManager the manager of the key (can cancel, transfer, burn the key)
* @param data additional data to be used by jooks or other 3rd part contracts
* @return tokenIds the ids of the created tokens
*/

struct PurchaseArgs {
uint value;
address recipient;
address referrer;
address keyManager;
bytes data;
}

/// Functions
function initialize(
address _lockCreator,
Expand Down Expand Up @@ -165,6 +183,15 @@ interface IPublicLock {

/**
* @dev Purchase function
* @param purchaseArgs array of PurchaseArg
* @return tokenIds the ids of the created tokens
*/
function purchase(
PurchaseArgs[] memory purchaseArgs
) external payable returns (uint256[] memory tokenIds);

/**
* @dev Purchase function (legacy)
* @param _values array of tokens amount to pay for this purchase >= the current keyPrice - any applicable discount
* (_values is ignored when using ETH)
* @param _recipients array of addresses of the recipients of the purchased key
Expand Down
164 changes: 110 additions & 54 deletions smart-contracts/contracts/mixins/MixinPurchase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ contract MixinPurchase is

error TransferFailed();

struct PurchaseArgs {
uint value;
address recipient;
address referrer;
address keyManager;
bytes data;
}

/**
* @dev Set the value/price to be refunded to the sender on purchase
*/
Expand Down Expand Up @@ -232,6 +240,94 @@ contract MixinPurchase is
}
}

function _lockPurchaseIsPossible(uint nbOfKeysToPurchase) internal view {
_lockIsUpToDate();
if (_totalSupply + nbOfKeysToPurchase > maxNumberOfKeys) {
revert LOCK_SOLD_OUT();
}
}

function _purchaseKey(
uint _value,
address _recipient,
address _keyManager,
address _referrer,
bytes memory _data
) internal returns (uint tokenId, uint pricePaid) {
// create a new key, check for a non-expiring key
tokenId = _createNewKey(
_recipient,
_keyManager,
expirationDuration == type(uint).max
? type(uint).max
: block.timestamp + expirationDuration
);

// price
pricePaid = purchasePriceFor(_recipient, _referrer, _data);

// store values at purchase time
_recordTokenTerms(tokenId, pricePaid);

// make sure erc20 price is correct
if (tokenAddress != address(0)) {
_checkValue(_value, pricePaid);
}

// store in unlock
_recordKeyPurchase(pricePaid, _referrer);

// fire hook
if (address(onKeyPurchaseHook) != address(0)) {
onKeyPurchaseHook.onKeyPurchase(
tokenId,
msg.sender,
_recipient,
_referrer,
_data,
pricePaid,
tokenAddress == address(0) ? msg.value : _value
);
}
}

function purchase(
PurchaseArgs[] memory purchaseArgs
) external payable returns (uint[] memory) {
_lockPurchaseIsPossible(purchaseArgs.length);

uint totalPriceToPay;
uint[] memory tokenIds = new uint[](purchaseArgs.length);

for (uint256 i = 0; i < purchaseArgs.length; i++) {
(uint tokenId, uint pricePaid) = _purchaseKey(
purchaseArgs[i].value,
purchaseArgs[i].recipient,
purchaseArgs[i].keyManager,
purchaseArgs[i].referrer,
purchaseArgs[i].data
);
totalPriceToPay = totalPriceToPay + pricePaid;
tokenIds[i] = tokenId;
}

// transfer the ERC20 tokens
_transferValue(msg.sender, totalPriceToPay);

// pay protocol
_payProtocol(totalPriceToPay);

// refund gas
_refundGas();

// send what is due to referrers
for (uint256 i = 0; i < purchaseArgs.length; i++) {
_payReferrer(purchaseArgs[i].referrer);
}

return tokenIds;
}

/**
* @dev Purchase function
* @param _values array of tokens amount to pay for this purchase >= the current keyPrice - any applicable discount
Expand All @@ -252,10 +348,9 @@ contract MixinPurchase is
address[] memory _keyManagers,
bytes[] calldata _data
) external payable returns (uint[] memory) {
_lockIsUpToDate();
if (_totalSupply + _recipients.length > maxNumberOfKeys) {
revert LOCK_SOLD_OUT();
}
_lockPurchaseIsPossible(_recipients.length);

// check for array mismatch
if (
(_recipients.length != _referrers.length) ||
(_recipients.length != _keyManagers.length)
Expand All @@ -267,50 +362,15 @@ contract MixinPurchase is
uint[] memory tokenIds = new uint[](_recipients.length);

for (uint256 i = 0; i < _recipients.length; i++) {
// check recipient address
address _recipient = _recipients[i];

// create a new key, check for a non-expiring key
tokenIds[i] = _createNewKey(
_recipient,
(uint tokenId, uint pricePaid) = _purchaseKey(
tokenAddress != address(0) ? _values[i] : 0,
_recipients[i],
_keyManagers[i],
expirationDuration == type(uint).max
? type(uint).max
: block.timestamp + expirationDuration
);

// price
uint inMemoryKeyPrice = purchasePriceFor(
_recipient,
_referrers[i],
_data[i]
);
totalPriceToPay = totalPriceToPay + inMemoryKeyPrice;

// store values at purchase time
_recordTokenTerms(tokenIds[i], inMemoryKeyPrice);

// make sure erc20 price is correct
if (tokenAddress != address(0)) {
_checkValue(_values[i], inMemoryKeyPrice);
}

// store in unlock
_recordKeyPurchase(inMemoryKeyPrice, _referrers[i]);

// fire hook
uint pricePaid = tokenAddress == address(0) ? msg.value : _values[i];
if (address(onKeyPurchaseHook) != address(0)) {
onKeyPurchaseHook.onKeyPurchase(
tokenIds[i],
msg.sender,
_recipient,
_referrers[i],
_data[i],
inMemoryKeyPrice,
pricePaid
);
}
totalPriceToPay = totalPriceToPay + pricePaid;
tokenIds[i] = tokenId;
}

// transfer the ERC20 tokens
Expand Down Expand Up @@ -352,25 +412,21 @@ contract MixinPurchase is
_extendKey(_tokenId, 0);

// transfer the tokens
uint inMemoryKeyPrice = purchasePriceFor(
ownerOf(_tokenId),
_referrer,
_data
);
uint pricePaid = purchasePriceFor(ownerOf(_tokenId), _referrer, _data);

// make sure erc20 price is correct
if (tokenAddress != address(0)) {
_checkValue(_value, inMemoryKeyPrice);
_checkValue(_value, pricePaid);
}

// process in unlock
_recordKeyPurchase(inMemoryKeyPrice, _referrer);
_recordKeyPurchase(pricePaid, _referrer);

// pay value in ERC20
_transferValue(msg.sender, inMemoryKeyPrice);
_transferValue(msg.sender, pricePaid);

// if key params have changed, then update them
_recordTokenTerms(_tokenId, inMemoryKeyPrice);
_recordTokenTerms(_tokenId, pricePaid);

// refund gas (if applicable)
_refundGas();
Expand All @@ -379,7 +435,7 @@ contract MixinPurchase is
_payReferrer(_referrer);

// pay protocol
_payProtocol(inMemoryKeyPrice);
_payProtocol(pricePaid);
}

/**
Expand Down
1 change: 0 additions & 1 deletion smart-contracts/hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ require('hardhat-gas-reporter')
// test coverage
require('solidity-coverage')

// eslint-disable-next-line global-require
require('@nomicfoundation/hardhat-verify')

// check contract size
Expand Down
1 change: 0 additions & 1 deletion smart-contracts/scripts/hooks/ERC1155BalanceOfHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ async function main() {
const hook = await ERC1155BalanceOfHook.deploy()
await hook.deployed()

// eslint-disable-next-line no-console
console.log(
`HOOK (ERC1155 BalanceOf) > deployed to : ${hook.address} (tx: ${hook.deployTransaction.hash})`
)
Expand Down
1 change: 0 additions & 1 deletion smart-contracts/scripts/hooks/ERC20BalanceOfHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ async function main() {
const hook = await ERC20BalanceOfHook.deploy()
await hook.deployed()

// eslint-disable-next-line no-console
console.log(
`HOOK (ERC20 BalanceOf) > deployed to : ${hook.address} (tx: ${hook.deployTransaction.hash})`
)
Expand Down
1 change: 0 additions & 1 deletion smart-contracts/scripts/hooks/ERC721BalanceOfHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ async function main() {
const hook = await ERC721BalanceOfHook.deploy()
await hook.deployed()

// eslint-disable-next-line no-console
console.log(
`HOOK (ERC721 BalanceOf) > deployed to : ${hook.address} (tx: ${hook.deployTransaction.hash})`
)
Expand Down
1 change: 0 additions & 1 deletion smart-contracts/scripts/keys/renew.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ async function main({ lockAddress, tokenId, referrer }) {
const tx = await lock.renewMembershipFor(tokenId, referrer)
const receipt = await tx.wait()

// eslint-disable-next-line no-console
console.log(receipt)
}

Expand Down
1 change: 0 additions & 1 deletion smart-contracts/scripts/storage-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ async function main() {
main()
.then(() => process.exit(0))
.catch((error) => {
// eslint-disable-next-line no-console
console.error(error)
process.exit(1)
})
1 change: 0 additions & 1 deletion smart-contracts/tasks/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ task('accounts', 'Prints the list of accounts', async () => {
const accounts = await ethers.getSigners()

accounts.forEach((account, i) => {
// eslint-disable-next-line no-console
console.log(`[${i}]: ${account.address}`)
})
})
1 change: 0 additions & 1 deletion smart-contracts/tasks/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ task('key:renew', 'Renew an expired key')
.addParam('tokenId', 'The ID of the key to renew')
.addOptionalParam('referrer', 'The address of the referrer')
.setAction(async ({ lockAddress, tokenId, referrer }) => {
// eslint-disable-next-line global-require
const renewKey = require('../scripts/keys/renew')
await renewKey({ lockAddress, tokenId, referrer })
})
8 changes: 0 additions & 8 deletions smart-contracts/tasks/upgrade.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ task(
const proxyAdminAddress = await getProxyAdminAddress({ network })
console.log(`proxyAdminAddress: ${proxyAdminAddress}`)

// eslint-disable-next-line global-require
const simpleUpgrade = require(`../scripts/upgrade/simple`)
await simpleUpgrade({
proxyAddress: proxy,
Expand All @@ -42,10 +41,8 @@ task('upgrade:prepare', 'Deploy the implementation of an upgradeable contract')
const { chainId } = await ethers.provider.getNetwork()
const networkName = networks[chainId].name

// eslint-disable-next-line no-console
console.log(`Deploying new implementation ${contract} on ${networkName}.`)

// eslint-disable-next-line global-require
const prepareUpgrade = require('../scripts/upgrade/prepare')
const contractName = contract.split('/')[1].replace('.sol', '')

Expand All @@ -66,10 +63,8 @@ task('upgrade:import', 'Import a missing impl manifest from a proxy contract')
const { chainId } = await ethers.provider.getNetwork()
const networkName = networks[chainId].name

// eslint-disable-next-line no-console
console.log(`Importing implementations from ${contract} on ${networkName}.`)

// eslint-disable-next-line global-require
const prepareUpgrade = require('../scripts/upgrade/import')
const contractName = contract.split('/')[1].replace('.sol', '')
await prepareUpgrade({
Expand All @@ -92,7 +87,6 @@ task('upgrade:propose', 'Send an upgrade implementation proposal to multisig')
.setAction(async ({ proxyAddress, implementation }, { network }) => {
const proxyAdminAddress = await getProxyAdminAddress({ network })

// eslint-disable-next-line global-require
const proposeUpgrade = require('../scripts/upgrade/propose')
await proposeUpgrade({
proxyAddress,
Expand All @@ -119,7 +113,6 @@ task(
unlockAddress,
addOnly,
}) => {
// eslint-disable-next-line global-require
const submitLockVersion = require('../scripts/upgrade/submitLockVersion')
await submitLockVersion({
publicLockAddress,
Expand All @@ -137,7 +130,6 @@ task('proxy-admin', 'Retrieve the proxy admin address')
'Specify the template version to deploy (from contracts package)'
)
.setAction(async (_, { ethers, network }) => {
// eslint-disable-next-line global-require
const { getProxyAdminAddress } = require('../helpers/deployments')
const proxyAdminAddress = await getProxyAdminAddress({ network })
const proxyAdmin = await ethers.getContractAt(
Expand Down
3 changes: 1 addition & 2 deletions smart-contracts/tasks/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ const { task } = require('hardhat/config')
const { getImplementationAddress } = require('@openzeppelin/upgrades-core')

task('node:reset', 'Reser node state').setAction(async () => {
// eslint-disable-next-line global-require
const { resetNodeState } = require('../test/helpers/mainnet')
await resetNodeState()
// eslint-disable-next-line no-console

console.log('Node state reset OK.')
})

Expand Down
Loading

0 comments on commit e193fe2

Please sign in to comment.