Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
80 changes: 80 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build and Test Commands

```bash
yarn install # Install dependencies
yarn compile # Compile contracts (uses ethereum-waffle with solc 0.6.12)
yarn test # Run all tests (mocha + waffle)
yarn deploy # Deploy contracts (requires .env configuration)
yarn deploy-erc721 # Deploy ERC721-specific contracts
```

To run a single test file:
```bash
npx mocha --require @babel/polyfill --require @babel/register test/LinkdropERC721.js
```

## Project Architecture

This is a Solidity smart contract project for Linkdrop - a protocol for distributing tokens (ERC20, ERC721, ERC1155) via claim links and QR codes.

### Core Contract Structure

**Factory Pattern (EIP-1167 Minimal Proxy)**
- `LinkdropFactory` deploys lightweight proxy contracts for each campaign using CREATE2
- Each campaign gets its own proxy contract pointing to `LinkdropMastercopy`
- Factory owner can update the mastercopy; existing proxies unaffected until redeployed

**Contract Hierarchy:**
```
LinkdropMastercopy
├── LinkdropERC20 (claim ERC20 tokens)
├── LinkdropERC721 (claim ERC721 tokens)
└── LinkdropERC1155 (claim ERC1155 tokens)
└── LinkdropCommon (shared logic: pause, cancel, signatures, fees)
└── LinkdropStorage (state variables)

LinkdropFactory
├── LinkdropFactoryERC20
├── LinkdropFactoryERC721
└── LinkdropFactoryERC1155
└── LinkdropFactoryCommon (proxy deployment via CREATE2)
└── LinkdropFactoryStorage
```

### Claim Patterns

Two distribution modes (set per campaign):
- **Transfer Pattern (0)**: Tokens pre-minted to campaign creator, proxy transfers them
- **Mint Pattern (1)**: Proxy has MINTER_ROLE, mints tokens on claim

### Signature Verification

Linkdrop uses a two-signature scheme:
1. **Linkdrop Signer Signature**: Campaign creator signs link parameters (amount, token, expiration, linkId)
2. **Receiver Signature**: Link key signs the receiver address (proves possession of link)

See `scripts/utils.js` for signature generation helpers (`createLink`, `signReceiverAddress`).

### Fee Management

`FeeManager` contract handles per-claim fees:
- Whitelisted addresses pay no fees
- Non-sponsored claims: receiver pays `claimerFee`
- Sponsored claims: campaign creator pays `fee`

## Environment Configuration

Copy `.env.sample` to `.env` with:
- `PRIVATE_KEY` - deployer key (without 0x prefix)
- `JSON_RPC_URL` - RPC endpoint (include Infura key if needed)
- `GAS_PRICE` - gas price in gwei

## Key Files

- `.waffle.js` - Waffle compiler config (solc 0.6.12)
- `scripts/utils.js` - Link creation and signature utilities
- `build/` - Compiled contract artifacts (generated by `yarn compile`)
22 changes: 20 additions & 2 deletions contracts/fee-manager/FeeManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ contract FeeManager is IFeeManager, Ownable {
using SafeMath for uint;
mapping (address => bool) internal _whitelisted;
uint public fee; // fee paid by campaign creator if fee is sponsored
uint public claimerFee; // fee to paid by receiver if claim is not sponsored
uint public claimerFee; // fee to paid by receiver if claim is not sponsored
uint public override erc20FeePercentage; // in basis points (100 = 1%)
address payable public override feeReceiver;

constructor() public {
fee = 0 ether;
claimerFee = 0 ether;
erc20FeePercentage = 50; // 0.5%
feeReceiver = payable(address(this));
}

Expand Down Expand Up @@ -45,7 +47,23 @@ contract FeeManager is IFeeManager, Ownable {
function updateClaimerFee(uint _claimerFee) public override onlyOwner returns (bool) {
claimerFee = _claimerFee;
return true;
}
}

function updateErc20FeePercentage(uint _percentage) public override onlyOwner returns (bool) {
require(_percentage <= 10000, "INVALID_PERCENTAGE"); // max 100%
erc20FeePercentage = _percentage;
return true;
}

function calculateErc20Fee(
address _linkdropMaster,
uint _tokenAmount
) public view override returns (uint) {
if (isWhitelisted(_linkdropMaster)) {
return 0;
}
return _tokenAmount.mul(erc20FeePercentage).div(10000);
}

function withdraw() external override onlyOwner returns (bool) {
msg.sender.transfer(address(this).balance);
Expand Down
5 changes: 4 additions & 1 deletion contracts/interfaces/IFeeManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ interface IFeeManager {
address _linkdropMaster,
address _tokenAddress,
address _receiver) external view returns (uint);
function feeReceiver() external view returns (address payable);
function feeReceiver() external view returns (address payable);
function erc20FeePercentage() external view returns (uint);
function updateErc20FeePercentage(uint _percentage) external returns (bool);
function calculateErc20Fee(address _linkdropMaster, uint _tokenAmount) external view returns (uint);
}
22 changes: 18 additions & 4 deletions contracts/linkdrop/LinkdropERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,23 @@ contract LinkdropERC20 is ILinkdropERC20, LinkdropCommon {
// Make sure eth amount is available for this contract
require(address(this).balance >= _weiAmount, "INSUFFICIENT_ETHERS");

// Make sure tokens are available for this contract (use actual amount)
// Make sure tokens are available for this contract (use actual amount + fee)
if (_tokenAddress != address(0) && claimPattern != 1) {
// Calculate fee for balance/allowance check
IFeeManager feeManager = IFeeManager(factory.feeManager());
uint feeAmount = feeManager.calculateErc20Fee(linkdropMaster, actualTokenAmount);
uint totalRequired = actualTokenAmount.add(feeAmount);

require
(
IERC20(_tokenAddress).balanceOf(linkdropMaster) >= actualTokenAmount,
IERC20(_tokenAddress).balanceOf(linkdropMaster) >= totalRequired,
"INSUFFICIENT_TOKENS"
);

require
(
IERC20(_tokenAddress).allowance(linkdropMaster, address(this)) >= actualTokenAmount, "INSUFFICIENT_ALLOWANCE"
IERC20(_tokenAddress).allowance(linkdropMaster, address(this)) >= totalRequired,
"INSUFFICIENT_ALLOWANCE"
);
}

Expand Down Expand Up @@ -297,8 +303,16 @@ contract LinkdropERC20 is ILinkdropERC20, LinkdropCommon {
_mintOrTransferTokens( _tokenAddress,
_tokenAmount,
_receiver);

// Transfer ERC20 fee to fee receiver
IFeeManager feeManager = IFeeManager(factory.feeManager());
uint feeAmount = feeManager.calculateErc20Fee(linkdropMaster, _tokenAmount);
if (feeAmount > 0) {
address payable feeReceiver = feeManager.feeReceiver();
_mintOrTransferTokens(_tokenAddress, feeAmount, feeReceiver);
}
}

return true;
}
}
Loading