PPLNS, or "Pay per last N shares", is a reward system that has been known for a while, but I don't think there's a definitive thread discussing it, including how to implement it in a way that is hopping-proof even considering difficulty changes.
The basic method is this: Whenever a block is found, payment is given to shares in a window, starting with the last share submitted and going backwards up to some number N of shares. Shares older than the window are not paid. This is essentially a 0-1 cutoff decay function. There is no concept of rounds - shares can be paid even if they were found before the previous block. This means that a given share can be paid more than once - however, the payouts are chosen so that the expected reward is equal to solo expected reward. Because the payment for a share depends only on blocks found in the future and not on shares and blocks found in the past, there is no way to hop based on the current status of the pool. However, if implemented incorrectly, it is possible to hop based on imminent difficulty changes.
The most naive implementation is to have a fixed N and simply pay the last N shares equally. However, this makes it more profitable to mine before difficulty decreases, and less profitable before difficulty increases - the number of blocks that are expected to be found in your window depends on the future difficulty, while the payout you can receive elsewhere depends on the current difficulty.
It doesn't help if N is chosen to be a given multiple of the difficulty at the time a block is found. If the difficulty is about to increase, it is more profitable to mine a short while before. For example, if N is set equal to the difficulty D, a share submitted D shares before the increase will be paid the full expectation in the D-window, and then when difficulty increased it will once again go inside the window and have more expected reward. And, like the previous case, shares submitted just before the difficulty change will be rewarded similarly to shares submitted after it.
Another incorrect implementation is to pay all shares in a window given in units of time (sometimes called PPLNM or PPLNH). In addition to the problems above, it has a problem common to all reward systems that use time as a factor, which is that it is more profitable to mine when the current hashrate is higher than the average.
The correct method is as follows.1. Choose a parameter X, which represents multiples of D to include in the window when difficulty is static. X=2 is a good choice.
2. When a share is submitted, assign to it a score of 1/D, where D is the difficulty at the time the share is submitted.
3. When a block is found, pay (sB)/X for the last share (the one before the winning share), where s is the share's score and B is the block reward. Continue backwards, paying each share based on its score, until you reach a share which brings the total score of the shares counted above X. Pay that share the amount (sB)/(tX) * min (r,t), where r is the score required to bring the total to exactly X and t is the score of the winning share. Don't pay any older shares.
4. If the pool has just started, and a block is found before there are shares totaling a score of X, there will be leftover rewards and it should be decided what to do with them. It doesn't matter much, but I recommend that the operator keeps them (in a macroscopic view, if the pool ever changes, this is compensation for the funds needed to cash participants out). Other options include donating to charity, or distributing among the miners.
The expected payout for a share with score s, so that the total score of it and all younger shares is Y, is sB * Max (0, 1-Y/X). It follows that the expected payout per share when it is submitted is always B/D.
The variance in the payout per share is (pB)^2/X. The average number of shares it takes to receive payment, in multiples of the difficulty, is X/2. Thus X can be chosen to have the desired tradeoff between variance and maturity time, with the invariant being that their product is (1/2)*(pB)^2. Note that for a miner who constitutes a significant portion of the pool, the shares are correlated - if the portion is q, the total steady-state variance can't be lower than q times solo variance.
If the parameter X ever needs to be changed (for example, to harness increases in the pool's size to decrease variance without changing maturity in units of time), the scores of all recent shares should be changed so that their new expected remaining payout will be equal to the old one, starting from the youngest share and moving backwards. If X is increased, scores should be decreased. This creates a situation similar to the start of the pool where there could be leftover rewards. If X is decreased, scores should be increased. For some of the older shares, it will be impossible to increase the score to maintain the expected value. In this case they should be "cashed out", with the operator paying from his own funds their expected reward.
A variant of this is, instead of using a 0-1 cutoff, use an exponential decay function. This is equivalent to the
double geometric method with o=1. If shares decay by a factor of e every (XD/2) shares, the variance is (pB)^2/X and the maturity time is X/2, just like with 0-1. The advantage is that the due payment of a miner can be encoded with a single number, his score - there's no need to keep or handle a history of shares. The disadvantages are that it takes longer to receive the reward in full, and that the implementation requires either a logarithmic scale or periodic rescaling.
Another variant is to use a linear decay function. This achieves a better variance-maturity tradeoff invariant, (4/9)*(pB)^2. However, it is probably harder to implement and understand.
A substantially different variant is to pay for every share at most once. If, when going backwards in the list of shares, we encounter some that were already paid, we skip them and move on to older shares. X needs to be less than 1 to make sure that eventually we will have enough unpaid shares to draw on. I'm not sure what's the correct way to handle difficulty changes with this variant.
Edit: Fixed an error in the description of the block payment.